feat(console): ui, refactors
This commit is contained in:
@@ -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,53 +188,45 @@ 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);
|
||||
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();
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user