From 277f87b8bbede6623fc74cf8980667b4de7596f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Thu, 8 May 2025 10:13:59 +0200 Subject: [PATCH] feat(gui): base project --- build.zig | 44 +++++++-- build.zig.zon | 4 + src/app_common/Model.zig | 14 +++ src/app_common/root.zig | 44 +++++++++ src/console/main.zig | 91 ++++-------------- src/gui/main.zig | 194 +++++++++++++++++++++++++++++++++++++++ src/gui_main.zig | 5 + 7 files changed, 314 insertions(+), 82 deletions(-) create mode 100644 src/app_common/Model.zig create mode 100644 src/app_common/root.zig create mode 100644 src/gui/main.zig create mode 100644 src/gui_main.zig diff --git a/build.zig b/build.zig index 9ffb10d..ebbe583 100644 --- a/build.zig +++ b/build.zig @@ -10,10 +10,20 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); const console_exe = b.addExecutable(.{ - .name = "FileTime3", + .name = "ftime", .root_module = console_exe_mod, }); + const gui_exe_mod = b.createModule(.{ + .root_source_file = b.path("src/gui_main.zig"), + .target = target, + .optimize = optimize, + }); + const gui_exe = b.addExecutable(.{ + .name = "filetime", + .root_module = gui_exe_mod, + }); + const vaxis = b.dependency("vaxis", .{ .target = target, .optimize = optimize, @@ -22,22 +32,38 @@ pub fn build(b: *std.Build) void { console_exe.root_module.addImport("vaxis", vaxis.module("vaxis")); b.installArtifact(console_exe); + const dvui = b.dependency("dvui", .{ + .target = target, + .optimize = optimize, + .backend = .raylib, + }); + + gui_exe.root_module.addImport("dvui", dvui.module("dvui_raylib")); + b.installArtifact(gui_exe); + const run_console_cmd = b.addRunArtifact(console_exe); run_console_cmd.step.dependOn(b.getInstallStep()); + const run_gui_cmd = b.addRunArtifact(gui_exe); + run_gui_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { run_console_cmd.addArgs(args); + run_gui_cmd.addArgs(args); } - const run_console_step = b.step("run", "Run the console app"); + const run_console_step = b.step("run:console", "Run the console app"); run_console_step.dependOn(&run_console_cmd.step); - const console_unit_tests = b.addTest(.{ - .root_module = console_exe_mod, - }); + const run_gui_step = b.step("run:gui", "Run the gui app"); + run_gui_step.dependOn(&run_gui_cmd.step); - const run_console_unit_tests = b.addRunArtifact(console_unit_tests); - - const test_step = b.step("test", "Run unit console tests"); - test_step.dependOn(&run_console_unit_tests.step); + // const console_unit_tests = b.addTest(.{ + // .root_module = console_exe_mod, + // }); + // + // const run_console_unit_tests = b.addRunArtifact(console_unit_tests); + // + // const test_step = b.step("test", "Run unit console tests"); + // test_step.dependOn(&run_console_unit_tests.step); } diff --git a/build.zig.zon b/build.zig.zon index 6854420..87ee252 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -40,6 +40,10 @@ .url = "git+https://github.com/rockorager/libvaxis.git#ae71b6545c099e73d45df7e5dd7c7a3081839468", .hash = "vaxis-0.1.0-BWNV_JEMCQBskZQsnlzh6GoyHSDgOi41bCoZIB2pW-E7", }, + .dvui = .{ + .url = "git+https://github.com/david-vanderson/dvui.git#5703f8f02fb8e1bf2c866517f208745e4d7af869", + .hash = "dvui-0.2.0-AQFJmXGezQAwyj44yNdQ3oZ2XkMybh9r6ltiMEOKIwT6", + }, }, .paths = .{ "build.zig", diff --git a/src/app_common/Model.zig b/src/app_common/Model.zig new file mode 100644 index 0000000..69365f8 --- /dev/null +++ b/src/app_common/Model.zig @@ -0,0 +1,14 @@ +const std = @import("std"); +const models = @import("../core/models.zig"); +const Tab = @import("../core/tab/tab.zig").Tab; +const locked = @import("../core/sync.zig").locked; + +running: bool = true, +usage_number: locked(u16) = .{ .data = 0 }, +current_items: locked(?[]*models.Item) = .{ .data = null }, +current_items_allocator: std.heap.ArenaAllocator, +tab: *Tab, + +pub fn deinit(self: *@This()) void { + self.current_items_allocator.deinit(); +} diff --git a/src/app_common/root.zig b/src/app_common/root.zig new file mode 100644 index 0000000..93e4f1c --- /dev/null +++ b/src/app_common/root.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const models = @import("../core/models.zig"); +const Model = @import("Model.zig"); + +pub fn data_loop(vm: *Model) void { + vm.usage_number.data += 1; + while (vm.running) { + inner_loop(vm) catch {}; + std.Thread.sleep(100 * std.time.ns_per_ms); + } + vm.usage_number.data -= 1; +} +fn inner_loop(vm: *Model) !void { + if (vm.tab.currentItemsChanged) { + std.Thread.sleep(10 * std.time.ns_per_ms); + + vm.tab.currentItems.mutex.lock(); + defer vm.tab.currentItems.mutex.unlock(); + + if (vm.tab.currentItems.data) |current_items| { + _ = vm.current_items_allocator.reset(.retain_capacity); + const allocator = vm.current_items_allocator.allocator(); + + const items = try allocator.alloc(*models.Item, current_items.items.len); + for (current_items.items, 0..) |item, i| { + items[i] = item; + } + + std.mem.sort(*models.Item, items, {}, struct { + fn sort(_: void, lhs: *models.Item, rhs: *models.Item) bool { + if (lhs.item == .container and rhs.item == .element) return true; + if (lhs.item == .element and rhs.item == .container) return false; + return std.mem.order(u8, lhs.displayName, rhs.displayName) == .lt; + } + }.sort); + + vm.current_items.mutex.lock(); + defer vm.current_items.mutex.unlock(); + vm.current_items.data = items; + + vm.tab.currentItemsChanged = false; + } + } +} diff --git a/src/console/main.zig b/src/console/main.zig index 3e3c62a..2afdc16 100644 --- a/src/console/main.zig +++ b/src/console/main.zig @@ -7,19 +7,15 @@ const provider = @import("../core/provider/provider.zig"); const local_provider = @import("../core/provider/local.zig"); const Tab = @import("../core/tab/tab.zig").Tab; const locked = @import("../core/sync.zig").locked; +const CoreModel = @import("../app_common/Model.zig"); +const core = @import("../app_common/root.zig"); /// Our main application state const Model = struct { crash: bool = false, - usage_number: locked(u16) = .{ .data = 0 }, - running: bool = true, allocator: std.mem.Allocator, - current_items: locked(?[]*models.Item) = .{ .data = null }, current_items_view: *vxfw.ListView, - current_items_allocator: std.heap.ArenaAllocator, - tab: *Tab, - /// State of the counter - count: usize = 0, + core_model: CoreModel, /// Helper function to return a vxfw.Widget struct pub fn widget(self: *Model) vxfw.Widget { @@ -97,7 +93,7 @@ const Model = struct { }; try rootWidgets.append(list_surface); - const current_location_text = if (vm.tab.currentLocation) |loc| loc.item.fullName.path else ""; + const current_location_text = if (vm.core_model.tab.currentLocation) |loc| loc.item.fullName.path else ""; const current_location_text_element = try ctx.arena.create(vxfw.Text); current_location_text_element.* = vxfw.Text{ .text = current_location_text, @@ -126,9 +122,9 @@ const Model = struct { fn createCurrentItems(ctx: vxfw.DrawContext, vm: *Model) !void { if (vm.crash) @panic("asd123"); - vm.current_items.mutex.lock(); - defer vm.current_items.mutex.unlock(); - const text_items = if (vm.current_items.data) |items| blk: { + vm.core_model.current_items.mutex.lock(); + defer vm.core_model.current_items.mutex.unlock(); + const text_items = if (vm.core_model.current_items.data) |items| blk: { const children = try ctx.arena.alloc(*vxfw.Text, items.len); for (0.., items[0..children.len]) |i, child| { const is_active = i == vm.current_items_view.cursor; @@ -179,60 +175,8 @@ const Model = struct { vm.current_items_view.children.slice = widgets; } - - fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void { - const ptr = maybe_ptr orelse return; - const self: *Model = @ptrCast(@alignCast(ptr)); - self.count +|= 1; - return ctx.consumeAndRedraw(); - } }; -fn data_loop(vm: *Model) void { - vm.usage_number.data += 1; - while (vm.running) { - inner_loop(vm) catch {}; - std.Thread.sleep(100 * std.time.ns_per_ms); - } - vm.usage_number.data -= 1; -} -fn inner_loop(vm: *Model) !void { - if (vm.tab.currentItemsChanged) { - std.Thread.sleep(10 * std.time.ns_per_ms); - - vm.tab.currentItems.mutex.lock(); - defer vm.tab.currentItems.mutex.unlock(); - - if (vm.tab.currentItems.data) |current_items| { - _ = vm.current_items_allocator.reset(.retain_capacity); - const allocator = vm.current_items_allocator.allocator(); - - const items = try allocator.alloc(*models.Item, current_items.items.len); - for (current_items.items, 0..) |item, i| { - items[i] = item; - } - - std.mem.sort(*models.Item, items, {}, struct { - fn sort(_: void, lhs: *models.Item, rhs: *models.Item) bool { - if (lhs.item == .container and rhs.item == .element) return true; - if (lhs.item == .element and rhs.item == .container) return false; - return std.mem.order(u8, lhs.displayName, rhs.displayName) == .lt; - } - }.sort); - - vm.current_items.mutex.lock(); - defer vm.current_items.mutex.unlock(); - vm.current_items.data = items; - - vm.tab.currentItemsChanged = false; - } - } - - vm.tab.currentItems.mutex.lock(); - defer vm.tab.currentItems.mutex.unlock(); - vm.count = if (vm.tab.currentItems.data) |c| c.items.len else 999; -} - pub fn main() !void { const DebugAllocator = std.heap.DebugAllocator(.{}); var gpa = DebugAllocator{}; @@ -277,7 +221,6 @@ pub fn main() !void { const homeFullName: models.FullName = .{ .path = "/home/adam" }; const homeItem = try localContentProvider.getItemByFullName(homeFullName, &.{}, allocator); - // defer homeItem.deinit(); const c = switch (homeItem.item) { .container => |c| c, .element => unreachable, @@ -294,6 +237,7 @@ pub fn main() !void { const model = try allocator.create(Model); defer allocator.destroy(model); + // TODO: remove std.Thread.sleep(1 * std.time.ns_per_s); const empty_text_element = try allocator.create(vxfw.Text); @@ -318,16 +262,17 @@ pub fn main() !void { }; model.* = .{ .allocator = allocator, - .current_items_allocator = std.heap.ArenaAllocator.init(allocator), + .core_model = .{ + .current_items_allocator = std.heap.ArenaAllocator.init(allocator), + .tab = tab1, + }, .current_items_view = &list, - .tab = tab1, - .count = 0, }; - defer model.current_items_allocator.deinit(); + defer model.core_model.deinit(); - model.usage_number.data += 1; + model.core_model.usage_number.data += 1; - try pool.spawn(data_loop, .{model}); + try pool.spawn(core.data_loop, .{&model.core_model}); var app = try vxfw.App.init(allocator); defer app.deinit(); @@ -335,10 +280,10 @@ pub fn main() !void { try app.run(model.widget(), .{}); // std.Thread.sleep(10 * std.time.ns_per_s); - model.usage_number.data -= 1; - model.running = false; + model.core_model.usage_number.data -= 1; + model.core_model.running = false; - while (model.usage_number.data > 0) { + while (model.core_model.usage_number.data > 0) { std.Thread.sleep(10 * std.time.ns_per_ms); } } diff --git a/src/gui/main.zig b/src/gui/main.zig new file mode 100644 index 0000000..057a919 --- /dev/null +++ b/src/gui/main.zig @@ -0,0 +1,194 @@ +const std = @import("std"); +const CoreModel = @import("../app_common/Model.zig"); +const models = @import("../core/models.zig"); +const provider = @import("../core/provider/provider.zig"); +const local_provider = @import("../core/provider/local.zig"); +const Tab = @import("../core/tab/tab.zig").Tab; +const core = @import("../app_common/root.zig"); + +const dvui = @import("dvui"); +const RaylibBackend = dvui.backend; +comptime { + std.debug.assert(@hasDecl(RaylibBackend, "RaylibBackend")); +} + +const vsync = true; +var scale_val: f32 = 1.0; + +var show_dialog_outside_frame: bool = false; + +pub const c = RaylibBackend.c; + +const Model = struct { + allocator: std.mem.Allocator, + core_model: CoreModel, + selected_item: u32 = 0, +}; + +/// This example shows how to use the dvui for a normal application: +/// - dvui renders the whole application +/// - render frames only when needed +pub fn main() !void { + var gpa_instance = std.heap.GeneralPurposeAllocator(.{}){}; + const gpa = gpa_instance.allocator(); + defer { + _ = gpa_instance.detectLeaks(); + _ = gpa_instance.deinit(); + } + + var tsa = std.heap.ThreadSafeAllocator{ .child_allocator = gpa }; + const allocator = tsa.allocator(); + + var pool: std.Thread.Pool = undefined; + try pool.init(.{ + .allocator = allocator, + }); + defer pool.deinit(); + + var localContentProvider = local_provider.LocalContentProvider{ .threadPool = &pool }; + + const homeFullName: models.FullName = .{ .path = "/home/adam" }; + const homeItem = try localContentProvider.getItemByFullName(homeFullName, &.{}, allocator); + const startLocation = switch (homeItem.item) { + .container => |c1| c1, + .element => unreachable, + }; + + var tab1 = try allocator.create(Tab); + defer allocator.destroy(tab1); + + tab1.init(&pool, allocator); + defer tab1.deinit(); + + tab1.setCurrentLocation(startLocation); + + const model = try allocator.create(Model); + defer allocator.destroy(model); + + model.* = .{ + .allocator = allocator, + .core_model = .{ + .current_items_allocator = std.heap.ArenaAllocator.init(allocator), + .tab = tab1, + }, + }; + defer model.core_model.deinit(); + try pool.spawn(core.data_loop, .{&model.core_model}); + + // init Raylib backend (creates OS window) + // initWindow() means the backend calls CloseWindow for you in deinit() + var backend = try RaylibBackend.initWindow(.{ + .gpa = gpa, + .size = .{ .w = 800.0, .h = 600.0 }, + .vsync = vsync, + .title = "DVUI Raylib Standalone Example", + // .icon = window_icon_png, // can also call setIconFromFileContent() + }); + defer backend.deinit(); + backend.log_events = true; + + // init dvui Window (maps onto a single OS window) + var win = try dvui.Window.init(@src(), gpa, backend.backend(), .{}); + defer win.deinit(); + + main_loop: while (true) { + c.BeginDrawing(); + + // Raylib does not support waiting with event interruption, so dvui + // can't do variable framerate. So can't call win.beginWait() or + // win.waitTime(). + try win.begin(std.time.nanoTimestamp()); + + // send all events to dvui for processing + const quit = try backend.addAllEvents(&win); + if (quit) break :main_loop; + + // if dvui widgets might not cover the whole window, then need to clear + // the previous frame's render + backend.clear(); + + for (dvui.events()) |*e| { + switch (e.evt) { + .key => |ke| { + if (ke.action == .down or ke.action == .repeat) { + switch (ke.code) { + .down => model.selected_item +|= 1, + .up => model.selected_item -|= 1, + else => {}, + } + } + }, + else => {}, + } + } + + try dvui_frame(model); + + // marks end of dvui frame, don't call dvui functions after this + // - sends all dvui stuff to backend for rendering, must be called before renderPresent() + _ = try win.end(.{}); + + // cursor management + backend.setCursor(win.cursorRequested()); + + // render frame to OS + c.EndDrawing(); + } +} + +fn dvui_frame(model: *Model) !void { + { + model.core_model.current_items.mutex.lock(); + defer model.core_model.current_items.mutex.unlock(); + + if (model.core_model.current_items.data) |current_items| { + for (0.., current_items) |i, item| { + const is_active = i == model.selected_item; + const fg, const bg = colors: { + var fg: dvui.Color = .{ .r = 100, .g = 100, .b = 100 }; + var bg: dvui.Color = .{ .r = 100, .g = 100, .b = 100 }; + if (is_active) { + fg = switch (item.item) { + .container => .{ .r = 100, .g = 100, .b = 100 }, + .element => .{ .r = 100, .g = 100, .b = 100 }, + }; + bg = switch (item.item) { + .container => .{ .r = 200, .g = 200, .b = 200 }, + .element => .{ .r = 200, .g = 200, .b = 200 }, + }; + } else { + fg = switch (item.item) { + .container => .{ .r = 100, .g = 100, .b = 100 }, + .element => .{ .r = 100, .g = 100, .b = 100 }, + }; + bg = .{ .r = 100, .g = 100, .b = 100 }; + } + break :colors .{ fg, bg }; + }; + + _ = fg; + + var tl = try dvui.textLayout(@src(), .{}, .{ + .id_extra = i, + .expand = .horizontal, + .font_style = .title_4, + .color_fill = .{ .color = bg }, + }); + + const text = try dvui.currentWindow().arena().dupe(u8, item.displayName); + try tl.addText(text, .{}); + tl.deinit(); + } + } else { + var tl = try dvui.textLayout(@src(), .{}, .{ + .expand = .horizontal, + .font_style = .title_4, + .color_fill = .{ .color = .{ .r = 100, .g = 100, .b = 100 } }, + }); + + const lorem = "This example shows how to use dvui in a normal application."; + try tl.addText(lorem, .{}); + tl.deinit(); + } + } +} diff --git a/src/gui_main.zig b/src/gui_main.zig new file mode 100644 index 0000000..1b00516 --- /dev/null +++ b/src/gui_main.zig @@ -0,0 +1,5 @@ +const run = @import("gui/main.zig").main; + +pub fn main() !void { + try run(); +}