feat(console+core): tui, core
This commit is contained in:
209
src/console/main.zig
Normal file
209
src/console/main.zig
Normal file
@@ -0,0 +1,209 @@
|
||||
const std = @import("std");
|
||||
const vaxis = @import("vaxis");
|
||||
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;
|
||||
|
||||
/// Our main application state
|
||||
const Model = struct {
|
||||
current_items: vxfw.ListView,
|
||||
tab: *Tab,
|
||||
/// State of the counter
|
||||
count: u32 = 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 {
|
||||
return .{
|
||||
.userdata = self,
|
||||
.eventHandler = Model.typeErasedEventHandler,
|
||||
.drawFn = Model.typeErasedDrawFn,
|
||||
};
|
||||
}
|
||||
|
||||
/// 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));
|
||||
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()),
|
||||
.key_press => |key| {
|
||||
if (key.matches('c', .{ .ctrl = true })) {
|
||||
ctx.quit = true;
|
||||
return;
|
||||
}
|
||||
},
|
||||
// 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(self.current_items.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));
|
||||
// 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 };
|
||||
|
||||
// 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),
|
||||
};
|
||||
|
||||
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 list_surface: vxfw.SubSurface = .{
|
||||
.origin = .{ .row = 12, .col = 30 },
|
||||
.surface = try vm.current_items.widget().draw(ctx.withConstraints(ctx.min, .{ .width = 30, .height = 10 })),
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// The onClick callback for our button. This is also called if we press enter while the button
|
||||
/// has focus
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.DebugAllocator(.{}){};
|
||||
const gp_allocator = gpa.allocator();
|
||||
defer {
|
||||
_ = gpa.detectLeaks();
|
||||
_ = gpa.deinit();
|
||||
}
|
||||
|
||||
var tsa = std.heap.ThreadSafeAllocator{ .child_allocator = gp_allocator };
|
||||
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 };
|
||||
|
||||
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();
|
||||
const c = switch (homeItem.item) {
|
||||
.container => |c| c,
|
||||
.element => unreachable,
|
||||
};
|
||||
|
||||
var tab1 = try allocator.create(Tab);
|
||||
defer allocator.destroy(tab1);
|
||||
|
||||
tab1.init(&pool, allocator);
|
||||
defer tab1.deinit();
|
||||
|
||||
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 },
|
||||
};
|
||||
// Set the initial state of our button
|
||||
model.* = .{
|
||||
.current_items = list,
|
||||
.tab = tab1,
|
||||
.count = 0,
|
||||
.button = .{
|
||||
.label = "Click me!",
|
||||
.onClick = Model.onClick,
|
||||
.userdata = model,
|
||||
},
|
||||
};
|
||||
|
||||
try app.run(model.widget(), .{});
|
||||
std.Thread.sleep(1 * std.time.ns_per_s);
|
||||
}
|
||||
Reference in New Issue
Block a user