First commit
This commit is contained in:
commit
e0a61196ce
8 changed files with 529 additions and 0 deletions
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
|
@ -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/
|
90
build.zig
Normal file
90
build.zig
Normal file
|
@ -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);
|
||||
}
|
73
src/curl.zig
Normal file
73
src/curl.zig
Normal file
|
@ -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);
|
||||
}
|
22
src/departure.zig
Normal file
22
src/departure.zig
Normal file
|
@ -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();
|
||||
}
|
192
src/home.zig
Normal file
192
src/home.zig
Normal file
|
@ -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();
|
||||
}
|
46
src/main.zig
Normal file
46
src/main.zig
Normal file
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
52
src/raylib.zig
Normal file
52
src/raylib.zig
Normal file
|
@ -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;
|
||||
}
|
36
src/state.zig
Normal file
36
src/state.zig
Normal file
|
@ -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…
Add table
Reference in a new issue