feat(console): ui, refactors
This commit is contained in:
@@ -10,16 +10,16 @@ const locked = @import("../core/sync.zig").locked;
|
|||||||
|
|
||||||
/// Our main application state
|
/// Our main application state
|
||||||
const Model = struct {
|
const Model = struct {
|
||||||
|
crash: bool = false,
|
||||||
usage_number: locked(u16) = .{ .data = 0 },
|
usage_number: locked(u16) = .{ .data = 0 },
|
||||||
running: bool = true,
|
running: bool = true,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
current_items: *vxfw.ListView,
|
current_items: locked(?[]*models.Item) = .{ .data = null },
|
||||||
current_items_allocator: ?std.heap.ArenaAllocator = null,
|
current_items_view: *vxfw.ListView,
|
||||||
|
current_items_allocator: std.heap.ArenaAllocator,
|
||||||
tab: *Tab,
|
tab: *Tab,
|
||||||
/// State of the counter
|
/// State of the counter
|
||||||
count: usize = 0,
|
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
|
/// Helper function to return a vxfw.Widget struct
|
||||||
pub fn widget(self: *Model) vxfw.Widget {
|
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 {
|
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
|
||||||
const vm: *Model = @ptrCast(@alignCast(ptr));
|
const vm: *Model = @ptrCast(@alignCast(ptr));
|
||||||
switch (event) {
|
switch (event) {
|
||||||
// The root widget is always sent an init event as the first event. Users of the
|
.init => return ctx.requestFocus(vm.current_items_view.widget()),
|
||||||
// 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()),
|
|
||||||
.key_press => |key| {
|
.key_press => |key| {
|
||||||
if (key.matches('c', .{ .ctrl = true })) {
|
if (key.matches('c', .{ .ctrl = true })) {
|
||||||
ctx.quit = true;
|
ctx.quit = true;
|
||||||
return;
|
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
|
.focus_in => return ctx.requestFocus(vm.current_items_view.widget()),
|
||||||
// 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()),
|
|
||||||
else => {},
|
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 {
|
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
|
||||||
const vm: *Model = @ptrCast(@alignCast(ptr));
|
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();
|
const max_size = ctx.max.size();
|
||||||
|
|
||||||
// The DrawContext also contains an arena allocator that can be used for each frame. The
|
try createCurrentItems(ctx, vm);
|
||||||
// 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 };
|
|
||||||
|
|
||||||
// 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
|
if (vm.current_items_view.children.slice.len == 0) {
|
||||||
// it? does it handle the mouse?
|
const empty_text_element = try ctx.arena.create(vxfw.Text);
|
||||||
//
|
empty_text_element.* = vxfw.Text{
|
||||||
// It DOES NOT contain the location it should be within it's parent. Only the parent can set
|
.text = "Empty",
|
||||||
// this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
|
.overflow = .clip,
|
||||||
// has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
|
.softwrap = false,
|
||||||
// with an offset and a z-index - the offset can be negative. This lets a parent draw a
|
// .style = .{
|
||||||
// child and place it within itself
|
// .bg = bg,
|
||||||
const text_child: vxfw.SubSurface = .{
|
// .fg = fg,
|
||||||
.origin = .{ .row = 0, .col = 0 },
|
// },
|
||||||
.surface = try text.draw(ctx),
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const button_child: vxfw.SubSurface = .{
|
const items = try ctx.arena.alloc(vxfw.Widget, 1);
|
||||||
.origin = .{ .row = 2, .col = 0 },
|
items[0] = empty_text_element.widget();
|
||||||
.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 },
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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 = .{
|
const list_surface: vxfw.SubSurface = .{
|
||||||
.origin = .{ .row = 12, .col = 30 },
|
.origin = .{ .row = currentItemsTop, .col = parentItemsWidth },
|
||||||
.surface = try vm.current_items.widget().draw(ctx.withConstraints(ctx.min, .{ .width = 30, .height = 10 })),
|
.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
|
try rootWidgets.append(current_location_text_surface);
|
||||||
// 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;
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
// A Surface must have a size. Our root widget is the size of the screen
|
|
||||||
.size = max_size,
|
.size = max_size,
|
||||||
.widget = vm.widget(),
|
.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 = &.{},
|
.buffer = &.{},
|
||||||
.children = children,
|
.children = rootWidgets.items,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The onClick callback for our button. This is also called if we press enter while the button
|
fn createCurrentItems(ctx: vxfw.DrawContext, vm: *Model) !void {
|
||||||
/// has focus
|
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 {
|
fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
|
||||||
const ptr = maybe_ptr orelse return;
|
const ptr = maybe_ptr orelse return;
|
||||||
const self: *Model = @ptrCast(@alignCast(ptr));
|
const self: *Model = @ptrCast(@alignCast(ptr));
|
||||||
@@ -134,52 +188,44 @@ const Model = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fn updateChildren(vm: *Model) !void {
|
fn data_loop(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;
|
vm.usage_number.data += 1;
|
||||||
while (vm.running) {
|
while (vm.running) {
|
||||||
inner_loop(vm) catch {};
|
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;
|
vm.usage_number.data -= 1;
|
||||||
}
|
}
|
||||||
fn inner_loop(vm: *Model) !void {
|
fn inner_loop(vm: *Model) !void {
|
||||||
if (vm.tab.currentItemsChanged) {
|
if (vm.tab.currentItemsChanged) {
|
||||||
std.Thread.sleep(2000 * std.time.ns_per_ms);
|
std.Thread.sleep(10 * std.time.ns_per_ms);
|
||||||
try updateChildren(vm);
|
|
||||||
vm.tab.currentItemsChanged = false;
|
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();
|
vm.tab.currentItems.mutex.lock();
|
||||||
@@ -188,16 +234,39 @@ fn inner_loop(vm: *Model) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.DebugAllocator(.{}){};
|
const DebugAllocator = std.heap.DebugAllocator(.{});
|
||||||
|
var gpa = DebugAllocator{};
|
||||||
const gp_allocator = gpa.allocator();
|
const gp_allocator = gpa.allocator();
|
||||||
defer {
|
defer {
|
||||||
_ = gpa.detectLeaks();
|
_ = gpa.detectLeaks();
|
||||||
_ = gpa.deinit();
|
_ = 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 };
|
var tsa = std.heap.ThreadSafeAllocator{ .child_allocator = gp_allocator };
|
||||||
const allocator = tsa.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;
|
var pool: std.Thread.Pool = undefined;
|
||||||
try pool.init(.{
|
try pool.init(.{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
@@ -227,25 +296,38 @@ pub fn main() !void {
|
|||||||
|
|
||||||
std.Thread.sleep(1 * std.time.ns_per_s);
|
std.Thread.sleep(1 * std.time.ns_per_s);
|
||||||
|
|
||||||
const list = try allocator.create(vxfw.ListView);
|
const empty_text_element = try allocator.create(vxfw.Text);
|
||||||
list.* = .{
|
defer allocator.destroy(empty_text_element);
|
||||||
.children = .{ .slice = &.{} },
|
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.* = .{
|
model.* = .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.current_items = list,
|
.current_items_allocator = std.heap.ArenaAllocator.init(allocator),
|
||||||
|
.current_items_view = &list,
|
||||||
.tab = tab1,
|
.tab = tab1,
|
||||||
.count = 0,
|
.count = 0,
|
||||||
.button = .{
|
|
||||||
.label = "Click me!",
|
|
||||||
.onClick = Model.onClick,
|
|
||||||
.userdata = model,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
defer model.current_items_allocator.deinit();
|
||||||
|
|
||||||
model.usage_number.data += 1;
|
model.usage_number.data += 1;
|
||||||
|
|
||||||
try pool.spawn(loop1, .{model});
|
try pool.spawn(data_loop, .{model});
|
||||||
|
|
||||||
var app = try vxfw.App.init(allocator);
|
var app = try vxfw.App.init(allocator);
|
||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
@@ -257,10 +339,8 @@ pub fn main() !void {
|
|||||||
model.running = false;
|
model.running = false;
|
||||||
|
|
||||||
while (model.usage_number.data > 0) {
|
while (model.usage_number.data > 0) {
|
||||||
std.Thread.sleep(100 * std.time.ns_per_ms);
|
std.Thread.sleep(10 * std.time.ns_per_ms);
|
||||||
}
|
|
||||||
|
|
||||||
if (model.current_items_allocator) |a| {
|
|
||||||
a.deinit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const asd1 = @import("../core/allocator.zig");
|
||||||
|
|||||||
Reference in New Issue
Block a user