···11+const std = @import("std");
22+const vaxis = @import("vaxis");
33+const vxfw = @import("vxfw.zig");
44+55+const assert = std.debug.assert;
66+77+const Allocator = std.mem.Allocator;
88+99+const EventLoop = vaxis.Loop(vxfw.Event);
1010+const Widget = vxfw.Widget;
1111+1212+const App = @This();
1313+1414+quit_key: vaxis.Key = .{ .codepoint = 'c', .mods = .{ .ctrl = true } },
1515+1616+allocator: Allocator,
1717+tty: vaxis.Tty,
1818+vx: vaxis.Vaxis,
1919+timers: std.ArrayList(vxfw.Tick),
2020+wants_focus: ?vxfw.Widget,
2121+2222+/// Runtime options
2323+pub const Options = struct {
2424+ /// Frames per second
2525+ framerate: u8 = 60,
2626+};
2727+2828+/// Create an application. We require stable pointers to do the set up, so this will create an App
2929+/// object on the heap. Call destroy when the app is complete to reset terminal state and release
3030+/// resources
3131+pub fn init(allocator: Allocator) !App {
3232+ return .{
3333+ .allocator = allocator,
3434+ .tty = try vaxis.Tty.init(),
3535+ .vx = try vaxis.init(allocator, .{ .system_clipboard_allocator = allocator }),
3636+ .timers = std.ArrayList(vxfw.Tick).init(allocator),
3737+ .wants_focus = null,
3838+ };
3939+}
4040+4141+pub fn deinit(self: *App) void {
4242+ self.timers.deinit();
4343+ self.vx.deinit(self.allocator, self.tty.anyWriter());
4444+ self.tty.deinit();
4545+}
4646+4747+pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void {
4848+ const tty = &self.tty;
4949+ const vx = &self.vx;
5050+5151+ var loop: EventLoop = .{ .tty = tty, .vaxis = vx };
5252+ try loop.start();
5353+ defer loop.stop();
5454+5555+ // Send the init event
5656+ loop.postEvent(.init);
5757+5858+ try vx.enterAltScreen(tty.anyWriter());
5959+ try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
6060+6161+ {
6262+ // This part deserves a comment. loop.init installs a signal handler for the tty. We wait to
6363+ // init the loop until we know if we need this handler. We don't need it if the terminal
6464+ // supports in-band-resize
6565+ if (!vx.state.in_band_resize) try loop.init();
6666+ }
6767+6868+ // HACK: Ghostty is reporting incorrect pixel screen size
6969+ vx.caps.sgr_pixels = false;
7070+ try vx.setMouseMode(tty.anyWriter(), true);
7171+7272+ // Give DrawContext the unicode data
7373+ vxfw.DrawContext.init(&vx.unicode, vx.screen.width_method);
7474+7575+ const framerate: u64 = if (opts.framerate > 0) opts.framerate else 60;
7676+ // Calculate tick rate
7777+ const tick_ms: u64 = @divFloor(std.time.ms_per_s, framerate);
7878+7979+ // Set up arena and context
8080+ var arena = std.heap.ArenaAllocator.init(self.allocator);
8181+ defer arena.deinit();
8282+8383+ var buffered = tty.bufferedWriter();
8484+8585+ var mouse_handler = MouseHandler.init(widget);
8686+ var focus_handler = FocusHandler.init(self.allocator, widget);
8787+ focus_handler.intrusiveInit();
8888+ defer focus_handler.deinit();
8989+9090+ // Timestamp of our next frame
9191+ var next_frame_ms: u64 = @intCast(std.time.milliTimestamp());
9292+9393+ // Create our event context
9494+ var ctx: vxfw.EventContext = .{
9595+ .phase = .at_target,
9696+ .cmds = vxfw.CommandList.init(self.allocator),
9797+ .consume_event = false,
9898+ .redraw = false,
9999+ .quit = false,
100100+ };
101101+ defer ctx.cmds.deinit();
102102+103103+ while (true) {
104104+ const now_ms: u64 = @intCast(std.time.milliTimestamp());
105105+ if (now_ms >= next_frame_ms) {
106106+ // Deadline exceeded. Schedule the next frame
107107+ next_frame_ms = now_ms + tick_ms;
108108+ } else {
109109+ // Sleep until the deadline
110110+ std.time.sleep((next_frame_ms - now_ms) * std.time.ns_per_ms);
111111+ next_frame_ms += tick_ms;
112112+ }
113113+114114+ try self.checkTimers(&ctx);
115115+116116+ while (loop.tryEvent()) |event| {
117117+ ctx.consume_event = false;
118118+ switch (event) {
119119+ .key_press => |key| {
120120+ try focus_handler.handleEvent(&ctx, event);
121121+ try self.handleCommand(&ctx.cmds);
122122+ if (!ctx.consume_event) {
123123+ if (key.matches(self.quit_key.codepoint, self.quit_key.mods)) {
124124+ ctx.quit = true;
125125+ }
126126+ if (key.matches(vaxis.Key.tab, .{})) {
127127+ try focus_handler.focusNext(&ctx);
128128+ try self.handleCommand(&ctx.cmds);
129129+ }
130130+ if (key.matches(vaxis.Key.tab, .{ .shift = true })) {
131131+ try focus_handler.focusPrev(&ctx);
132132+ try self.handleCommand(&ctx.cmds);
133133+ }
134134+ }
135135+ },
136136+ .focus_out => try mouse_handler.mouseExit(self, &ctx),
137137+ .mouse => |mouse| try mouse_handler.handleMouse(self, &ctx, mouse),
138138+ .winsize => |ws| {
139139+ try vx.resize(self.allocator, buffered.writer().any(), ws);
140140+ try buffered.flush();
141141+ ctx.redraw = true;
142142+ },
143143+ else => {
144144+ try widget.handleEvent(&ctx, event);
145145+ try self.handleCommand(&ctx.cmds);
146146+ },
147147+ }
148148+ }
149149+150150+ // Check if we should quit
151151+ if (ctx.quit) return;
152152+153153+ // Check if we need a redraw
154154+ if (!ctx.redraw) continue;
155155+ ctx.redraw = false;
156156+ // Assert that we have handled all commands
157157+ assert(ctx.cmds.items.len == 0);
158158+159159+ _ = arena.reset(.retain_capacity);
160160+161161+ const draw_context: vxfw.DrawContext = .{
162162+ .arena = arena.allocator(),
163163+ .min = .{ .width = 0, .height = 0 },
164164+ .max = .{
165165+ .width = @intCast(vx.screen.width),
166166+ .height = @intCast(vx.screen.height),
167167+ },
168168+ };
169169+ const win = vx.window();
170170+ win.clear();
171171+ win.hideCursor();
172172+ win.setCursorShape(.default);
173173+ const surface = try widget.draw(draw_context);
174174+175175+ const focused = self.wants_focus orelse focus_handler.focused.widget;
176176+ surface.render(win, focused);
177177+ try vx.render(buffered.writer().any());
178178+ try buffered.flush();
179179+180180+ // Store the last frame
181181+ mouse_handler.last_frame = surface;
182182+ try focus_handler.update(surface, self.wants_focus);
183183+ self.wants_focus = null;
184184+ }
185185+}
186186+187187+fn addTick(self: *App, tick: vxfw.Tick) Allocator.Error!void {
188188+ try self.timers.append(tick);
189189+ std.sort.insertion(vxfw.Tick, self.timers.items, {}, vxfw.Tick.lessThan);
190190+}
191191+192192+fn handleCommand(self: *App, cmds: *vxfw.CommandList) Allocator.Error!void {
193193+ defer cmds.clearRetainingCapacity();
194194+ for (cmds.items) |cmd| {
195195+ switch (cmd) {
196196+ .tick => |tick| try self.addTick(tick),
197197+ .set_mouse_shape => |shape| self.vx.setMouseShape(shape),
198198+ .request_focus => |widget| self.wants_focus = widget,
199199+ }
200200+ }
201201+}
202202+203203+fn checkTimers(self: *App, ctx: *vxfw.EventContext) anyerror!void {
204204+ const now_ms = std.time.milliTimestamp();
205205+206206+ // timers are always sorted descending
207207+ while (self.timers.popOrNull()) |tick| {
208208+ if (now_ms < tick.deadline_ms)
209209+ break;
210210+ try tick.widget.handleEvent(ctx, .tick);
211211+ try self.handleCommand(&ctx.cmds);
212212+ }
213213+}
214214+215215+const MouseHandler = struct {
216216+ last_frame: vxfw.Surface,
217217+ maybe_last_handler: ?vxfw.Widget = null,
218218+219219+ fn init(root: Widget) MouseHandler {
220220+ return .{
221221+ .last_frame = .{
222222+ .size = .{ .width = 0, .height = 0 },
223223+ .widget = root,
224224+ .buffer = &.{},
225225+ .children = &.{},
226226+ },
227227+ .maybe_last_handler = null,
228228+ };
229229+ }
230230+231231+ fn handleMouse(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext, mouse: vaxis.Mouse) anyerror!void {
232232+ const last_frame = self.last_frame;
233233+234234+ // For mouse events we store the last frame and use that for hit testing
235235+ var hits = std.ArrayList(vxfw.HitResult).init(app.allocator);
236236+ defer hits.deinit();
237237+ const sub: vxfw.SubSurface = .{
238238+ .origin = .{ .row = 0, .col = 0 },
239239+ .surface = last_frame,
240240+ .z_index = 0,
241241+ };
242242+ const mouse_point: vxfw.Point = .{
243243+ .row = @intCast(mouse.row),
244244+ .col = @intCast(mouse.col),
245245+ };
246246+ if (sub.containsPoint(mouse_point)) {
247247+ try last_frame.hitTest(&hits, mouse_point);
248248+ }
249249+ while (hits.popOrNull()) |item| {
250250+ var m_local = mouse;
251251+ m_local.col = item.local.col;
252252+ m_local.row = item.local.row;
253253+ try item.widget.handleEvent(ctx, .{ .mouse = m_local });
254254+ try app.handleCommand(&ctx.cmds);
255255+256256+ // If the event wasn't consumed, we keep passing it on
257257+ if (!ctx.consume_event) continue;
258258+259259+ if (self.maybe_last_handler) |last_mouse_handler| {
260260+ if (!last_mouse_handler.eql(item.widget)) {
261261+ try last_mouse_handler.handleEvent(ctx, .mouse_leave);
262262+ try app.handleCommand(&ctx.cmds);
263263+ }
264264+ }
265265+ self.maybe_last_handler = item.widget;
266266+ return;
267267+ }
268268+269269+ // If no one handled the mouse, we assume it exited
270270+ return self.mouseExit(app, ctx);
271271+ }
272272+273273+ fn mouseExit(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext) anyerror!void {
274274+ if (self.maybe_last_handler) |last_handler| {
275275+ try last_handler.handleEvent(ctx, .mouse_leave);
276276+ try app.handleCommand(&ctx.cmds);
277277+ self.maybe_last_handler = null;
278278+ }
279279+ }
280280+};
281281+282282+/// Maintains a tree of focusable nodes. Delivers events to the currently focused node, walking up
283283+/// the tree until the event is handled
284284+const FocusHandler = struct {
285285+ arena: std.heap.ArenaAllocator,
286286+287287+ root: Node,
288288+ focused: *Node,
289289+ maybe_wants_focus: ?vxfw.Widget = null,
290290+291291+ const Node = struct {
292292+ widget: Widget,
293293+ parent: ?*Node,
294294+ children: []*Node,
295295+296296+ fn nextSibling(self: Node) ?*Node {
297297+ const parent = self.parent orelse return null;
298298+ const idx = for (0..parent.children.len) |i| {
299299+ const node = parent.children[i];
300300+ if (self.widget.eql(node.widget))
301301+ break i;
302302+ } else unreachable;
303303+304304+ // Return null if last child
305305+ if (idx == parent.children.len - 1)
306306+ return null
307307+ else
308308+ return parent.children[idx + 1];
309309+ }
310310+311311+ fn prevSibling(self: Node) ?*Node {
312312+ const parent = self.parent orelse return null;
313313+ const idx = for (0..parent.children.len) |i| {
314314+ const node = parent.children[i];
315315+ if (self.widget.eql(node.widget))
316316+ break i;
317317+ } else unreachable;
318318+319319+ // Return null if first child
320320+ if (idx == 0)
321321+ return null
322322+ else
323323+ return parent.children[idx - 1];
324324+ }
325325+326326+ fn lastChild(self: Node) ?*Node {
327327+ if (self.children.len > 0)
328328+ return self.children[self.children.len - 1]
329329+ else
330330+ return null;
331331+ }
332332+333333+ fn firstChild(self: Node) ?*Node {
334334+ if (self.children.len > 0)
335335+ return self.children[0]
336336+ else
337337+ return null;
338338+ }
339339+340340+ /// returns the next logical node in the tree
341341+ fn nextNode(self: *Node) *Node {
342342+ // If we have a sibling, we return it's first descendant line
343343+ if (self.nextSibling()) |sibling| {
344344+ var node = sibling;
345345+ while (node.firstChild()) |child| {
346346+ node = child;
347347+ }
348348+ return node;
349349+ }
350350+351351+ // If we don't have a sibling, we return our parent
352352+ if (self.parent) |parent| return parent;
353353+354354+ // If we don't have a parent, we are the root and we return or first descendant
355355+ var node = self;
356356+ while (node.firstChild()) |child| {
357357+ node = child;
358358+ }
359359+ return node;
360360+ }
361361+362362+ fn prevNode(self: *Node) *Node {
363363+ // If we have children, we return the last child descendant
364364+ if (self.children.len > 0) {
365365+ var node = self;
366366+ while (node.lastChild()) |child| {
367367+ node = child;
368368+ }
369369+ return node;
370370+ }
371371+372372+ // If we have siblings, we return the last descendant line of the sibling
373373+ if (self.prevSibling()) |sibling| {
374374+ var node = sibling;
375375+ while (node.lastChild()) |child| {
376376+ node = child;
377377+ }
378378+ return node;
379379+ }
380380+381381+ // If we don't have a sibling, we return our parent
382382+ if (self.parent) |parent| return parent;
383383+384384+ // If we don't have a parent, we are the root and we return our last descendant
385385+ var node = self;
386386+ while (node.lastChild()) |child| {
387387+ node = child;
388388+ }
389389+ return node;
390390+ }
391391+ };
392392+393393+ fn init(allocator: Allocator, root: Widget) FocusHandler {
394394+ const node: Node = .{
395395+ .widget = root,
396396+ .parent = null,
397397+ .children = &.{},
398398+ };
399399+ return .{
400400+ .root = node,
401401+ .focused = undefined,
402402+ .arena = std.heap.ArenaAllocator.init(allocator),
403403+ .maybe_wants_focus = null,
404404+ };
405405+ }
406406+407407+ fn intrusiveInit(self: *FocusHandler) void {
408408+ self.focused = &self.root;
409409+ }
410410+411411+ fn deinit(self: *FocusHandler) void {
412412+ self.arena.deinit();
413413+ }
414414+415415+ /// Update the focus list
416416+ fn update(self: *FocusHandler, root: vxfw.Surface, maybe_wants_focus: ?vxfw.Widget) Allocator.Error!void {
417417+ _ = self.arena.reset(.retain_capacity);
418418+ self.maybe_wants_focus = maybe_wants_focus;
419419+420420+ var list = std.ArrayList(*Node).init(self.arena.allocator());
421421+ for (root.children) |child| {
422422+ try self.findFocusableChildren(&self.root, &list, child.surface);
423423+ }
424424+ self.root = .{
425425+ .widget = root.widget,
426426+ .children = list.items,
427427+ .parent = null,
428428+ };
429429+ }
430430+431431+ /// Walks the surface tree, adding all focusable nodes to list
432432+ fn findFocusableChildren(
433433+ self: *FocusHandler,
434434+ parent: *Node,
435435+ list: *std.ArrayList(*Node),
436436+ surface: vxfw.Surface,
437437+ ) Allocator.Error!void {
438438+ if (surface.focusable) {
439439+ // We are a focusable child of parent. Create a new node, and find our own focusable
440440+ // children
441441+ const node = try self.arena.allocator().create(Node);
442442+ var child_list = std.ArrayList(*Node).init(self.arena.allocator());
443443+ for (surface.children) |child| {
444444+ try self.findFocusableChildren(node, &child_list, child.surface);
445445+ }
446446+ node.* = .{
447447+ .widget = surface.widget,
448448+ .parent = parent,
449449+ .children = child_list.items,
450450+ };
451451+ if (self.maybe_wants_focus) |wants_focus| {
452452+ if (wants_focus.eql(surface.widget)) {
453453+ self.focused = node;
454454+ self.maybe_wants_focus = null;
455455+ }
456456+ }
457457+ try list.append(node);
458458+ } else {
459459+ for (surface.children) |child| {
460460+ try self.findFocusableChildren(parent, list, child.surface);
461461+ }
462462+ }
463463+ }
464464+465465+ fn focusNode(self: *FocusHandler, ctx: *vxfw.EventContext, node: *Node) anyerror!void {
466466+ if (self.focused.widget.eql(node.widget)) return;
467467+468468+ try self.focused.widget.handleEvent(ctx, .focus_out);
469469+ self.focused = node;
470470+ try self.focused.widget.handleEvent(ctx, .focus_in);
471471+ }
472472+473473+ /// Focuses the next focusable widget
474474+ fn focusNext(self: *FocusHandler, ctx: *vxfw.EventContext) anyerror!void {
475475+ return self.focusNode(ctx, self.focused.nextNode());
476476+ }
477477+478478+ /// Focuses the previous focusable widget
479479+ fn focusPrev(self: *FocusHandler, ctx: *vxfw.EventContext) anyerror!void {
480480+ return self.focusNode(ctx, self.focused.prevNode());
481481+ }
482482+483483+ fn handleEvent(self: *FocusHandler, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
484484+ var maybe_node: ?*Node = self.focused;
485485+ while (maybe_node) |node| {
486486+ try node.widget.handleEvent(ctx, event);
487487+ if (ctx.consume_event) return;
488488+ maybe_node = node.parent;
489489+ }
490490+ }
491491+};
+7-1
src/vxfw/vxfw.zig
···8899const Allocator = std.mem.Allocator;
10101111+pub const App = @import("App.zig");
1212+1113pub const CommandList = std.ArrayList(Command);
12141315pub const UserEvent = struct {
···421423 try testing.expect(!surf.containsPoint(.{ .row = 12, .col = 2 }));
422424}
423425426426+test "refAllDecls" {
427427+ std.testing.refAllDecls(@This());
428428+}
429429+424430test "All widgets have a doctest and refAllDecls test" {
425431 // This test goes through every file in src/ and checks that it has a doctest (the filename
426432 // stripped of ".zig" matches a test name) and a test called "refAllDecls". It makes no
427433 // guarantees about the quality of the test, but it does ensure it exists which at least makes
428434 // it easy to fail CI early, or spot bad tests vs non-existant tests
429429- const excludes = &[_][]const u8{"vxfw.zig"};
435435+ const excludes = &[_][]const u8{ "vxfw.zig", "App.zig" };
430436431437 var cwd = try std.fs.cwd().openDir("./src/vxfw", .{ .iterate = true });
432438 var iter = cwd.iterate();