From 537e42ef5b6df7083f7221e47cd2a3c3d98c13cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Tue, 6 May 2025 16:23:07 +0200 Subject: [PATCH] feat(console): ui, refactors --- src/console/main.zig | 322 +++++++++++++++++++++++++++---------------- 1 file changed, 201 insertions(+), 121 deletions(-) diff --git a/src/console/main.zig b/src/console/main.zig index 1da8b74..3e3c62a 100644 --- a/src/console/main.zig +++ b/src/console/main.zig @@ -10,16 +10,16 @@ const locked = @import("../core/sync.zig").locked; /// 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: *vxfw.ListView, - current_items_allocator: ?std.heap.ArenaAllocator = null, + 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, - /// The button. This widget is stateful and must live between frames - button: vxfw.Button, /// Helper function to return a vxfw.Widget struct pub fn widget(self: *Model) vxfw.Widget { @@ -34,98 +34,152 @@ const Model = struct { fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 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(vm.current_items.widget()), + .init => return ctx.requestFocus(vm.current_items_view.widget()), .key_press => |key| { if (key.matches('c', .{ .ctrl = true })) { ctx.quit = true; return; } + if (key.matches('r', .{ .ctrl = true })) { + vm.crash = true; + ctx.redraw = true; + return ctx.requestFocus(vm.current_items_view.widget()); + } }, - // We can request a specific widget gets focus. In this case, we always want to focus - // 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(vm.current_items.widget()), + .focus_in => return ctx.requestFocus(vm.current_items_view.widget()), else => {}, } } - /// This function is called from the vxfw runtime. It will be called on a regular interval, and - /// only when any event handler has marked the redraw flag in EventContext as true. By - /// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events - /// 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)); + const rootWidgets = try ctx.arena.create(std.ArrayList(vxfw.SubSurface)); + rootWidgets.* = std.ArrayList(vxfw.SubSurface).init(ctx.arena); - // 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 - // that direction and the widget should take up as much space as it needs. By calling size() - // on the max, we assert that it has some constrained size. This is *always* the case for - // the root widget - the maximum size will always be the size of the terminal screen. const max_size = ctx.max.size(); - // The DrawContext also contains an arena allocator that can be used for each frame. The - // lifetime of this allocation is until the next time we draw a frame. This is useful for - // temporary allocations such as the one below: we have an integer we want to print as text. - // We can safely allocate this with the ctx arena since we only need it for this frame. - const count_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{vm.count}); - const text: vxfw.Text = .{ .text = count_text }; + try createCurrentItems(ctx, vm); - // Each widget returns a Surface from it's draw function. A Surface contains the rectangular - // area of the widget, as well as some information about the surface or widget: can we focus - // it? does it handle the mouse? - // - // It DOES NOT contain the location it should be within it's parent. Only the parent can set - // this via a SubSurface. Here, we will return a Surface for the root widget (Model), which - // has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface - // with an offset and a z-index - the offset can be negative. This lets a parent draw a - // child and place it within itself - const text_child: vxfw.SubSurface = .{ - .origin = .{ .row = 0, .col = 0 }, - .surface = try text.draw(ctx), - }; + { + if (vm.current_items_view.children.slice.len == 0) { + const empty_text_element = try ctx.arena.create(vxfw.Text); + empty_text_element.* = vxfw.Text{ + .text = "Empty", + .overflow = .clip, + .softwrap = false, + // .style = .{ + // .bg = bg, + // .fg = fg, + // }, + }; - const button_child: vxfw.SubSurface = .{ - .origin = .{ .row = 2, .col = 0 }, - .surface = try vm.button.draw(ctx.withConstraints( - ctx.min, - // Here we explicitly set a new maximum size constraint for the Button. A Button will - // expand to fill it's area and must have some hard limit in the maximum constraint - .{ .width = 16, .height = 3 }, - )), - }; + const items = try ctx.arena.alloc(vxfw.Widget, 1); + items[0] = empty_text_element.widget(); + vm.current_items_view.children.slice = items; + } + } + + const parentItemsWidth = max_size.width / 9; + const childrenWidth = max_size.width * 4 / 9; + const currentItemsWidth = max_size.width - parentItemsWidth - childrenWidth; + + const currentItemsTop = 1; const list_surface: vxfw.SubSurface = .{ - .origin = .{ .row = 12, .col = 30 }, - .surface = try vm.current_items.widget().draw(ctx.withConstraints(ctx.min, .{ .width = 30, .height = 10 })), + .origin = .{ .row = currentItemsTop, .col = parentItemsWidth }, + .surface = try vm.current_items_view + .widget() + .draw(ctx.withConstraints(ctx.min, .{ + .width = currentItemsWidth, + .height = ctx.max.height.? - currentItemsTop, + })), + // .surface = try vm.current_items.widget().draw(ctx), + }; + try rootWidgets.append(list_surface); + + const current_location_text = if (vm.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, + .overflow = .clip, + .softwrap = false, + .style = .{ + .fg = .{ .index = 4 }, + }, + }; + const current_location_text_surface: vxfw.SubSurface = .{ + .origin = .{ .row = 0, .col = 0 }, + .surface = try current_location_text_element.widget().draw(ctx), + // .surface = try vm.current_items.widget().draw(ctx), }; - // We also can use our arena to allocate the slice for our SubSurfaces. This slice only - // needs to live until the next frame, making this safe. - const children = try ctx.arena.alloc(vxfw.SubSurface, 3); - children[0] = text_child; - children[1] = button_child; - // children[2] = splitView_surface; - children[2] = list_surface; + try rootWidgets.append(current_location_text_surface); return .{ - // A Surface must have a size. Our root widget is the size of the screen .size = max_size, .widget = vm.widget(), - // We didn't actually need to draw anything for the root. In this case, we can set - // buffer to a zero length slice. If this slice is *not zero length*, the runtime will - // assert that it's length is equal to the size.width * size.height. .buffer = &.{}, - .children = children, + .children = rootWidgets.items, }; } - /// The onClick callback for our button. This is also called if we press enter while the button - /// has focus + 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: { + 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; + + const fg, const bg = colors: { + var fg: vaxis.Color = .default; + var bg: vaxis.Color = .default; + if (is_active) { + fg = switch (child.item) { + .container => .{ .index = 0 }, + .element => .{ .index = 0 }, + }; + bg = switch (child.item) { + .container => .{ .index = 4 }, + .element => .{ .index = 7 }, + }; + } else { + fg = switch (child.item) { + .container => .{ .index = 4 }, + .element => .default, + }; + bg = .default; + } + break :colors .{ fg, bg }; + }; + + const text = try ctx.arena.dupe(u8, child.fullName.path); + const text_element = try ctx.arena.create(vxfw.Text); + text_element.* = vxfw.Text{ + .text = text, + .overflow = .clip, + .softwrap = false, + .style = .{ + .bg = bg, + .fg = fg, + }, + }; + + children[i] = text_element; + } + break :blk children; + } else &.{}; + + const widgets = try ctx.arena.alloc(vxfw.Widget, text_items.len); + for (text_items, 0..) |t, i| { + widgets[i] = t.widget(); + } + + 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)); @@ -134,52 +188,44 @@ 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 { +fn data_loop(vm: *Model) void { vm.usage_number.data += 1; while (vm.running) { inner_loop(vm) catch {}; - std.Thread.sleep(2000 * std.time.ns_per_ms); + 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(2000 * std.time.ns_per_ms); - try updateChildren(vm); - vm.tab.currentItemsChanged = false; + 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(); @@ -188,16 +234,39 @@ fn inner_loop(vm: *Model) !void { } pub fn main() !void { - var gpa = std.heap.DebugAllocator(.{}){}; + const DebugAllocator = std.heap.DebugAllocator(.{}); + var gpa = DebugAllocator{}; const gp_allocator = gpa.allocator(); defer { _ = gpa.detectLeaks(); _ = gpa.deinit(); } + // const type2 = struct { + // allocator: *DebugAllocator, + // + // pub fn deinit(obj: *anyopaque) void { + // const self: *@This() = @ptrCast(@alignCast(obj)); + // _ = DebugAllocator.deinit(self.allocator); + // } + // }; + // var asd2: type2 = .{ + // .allocator = &gpa, + // }; + // const callback = asd1.Callback.create(&asd2); + // + // callback.call(); + var tsa = std.heap.ThreadSafeAllocator{ .child_allocator = gp_allocator }; const allocator = tsa.allocator(); + // const args = .{gpa}; + // const type1 = asd1.makeCallbackType(DebugAllocator.deinit, @TypeOf(args)); + // const instance1 = type1.init(args); + // // _ = instance1; + // const callback = instance1.makeCallback(); + // _ = callback; + var pool: std.Thread.Pool = undefined; try pool.init(.{ .allocator = allocator, @@ -227,25 +296,38 @@ pub fn main() !void { std.Thread.sleep(1 * std.time.ns_per_s); - const list = try allocator.create(vxfw.ListView); - list.* = .{ - .children = .{ .slice = &.{} }, + const empty_text_element = try allocator.create(vxfw.Text); + defer allocator.destroy(empty_text_element); + empty_text_element.* = vxfw.Text{ + .text = "Empty", + // .overflow = .clip, + // .softwrap = false, + // .style = .{ + // .bg = bg, + // .fg = fg, + // }, + }; + + const items = try allocator.alloc(vxfw.Widget, 1); + defer allocator.free(items); + items[0] = empty_text_element.widget(); + + var list: vxfw.ListView = .{ + .draw_cursor = false, + .children = .{ .slice = items }, }; model.* = .{ .allocator = allocator, - .current_items = list, + .current_items_allocator = std.heap.ArenaAllocator.init(allocator), + .current_items_view = &list, .tab = tab1, .count = 0, - .button = .{ - .label = "Click me!", - .onClick = Model.onClick, - .userdata = model, - }, }; + defer model.current_items_allocator.deinit(); model.usage_number.data += 1; - try pool.spawn(loop1, .{model}); + try pool.spawn(data_loop, .{model}); var app = try vxfw.App.init(allocator); defer app.deinit(); @@ -257,10 +339,8 @@ pub fn main() !void { 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(); + std.Thread.sleep(10 * std.time.ns_per_ms); } } + +const asd1 = @import("../core/allocator.zig");