Kenneth Bruen
9 months ago
commit
e0a61196ce
8 changed files with 529 additions and 0 deletions
@ -0,0 +1,18 @@
|
||||
# This file is for zig-specific build artifacts. |
||||
# If you have OS-specific or editor-specific files to ignore, |
||||
# such as *.swp or .DS_Store, put those in your global |
||||
# ~/.gitignore and put this in your ~/.gitconfig: |
||||
# |
||||
# [core] |
||||
# excludesfile = ~/.gitignore |
||||
# |
||||
# Cheers! |
||||
# -andrewrk |
||||
|
||||
zig-cache/ |
||||
zig-out/ |
||||
/release/ |
||||
/debug/ |
||||
/build/ |
||||
/build-*/ |
||||
/docgen_tmp/ |
@ -0,0 +1,90 @@
|
||||
const std = @import("std"); |
||||
|
||||
// Although this function looks imperative, note that its job is to |
||||
// declaratively construct a build graph that will be executed by an external |
||||
// runner. |
||||
pub fn build(b: *std.Build) void { |
||||
// Standard target options allows the person running `zig build` to choose |
||||
// what target to build for. Here we do not override the defaults, which |
||||
// means any target is allowed, and the default is native. Other options |
||||
// for restricting supported target set are available. |
||||
const target = b.standardTargetOptions(.{}); |
||||
|
||||
// Standard optimization options allow the person running `zig build` to select |
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not |
||||
// set a preferred release mode, allowing the user to decide how to optimize. |
||||
const optimize = b.standardOptimizeOption(.{}); |
||||
|
||||
const exe = b.addExecutable(.{ |
||||
.name = "raylib-test", |
||||
// In this case the main source file is merely a path, however, in more |
||||
// complicated build scripts, this could be a generated file. |
||||
.root_source_file = .{ .path = "src/main.zig" }, |
||||
.target = target, |
||||
.optimize = optimize, |
||||
}); |
||||
|
||||
// Link Raylib |
||||
exe.addLibraryPath(.{ |
||||
.cwd_relative = "/opt/homebrew/Cellar/raylib/5.0/lib", |
||||
}); |
||||
exe.linkSystemLibrary("raylib"); |
||||
exe.linkSystemLibrary("curl"); |
||||
// exe.addObjectFile(.{ |
||||
// .cwd_relative = "/opt/homebrew/Cellar/raylib/5.0/lib/libraylib.a", |
||||
// }); |
||||
exe.addIncludePath(.{ |
||||
.cwd_relative = "/opt/homebrew/Cellar/raylib/5.0/include", |
||||
}); |
||||
// Raylib dependencies |
||||
exe.linkFramework("Foundation"); |
||||
exe.linkFramework("CoreVideo"); |
||||
exe.linkFramework("IOKit"); |
||||
exe.linkFramework("Cocoa"); |
||||
exe.linkFramework("GLUT"); |
||||
exe.linkFramework("OpenGL"); |
||||
|
||||
// This declares intent for the executable to be installed into the |
||||
// standard location when the user invokes the "install" step (the default |
||||
// step when running `zig build`). |
||||
b.installArtifact(exe); |
||||
|
||||
// This *creates* a Run step in the build graph, to be executed when another |
||||
// step is evaluated that depends on it. The next line below will establish |
||||
// such a dependency. |
||||
const run_cmd = b.addRunArtifact(exe); |
||||
|
||||
// By making the run step depend on the install step, it will be run from the |
||||
// installation directory rather than directly from within the cache directory. |
||||
// This is not necessary, however, if the application depends on other installed |
||||
// files, this ensures they will be present and in the expected location. |
||||
run_cmd.step.dependOn(b.getInstallStep()); |
||||
|
||||
// This allows the user to pass arguments to the application in the build |
||||
// command itself, like this: `zig build run -- arg1 arg2 etc` |
||||
if (b.args) |args| { |
||||
run_cmd.addArgs(args); |
||||
} |
||||
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu, |
||||
// and can be selected like this: `zig build run` |
||||
// This will evaluate the `run` step rather than the default, which is "install". |
||||
const run_step = b.step("run", "Run the app"); |
||||
run_step.dependOn(&run_cmd.step); |
||||
|
||||
// Creates a step for unit testing. This only builds the test executable |
||||
// but does not run it. |
||||
const unit_tests = b.addTest(.{ |
||||
.root_source_file = .{ .path = "src/main.zig" }, |
||||
.target = target, |
||||
.optimize = optimize, |
||||
}); |
||||
|
||||
const run_unit_tests = b.addRunArtifact(unit_tests); |
||||
|
||||
// Similar to creating the run step earlier, this exposes a `test` step to |
||||
// the `zig build --help` menu, providing a way for the user to request |
||||
// running the unit tests. |
||||
const test_step = b.step("test", "Run unit tests"); |
||||
test_step.dependOn(&run_unit_tests.step); |
||||
} |
@ -0,0 +1,73 @@
|
||||
pub const c_api = @cImport({ |
||||
@cInclude("curl/curl.h"); |
||||
}); |
||||
|
||||
pub const Option = enum(c_api.CURLoption) { |
||||
write_data = c_api.CURLOPT_WRITEDATA, |
||||
url = c_api.CURLOPT_URL, |
||||
port = c_api.CURLOPT_PORT, |
||||
proxy = c_api.CURLOPT_PROXY, |
||||
userpwd = c_api.CURLOPT_USERPWD, |
||||
proxy_userpwd = c_api.CURLOPT_PROXYUSERPWD, |
||||
range = c_api.CURLOPT_RANGE, |
||||
read_data = c_api.CURLOPT_READDATA, |
||||
error_buffer = c_api.CURLOPT_ERRORBUFFER, |
||||
write_function = c_api.CURLOPT_WRITEFUNCTION, |
||||
read_function = c_api.CURLOPT_READFUNCTION, |
||||
timeout = c_api.CURLOPT_TIMEOUT, |
||||
in_file_size = c_api.CURLOPT_INFILESIZE, |
||||
post_fields = c_api.CURLOPT_POSTFIELDS, |
||||
referer = c_api.CURLOPT_REFERER, |
||||
ftp_port = c_api.CURLOPT_FTPPORT, |
||||
user_agent = c_api.CURLOPT_USERAGENT, |
||||
low_speed_limit = c_api.CURLOPT_LOW_SPEED_LIMIT, |
||||
low_speed_time = c_api.CURLOPT_LOW_SPEED_TIME, |
||||
resume_from = c_api.CURLOPT_RESUME_FROM, |
||||
cookie = c_api.CURLOPT_COOKIE, |
||||
http_header = c_api.CURLOPT_HTTPHEADER, |
||||
// ... |
||||
}; |
||||
|
||||
handle: *c_api.CURL, |
||||
|
||||
pub fn init() ?@This() { |
||||
if (c_api.curl_easy_init()) |handle| { |
||||
return .{ |
||||
.handle = handle, |
||||
}; |
||||
} else { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
pub fn deinit(self: *@This()) void { |
||||
c_api.curl_easy_cleanup(self.handle); |
||||
} |
||||
|
||||
pub fn reset(self: *@This()) void { |
||||
c_api.curl_easy_reset(self.handle); |
||||
} |
||||
|
||||
pub fn perform(self: *@This()) c_api.CURLcode { |
||||
return c_api.curl_easy_perform(self.handle); |
||||
} |
||||
|
||||
pub fn setopt_raw( |
||||
self: *@This(), |
||||
option: c_api.CURLoption, |
||||
args: anytype, |
||||
) c_api.CURLcode { |
||||
return @call( |
||||
.auto, |
||||
c_api.curl_easy_setopt, |
||||
.{ self.handle, option } ++ args, |
||||
); |
||||
} |
||||
|
||||
pub fn setopt( |
||||
self: *@This(), |
||||
option: Option, |
||||
args: anytype, |
||||
) c_api.CURLcode { |
||||
return self.setopt_raw(@intFromEnum(option), args); |
||||
} |
@ -0,0 +1,22 @@
|
||||
const raylib = @import("raylib.zig"); |
||||
const rl = raylib.rl; |
||||
const stateMod = @import("state.zig"); |
||||
|
||||
pub fn render(state: *stateMod.AppState) !void { |
||||
while (raylib.GetKeyPressed()) |key| { |
||||
switch (key) { |
||||
rl.KEY_LEFT => { |
||||
state.screen = .home; |
||||
}, |
||||
else => {}, |
||||
} |
||||
} |
||||
|
||||
rl.BeginDrawing(); |
||||
defer rl.EndDrawing(); |
||||
|
||||
rl.ClearBackground(raylib.ColorInt(0x18226f)); |
||||
rl.DrawText(state.departure_screen_state.station_id.items.ptr, 16, 16, 32, rl.WHITE); |
||||
|
||||
state.close_app = rl.WindowShouldClose(); |
||||
} |
@ -0,0 +1,192 @@
|
||||
const std = @import("std"); |
||||
const raylib = @import("raylib.zig"); |
||||
const rl = raylib.rl; |
||||
const state_mod = @import("state.zig"); |
||||
const curl_mod = @import("curl.zig"); |
||||
|
||||
fn curlWriteHandler(ptr: [*]u8, size: usize, nmemb: usize, userdata: *std.ArrayList(u8)) callconv(.C) usize { |
||||
_ = size; |
||||
userdata.appendSlice(ptr[0..nmemb]) catch return 0; |
||||
return nmemb; |
||||
} |
||||
|
||||
fn fetchThread(state: *state_mod.AppState) !void { |
||||
std.debug.print("Started fetchThread\n", .{}); |
||||
defer std.debug.print("Ended fetchThread\n", .{}); |
||||
defer state.home_screen_state.fetch_thread = null; |
||||
const allocator = state.allocator; |
||||
var station_name_buf = std.BoundedArray(u8, 200){}; |
||||
var curl = curl_mod.init() orelse return; |
||||
defer curl.deinit(); |
||||
const locations_base = "https://v6.db.transport.rest/locations"; |
||||
var locations_uri = std.Uri.parse(locations_base) catch unreachable; |
||||
|
||||
while (state.home_screen_state.fetch_thread != null) { |
||||
if (std.mem.eql(u8, station_name_buf.slice(), state.home_screen_state.station_name.items)) { |
||||
std.time.sleep(100 * 1000); |
||||
continue; |
||||
} |
||||
|
||||
station_name_buf.resize(state.home_screen_state.station_name.items.len) catch continue; |
||||
std.mem.copyForwards(u8, station_name_buf.slice(), state.home_screen_state.station_name.items); |
||||
|
||||
std.debug.print("[fetchThread] Detected update: {s}\n", .{station_name_buf.slice()}); |
||||
|
||||
curl.reset(); |
||||
|
||||
const query = try std.fmt.allocPrint(allocator, "query={s}&results=10&addresses=false&poi=false&pretty=false", .{station_name_buf.slice()}); |
||||
defer allocator.free(query); |
||||
locations_uri.query = query; |
||||
defer locations_uri.query = null; |
||||
std.debug.print("[fetchThread] Making request to: {}\n", .{locations_uri}); |
||||
|
||||
const url = try std.fmt.allocPrintZ(allocator, "{}", .{locations_uri}); |
||||
defer allocator.free(url); |
||||
_ = curl.setopt(.url, .{url.ptr}); |
||||
|
||||
var result = std.ArrayList(u8).init(allocator); |
||||
defer result.deinit(); |
||||
_ = curl.setopt(.write_function, .{curlWriteHandler}); |
||||
_ = curl.setopt(.write_data, .{&result}); |
||||
|
||||
const code = curl.perform(); |
||||
std.debug.print("[fetchThread] cURL Code: {}\n", .{code}); |
||||
if (code != 0) continue; |
||||
|
||||
std.debug.print("[fetchThread] Fetched data: <redacted>(len: {})\n", .{result.items.len}); |
||||
const parsed = std.json.parseFromSlice([]const std.json.Value, allocator, result.items, .{}) catch |err| { |
||||
std.debug.print("[fetchThread] JSON parse error: {}\n", .{err}); |
||||
continue; |
||||
}; |
||||
defer parsed.deinit(); |
||||
|
||||
var results = std.ArrayList(state_mod.HSSuggestion).init(allocator); |
||||
for (parsed.value) |station| { |
||||
if (station.object.get("name")) |nameValue| { |
||||
const name = nameValue.string; |
||||
if (station.object.get("id")) |idValue| { |
||||
const id = idValue.string; |
||||
|
||||
results.append(.{ |
||||
.id = std.fmt.allocPrintZ(allocator, "{s}", .{id}) catch continue, |
||||
.name = std.fmt.allocPrintZ(allocator, "{s}", .{name}) catch continue, |
||||
}) catch continue; |
||||
} |
||||
} |
||||
} |
||||
if (state.home_screen_state.suggestions.len > 0) { |
||||
for (state.home_screen_state.suggestions) |suggestion| { |
||||
allocator.free(suggestion.id); |
||||
allocator.free(suggestion.name); |
||||
} |
||||
allocator.free(state.home_screen_state.suggestions); |
||||
} |
||||
state.home_screen_state.suggestions = results.toOwnedSlice() catch continue; |
||||
} |
||||
} |
||||
|
||||
pub fn render(state: *state_mod.AppState) !void { |
||||
var hs = &state.home_screen_state; |
||||
|
||||
if (hs.fetch_thread == null) { |
||||
hs.fetch_thread = std.Thread.spawn(.{}, fetchThread, .{state}) catch null; |
||||
} |
||||
if (hs.suggestions.len > 0 and hs.selection_idx > hs.suggestions.len - 1) { |
||||
hs.selection_idx = @intCast(hs.suggestions.len - 1); |
||||
} |
||||
|
||||
while (raylib.GetCharPressed()) |char| { |
||||
hs.station_name.appendAssumeCapacity(@intCast(char)); |
||||
} |
||||
while (raylib.GetKeyPressed()) |key| { |
||||
switch (key) { |
||||
rl.KEY_BACKSPACE => { |
||||
if (hs.station_name.items.len > 0) { |
||||
hs.station_name.items[hs.station_name.items.len - 1] = 0; |
||||
_ = hs.station_name.pop(); |
||||
} |
||||
}, |
||||
rl.KEY_UP => { |
||||
hs.selection_idx -= 1; |
||||
if (hs.suggestions.len > 0 and hs.selection_idx < 0) { |
||||
hs.selection_idx = @intCast(hs.suggestions.len - 1); |
||||
} |
||||
}, |
||||
rl.KEY_DOWN => { |
||||
hs.selection_idx += 1; |
||||
if (hs.suggestions.len > 0 and hs.selection_idx > hs.suggestions.len - 1) { |
||||
hs.selection_idx = 0; |
||||
} |
||||
}, |
||||
rl.KEY_ENTER => { |
||||
if (hs.suggestions.len > 0 and hs.selection_idx < hs.suggestions.len) { |
||||
state.departure_screen_state.station_id.clearRetainingCapacity(); |
||||
state.departure_screen_state.station_id.appendSliceAssumeCapacity(hs.suggestions[@intCast(hs.selection_idx)].id); |
||||
state.screen = .departure; |
||||
hs.fetch_thread = null; |
||||
} |
||||
}, |
||||
else => {}, |
||||
} |
||||
} |
||||
|
||||
rl.BeginDrawing(); |
||||
defer rl.EndDrawing(); |
||||
|
||||
var x: c_int = 16; |
||||
var y: c_int = 16; |
||||
|
||||
const title_size: c_int = 32; |
||||
const body_size: c_int = 28; |
||||
|
||||
rl.ClearBackground(rl.BLACK); |
||||
x += raylib.DrawAndMeasureText("Station: ", x, y, title_size, rl.WHITE).width + 8; |
||||
rl.DrawLine(x, y + title_size + 2, rl.GetScreenWidth() - 16, y + title_size + 2, rl.WHITE); |
||||
if (state.db_font) |db_font| { |
||||
rl.DrawTextEx(db_font, hs.station_name.items.ptr, rl.Vector2{ .x = @floatFromInt(x), .y = @floatFromInt(y) }, title_size, 0.9, rl.WHITE); |
||||
} else { |
||||
rl.DrawText(hs.station_name.items.ptr, x, y, title_size, rl.WHITE); |
||||
} |
||||
|
||||
y += title_size + 2 + 16; |
||||
|
||||
for (hs.suggestions, 0..) |suggestion, idx| { |
||||
var color = if (hs.selection_idx == idx) rl.YELLOW else rl.WHITE; |
||||
|
||||
// Draw arrow for selection |
||||
if (hs.selection_idx == idx) { |
||||
const arrow_margin: c_int = 16; |
||||
rl.DrawLine(x - 10 - arrow_margin, y + body_size / 4, x - arrow_margin, y + body_size / 2, color); |
||||
rl.DrawLine(x - arrow_margin, y + body_size / 2, x - 10 - arrow_margin, y + body_size * 3 / 4, color); |
||||
} |
||||
|
||||
// Check if mouse is hovering |
||||
if (rl.CheckCollisionPointRec(rl.GetMousePosition(), rl.Rectangle{ |
||||
.x = @floatFromInt(x), |
||||
.y = @floatFromInt(y), |
||||
.width = @floatFromInt(rl.GetScreenWidth() - 16 - x), |
||||
.height = @floatFromInt(body_size), |
||||
})) { |
||||
color = rl.BLUE; |
||||
|
||||
if (rl.IsMouseButtonPressed(rl.MOUSE_BUTTON_LEFT)) { |
||||
// Select |
||||
state.departure_screen_state.station_id.clearRetainingCapacity(); |
||||
state.departure_screen_state.station_id.appendSliceAssumeCapacity(suggestion.id); |
||||
state.screen = .departure; |
||||
hs.fetch_thread = null; |
||||
return; |
||||
} |
||||
} |
||||
|
||||
if (state.db_font) |db_font| { |
||||
rl.DrawTextEx(db_font, suggestion.name.ptr, rl.Vector2{ .x = @floatFromInt(x), .y = @floatFromInt(y) }, body_size, 0.9, color); |
||||
} else { |
||||
rl.DrawText(suggestion.name.ptr, x, y, body_size, color); |
||||
} |
||||
|
||||
y += body_size + 2; |
||||
} |
||||
|
||||
state.close_app = rl.WindowShouldClose(); |
||||
} |
@ -0,0 +1,46 @@
|
||||
const std = @import("std"); |
||||
const raylib = @import("raylib.zig"); |
||||
const rl = raylib.rl; |
||||
const stateMod = @import("state.zig"); |
||||
const home = @import("home.zig"); |
||||
const departure = @import("departure.zig"); |
||||
|
||||
pub fn main() !void { |
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; |
||||
const allocator = gpa.allocator(); |
||||
|
||||
rl.SetConfigFlags(rl.FLAG_WINDOW_RESIZABLE | rl.FLAG_VSYNC_HINT); |
||||
rl.SetTargetFPS(60); |
||||
rl.InitWindow(800, 600, "Testing Raylib"); |
||||
defer rl.CloseWindow(); |
||||
|
||||
// const font = blk: { |
||||
// const maybeFont = rl.LoadFontEx("./db.ttf", 64, null, 0); |
||||
// if (std.meta.eql(maybeFont, rl.GetFontDefault())) { |
||||
// break :blk null; |
||||
// } |
||||
// break :blk maybeFont; |
||||
// }; |
||||
|
||||
var station_name_buffer: [100]u8 = .{0} ** 100; |
||||
var platform_buffer: [20]u8 = .{0} ** 20; |
||||
var station_id_buffer: [10]u8 = .{0} ** 10; |
||||
var appState = stateMod.AppState{ |
||||
.allocator = allocator, |
||||
// .db_font = font, |
||||
.home_screen_state = .{ |
||||
.station_name = std.ArrayListUnmanaged(u8).initBuffer(&station_name_buffer), |
||||
}, |
||||
.departure_screen_state = .{ |
||||
.platform = std.ArrayListUnmanaged(u8).initBuffer(&platform_buffer), |
||||
.station_id = std.ArrayListUnmanaged(u8).initBuffer(&station_id_buffer), // 7 digit id |
||||
.departure_date = std.time.Instant.now() catch @panic("Idk buddy, hook a wall clock to your CPU ig"), |
||||
}, |
||||
}; |
||||
while (!appState.close_app) { |
||||
switch (appState.screen) { |
||||
.home => try home.render(&appState), |
||||
.departure => try departure.render(&appState), |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,52 @@
|
||||
pub const rl = @cImport({ |
||||
@cInclude("raylib.h"); |
||||
}); |
||||
|
||||
pub fn Color(r: u8, g: u8, b: u8, a: u8) rl.Color { |
||||
return .{ |
||||
.r = r, |
||||
.g = g, |
||||
.b = b, |
||||
.a = a, |
||||
}; |
||||
} |
||||
pub fn ColorInt(whole: u24) rl.Color { |
||||
return ColorIntA(@as(u32, whole) << 8 | 0xFF); |
||||
} |
||||
pub fn ColorIntA(whole: u32) rl.Color { |
||||
return .{ |
||||
// zig fmt: off |
||||
.r = @truncate(whole >> 24), |
||||
.g = @truncate(whole >> 16), |
||||
.b = @truncate(whole >> 8), |
||||
.a = @truncate(whole >> 0), |
||||
// zig fmt: on |
||||
}; |
||||
} |
||||
pub fn DrawAndMeasureText( |
||||
text: [*c]const u8, |
||||
pos_x: c_int, |
||||
pos_y: c_int, |
||||
font_size: c_int, |
||||
color: rl.Color, |
||||
) struct { width: c_int, height: c_int } { |
||||
rl.DrawText(text, pos_x, pos_y, font_size, color); |
||||
return .{ |
||||
.width = rl.MeasureText(text, font_size), |
||||
.height = 10, |
||||
}; |
||||
} |
||||
pub fn GetKeyPressed() ?c_int { |
||||
const result = rl.GetKeyPressed(); |
||||
return if (result == 0) |
||||
null |
||||
else |
||||
result; |
||||
} |
||||
pub fn GetCharPressed() ?c_int { |
||||
const result = rl.GetCharPressed(); |
||||
return if (result == 0) |
||||
null |
||||
else |
||||
result; |
||||
} |
@ -0,0 +1,36 @@
|
||||
const std = @import("std"); |
||||
const raylib = @import("raylib.zig"); |
||||
const rl = raylib.rl; |
||||
|
||||
pub const Screen = enum { |
||||
home, |
||||
departure, |
||||
}; |
||||
|
||||
pub const HSSuggestion = struct { |
||||
id: [:0]u8, |
||||
name: [:0]u8, |
||||
}; |
||||
|
||||
pub const HomeScreenState = struct { |
||||
station_name: std.ArrayListUnmanaged(u8), |
||||
fetch_thread: ?std.Thread = null, |
||||
suggestions: []HSSuggestion = &.{}, |
||||
selection_idx: i8 = 0, |
||||
}; |
||||
|
||||
pub const DepartureScreenState = struct { |
||||
station_id: std.ArrayListUnmanaged(u8), |
||||
platform: std.ArrayListUnmanaged(u8), |
||||
departure_date: std.time.Instant, |
||||
loading: bool = false, |
||||
}; |
||||
|
||||
pub const AppState = struct { |
||||
allocator: std.mem.Allocator, |
||||
close_app: bool = false, |
||||
db_font: ?rl.Font = null, |
||||
screen: Screen = .home, |
||||
home_screen_state: HomeScreenState, |
||||
departure_screen_state: DepartureScreenState, |
||||
}; |
Loading…
Reference in new issue