Immediate mode renderer for libvaxis
1// This can contain internal events as well as Vaxis events.
2// Internal events can be posted into the same queue as vaxis events to allow
3// for a single event loop with exhaustive switching. Booya
4const Event = union(enum) {
5 key_press: vaxis.Key,
6 winsize: vaxis.Winsize,
7 mouse: vaxis.Mouse,
8 mouse_focus: vaxis.Mouse,
9};
10
11const Widget = enum(u32) {
12 FileMenu,
13 AboutButton,
14 AboutWindow,
15 QuitButton,
16 CloseAboutButton,
17 ClickMe,
18 CounterModal,
19 ScrollDemoWindow,
20 ScrollDemoContent,
21};
22
23const Vxim = vxim.Vxim(Event, Widget);
24
25const Menu = enum {
26 File,
27};
28
29const Window = enum {
30 About,
31};
32
33const State = struct {
34 mouse: ?vaxis.Mouse = null,
35 clicks: usize = 0,
36 open_menu: ?Menu = null,
37 open_window: ?Window = null,
38 about_window_pos: struct { x: u16, y: u16 } = .{ .x = 0, .y = 1 },
39 counter_window_pos: struct { x: u16, y: u16 } = .{ .x = 30, .y = 10 },
40 scroll_window_pos: struct { x: u16, y: u16 } = .{ .x = 4, .y = 2 },
41 v_scroll_offset: usize = 0,
42 h_scroll_offset: usize = 0,
43};
44
45var state: State = .{};
46
47var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
48
49pub fn main() !void {
50 const gpa, const is_debug = gpa: {
51 break :gpa switch (builtin.mode) {
52 .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
53 .ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
54 };
55 };
56 defer if (is_debug) {
57 _ = debug_allocator.deinit();
58 };
59
60 var vx: Vxim = .init(gpa);
61 defer vx.deinit(gpa);
62
63 try vx.enterAltScreen();
64
65 try vx.startLoop(gpa, update);
66}
67
68pub fn update(ctx: Vxim.UpdateContext) anyerror!Vxim.UpdateResult {
69 switch (ctx.current_event) {
70 .key_press => |key| {
71 if (key.matches('c', .{ .ctrl = true }))
72 return .stop;
73 },
74 .mouse => |mouse| state.mouse = mouse,
75
76 .winsize => {},
77 .mouse_focus => |_| {},
78 }
79
80 ctx.root_win.clear();
81
82 // Counter window.
83 {
84 const modal_width = @min(25, ctx.root_win.width);
85 const modal_height = @min(7, ctx.root_win.height);
86 const modal = ctx.vxim.window(.CounterModal, ctx.root_win, .{
87 .width = modal_width,
88 .height = modal_height,
89 .x = &state.counter_window_pos.x,
90 .y = &state.counter_window_pos.y,
91 .title = "Counter demo",
92 });
93 const button_text = "Click Me!";
94
95 const button_x: u16 =
96 (modal_width / 2) -| ((@as(u16, @truncate(button_text.len)) + 2) / 2) -| 1;
97 const button_y: u16 = (modal_height / 2);
98
99 const button_action =
100 ctx.vxim.button(
101 .ClickMe,
102 modal,
103 .{ .x = button_x, .y = button_y, .text = button_text },
104 );
105
106 if (button_action == .clicked) state.clicks +|= 1;
107
108 const text = try std.fmt.allocPrint(ctx.vxim.arena(), "Clicks: {d}", .{state.clicks});
109 const text_x: u16 = (modal_width / 2) -| (@as(u16, @truncate(text.len)) / 2) -| 1;
110 const text_y: u16 = (modal_height / 2) -| 2;
111
112 ctx.vxim.text(modal, .{ .text = text, .x = text_x, .y = text_y, .allow_selection = true });
113 }
114
115 // Scroll demo
116 {
117 const scroll_window = ctx.vxim.window(.ScrollDemoWindow, ctx.root_win, .{
118 .x = &state.scroll_window_pos.x,
119 .y = &state.scroll_window_pos.y,
120 .height = 12,
121 .width = 22,
122 .title = "Scroll demo",
123 });
124 const content_height = 50;
125 const scroll_body = ctx.vxim.scrollArea(.ScrollDemoContent, scroll_window, .{
126 .content_height = content_height,
127 .content_width = 30,
128 .v_content_offset = &state.v_scroll_offset,
129 .h_content_offset = &state.h_scroll_offset,
130 });
131
132 // It's sufficient to draw from the top of the scroll area.
133 for (state.v_scroll_offset..content_height) |i| {
134 // No need to draw outside the scroll area.
135 if (i > state.v_scroll_offset + scroll_body.height) break;
136
137 const text = try std.fmt.allocPrint(ctx.vxim.arena(), "line: {d}", .{i + 1});
138
139 if (text.len <= state.h_scroll_offset) continue;
140
141 ctx.vxim.text(scroll_body, .{
142 .y = @as(u16, @intCast(i -| state.v_scroll_offset)),
143 .text = text[state.h_scroll_offset..],
144 });
145 }
146 }
147
148 // About window
149 if (state.open_window) |open_window| {
150 if (open_window == .About) {
151 const about_win = ctx.vxim.window(.AboutWindow, ctx.root_win, .{
152 .width = @min(ctx.root_win.width, 35),
153 .height = @min(ctx.root_win.height, 11),
154 .x = &state.about_window_pos.x,
155 .y = &state.about_window_pos.y,
156 .title = "About this program",
157 });
158
159 const about_body = ctx.vxim.padding(about_win, .{ .all = 1 });
160
161 const close = ctx.vxim.button(
162 .CloseAboutButton,
163 about_body,
164 .{ .x = about_body.width / 2 -| 3, .y = about_body.height -| 1, .text = "Close" },
165 );
166 if (close == .clicked) {
167 state.open_window = null;
168 }
169
170 ctx.vxim.text(
171 about_body,
172 // Extra space to center text in window.
173 .{ .text = " VXIM v0.0.0", .allow_selection = true },
174 );
175 ctx.vxim.text(
176 about_body,
177 .{
178 .text = "Experimental immediate mode renderer for libvaxis",
179 .y = 2,
180 .allow_selection = true,
181 },
182 );
183 }
184 }
185
186 const menuAction = ctx.vxim.menuBar(ctx.root_win, &.{
187 .{
188 .name = "File",
189 .id = .FileMenu,
190 .items = &.{
191 .{ .name = "About", .id = .AboutButton },
192 .{ .name = "Quit", .id = .QuitButton },
193 },
194 },
195 });
196
197 if (menuAction) |action| {
198 if (action.id == .AboutButton and action.action == .clicked) state.open_window = .About;
199 if (action.id == .QuitButton and action.action == .clicked) return .stop;
200 }
201
202 return .keep_going;
203}
204
205const std = @import("std");
206const builtin = @import("builtin");
207
208const vaxis = @import("vaxis");
209const vxim = @import("vxim");