From c23ee52f053c636ecb05a34ea13ffa91f9186868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Thu, 24 Apr 2025 17:51:33 +0200 Subject: [PATCH] feat(console): lazy load items --- src/console/main.zig | 119 ++++++++++++++++++++++++++++++++----------- src/core/sync.zig | 7 +++ src/core/tab/tab.zig | 36 +++++++++---- 3 files changed, 121 insertions(+), 41 deletions(-) create mode 100644 src/core/sync.zig diff --git a/src/console/main.zig b/src/console/main.zig index 3639bbe..1da8b74 100644 --- a/src/console/main.zig +++ b/src/console/main.zig @@ -5,15 +5,19 @@ const vxfw = vaxis.vxfw; 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"); -const Tab = tab.Tab; +const Tab = @import("../core/tab/tab.zig").Tab; +const locked = @import("../core/sync.zig").locked; /// Our main application state const Model = struct { - current_items: vxfw.ListView, + usage_number: locked(u16) = .{ .data = 0 }, + running: bool = true, + allocator: std.mem.Allocator, + current_items: *vxfw.ListView, + current_items_allocator: ?std.heap.ArenaAllocator = null, tab: *Tab, /// State of the counter - count: u32 = 0, + count: usize = 0, /// The button. This widget is stateful and must live between frames button: vxfw.Button, @@ -28,12 +32,12 @@ const Model = struct { /// This function will be called from the vxfw runtime. fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { - const self: *Model = @ptrCast(@alignCast(ptr)); + const vm: *Model = @ptrCast(@alignCast(ptr)); switch (event) { // The root widget is always sent an init event as the first event. Users of the // library can also send this event to other widgets they create if they need to do // some initialization. - .init => return ctx.requestFocus(self.current_items.widget()), + .init => return ctx.requestFocus(vm.current_items.widget()), .key_press => |key| { if (key.matches('c', .{ .ctrl = true })) { ctx.quit = true; @@ -44,7 +48,7 @@ const Model = struct { // our button. Having focus means that key events will be sent up the widget tree to // the focused widget, and then bubble back down the tree to the root. Users can tell // the runtime the event was handled and the capture or bubble phase will stop - .focus_in => return ctx.requestFocus(self.current_items.widget()), + .focus_in => return ctx.requestFocus(vm.current_items.widget()), else => {}, } } @@ -55,6 +59,7 @@ const Model = struct { /// which don't change state (ie mouse motion, unhandled key events, etc) fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { const vm: *Model = @ptrCast(@alignCast(ptr)); + // The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum // constraint. The minimum constraint will always be set, even if it is set to 0x0. The // maximum constraint can have null width and/or height - meaning there is no constraint in @@ -129,6 +134,59 @@ const Model = struct { } }; +fn updateChildren(vm: *Model) !void { + if (vm.current_items_allocator) |a| { + a.deinit(); + } + + vm.current_items_allocator = std.heap.ArenaAllocator.init(vm.allocator); + const arena_allocator = vm.current_items_allocator.?.allocator(); + + const tab = vm.tab; + var listView = vm.current_items; + + tab.currentItems.mutex.lock(); + defer tab.currentItems.mutex.unlock(); + const children = if (tab.currentItems.data) |items| blk: { + const children = try arena_allocator.alloc(vxfw.Widget, items.items.len); + for (0.., items.items[0..children.len]) |i, child| { + const text1 = try arena_allocator.dupe(u8, child.fullName.path); + + const text_c = try arena_allocator.create(vxfw.Text); + text_c.* = vxfw.Text{ + .text = text1, + .overflow = .clip, + .softwrap = false, + .style = .{ .bold = true }, + }; + children[i] = text_c.widget(); + } + break :blk children; + } else &.{}; + + listView.children.slice = children; +} + +fn loop1(vm: *Model) void { + vm.usage_number.data += 1; + while (vm.running) { + inner_loop(vm) catch {}; + std.Thread.sleep(2000 * std.time.ns_per_ms); + } + vm.usage_number.data -= 1; +} +fn inner_loop(vm: *Model) !void { + if (vm.tab.currentItemsChanged) { + std.Thread.sleep(2000 * std.time.ns_per_ms); + try updateChildren(vm); + 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 { var gpa = std.heap.DebugAllocator(.{}){}; const gp_allocator = gpa.allocator(); @@ -148,9 +206,6 @@ pub fn main() !void { var localContentProvider = local_provider.LocalContentProvider{ .threadPool = &pool }; - var app = try vxfw.App.init(allocator); - defer app.deinit(); - const homeFullName: models.FullName = .{ .path = "/home/adam" }; const homeItem = try localContentProvider.getItemByFullName(homeFullName, &.{}, allocator); // defer homeItem.deinit(); @@ -167,33 +222,17 @@ pub fn main() !void { tab1.setCurrentLocation(c); - // We heap allocate our model because we will require a stable pointer to it in our Button - // widget const model = try allocator.create(Model); defer allocator.destroy(model); - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - const arena_allocator = arena.allocator(); - std.Thread.sleep(1 * std.time.ns_per_s); - const children1 = if (tab1.currentItems) |items| blk: { - const children = try arena_allocator.alloc(vxfw.Widget, items.items.len); - for (0.., items.items[0..children.len]) |i, child| { - const text1 = try arena_allocator.dupe(u8, child.fullName.path); - const text_c = try arena_allocator.create(vxfw.Text); - text_c.* = vxfw.Text{ .text = text1, .overflow = .clip, .softwrap = false, .style = .{ .bold = true } }; - children[i] = text_c.widget(); - } - break :blk children; - } else &.{}; - - const list: vxfw.ListView = .{ - .children = .{ .slice = children1 }, + const list = try allocator.create(vxfw.ListView); + list.* = .{ + .children = .{ .slice = &.{} }, }; - // Set the initial state of our button model.* = .{ + .allocator = allocator, .current_items = list, .tab = tab1, .count = 0, @@ -204,6 +243,24 @@ pub fn main() !void { }, }; + model.usage_number.data += 1; + + try pool.spawn(loop1, .{model}); + + var app = try vxfw.App.init(allocator); + defer app.deinit(); + try app.run(model.widget(), .{}); - std.Thread.sleep(1 * std.time.ns_per_s); + // std.Thread.sleep(10 * std.time.ns_per_s); + + model.usage_number.data -= 1; + model.running = false; + + while (model.usage_number.data > 0) { + std.Thread.sleep(100 * std.time.ns_per_ms); + } + + if (model.current_items_allocator) |a| { + a.deinit(); + } } diff --git a/src/core/sync.zig b/src/core/sync.zig new file mode 100644 index 0000000..cc90373 --- /dev/null +++ b/src/core/sync.zig @@ -0,0 +1,7 @@ +const std = @import("std"); +pub fn locked(comptime T: type) type { + return struct { + data: T, + mutex: std.Thread.Mutex = .{}, + }; +} diff --git a/src/core/tab/tab.zig b/src/core/tab/tab.zig index 8b09ec2..8928c03 100644 --- a/src/core/tab/tab.zig +++ b/src/core/tab/tab.zig @@ -6,10 +6,12 @@ const models = @import("../models.zig"); const Container = models.Container; const Item = models.Item; +const locked = @import("../sync.zig").locked; + pub const Tab = struct { allocator: std.mem.Allocator, currentLocation: ?*Container, - currentItems: ?std.ArrayList(*Item), + currentItems: locked(?std.ArrayList(*Item)), currentItemsChanged: bool = false, threadPool: *std.Thread.Pool, _private: Private, @@ -25,7 +27,7 @@ pub const Tab = struct { ) void { self.* = Tab{ .allocator = allocator, - .currentItems = null, + .currentItems = .{ .data = null }, .currentLocation = null, .threadPool = threadPool, ._private = .{ @@ -52,15 +54,20 @@ pub const Tab = struct { if (self._private.currentItemsAllocator) |arena| { arena.deinit(); } - if (self.currentItems) |items| { - items.deinit(); + + { + self.currentItems.mutex.lock(); + defer self.currentItems.mutex.unlock(); + if (self.currentItems.data) |items| { + items.deinit(); + } } self._private.currentItemsAllocator = std.heap.ArenaAllocator.init(self.allocator); const arenaAllocator = &self._private.currentItemsAllocator.?; const arena = arenaAllocator.allocator(); - var threadSafeAllocator = std.heap.ThreadSafeAllocator{.child_allocator = arena}; + var threadSafeAllocator = std.heap.ThreadSafeAllocator{ .child_allocator = arena }; const allocator = threadSafeAllocator.allocator(); errdefer { @@ -68,10 +75,15 @@ pub const Tab = struct { self._private.currentItemsAllocator = null; } - self.currentItems = std.ArrayList(*Item).init(allocator); - errdefer { - self.currentItems.?.deinit(); - self.currentItems = null; + { + self.currentItems.mutex.lock(); + defer self.currentItems.mutex.unlock(); + + self.currentItems.data = std.ArrayList(*Item).init(allocator); + errdefer { + self.currentItems.data.?.deinit(); + self.currentItems.data = null; + } } while (location.childrenLoading) { @@ -80,7 +92,11 @@ pub const Tab = struct { for (location.children.items) |item| { const resolvedItem = try location.item.provider.getItemByFullName(item, &.{ .skipChildInit = false }, allocator); - try self.currentItems.?.append(resolvedItem); + + self.currentItems.mutex.lock(); + defer self.currentItems.mutex.unlock(); + + try self.currentItems.data.?.append(resolvedItem); self.currentItemsChanged = true; } }