feat: add keyboard support to menus

This commit is contained in:
CJ van den Berg 2024-03-26 22:01:31 +01:00
parent ad2d82ce43
commit 2c4452dd81
5 changed files with 107 additions and 23 deletions

View file

@ -12,16 +12,16 @@ pub fn Options(context: type) type {
ctx: Context, ctx: Context,
on_click: *const fn (ctx: context, button: *Button.State(*State(Context))) void = do_nothing, on_click: *const fn (ctx: context, button: *Button.State(*State(Context))) void = do_nothing,
on_render: *const fn (ctx: context, button: *Button.State(*State(Context)), theme: *const Widget.Theme) bool = on_render_default, on_render: *const fn (ctx: context, button: *Button.State(*State(Context)), theme: *const Widget.Theme, selected: bool) bool = on_render_default,
on_layout: *const fn (ctx: context, button: *Button.State(*State(Context))) Widget.Layout = on_layout_default, on_layout: *const fn (ctx: context, button: *Button.State(*State(Context))) Widget.Layout = on_layout_default,
on_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) void = on_resize_default, on_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) void = on_resize_default,
pub const Context = context; pub const Context = context;
pub fn do_nothing(_: context, _: *Button.State(*State(Context))) void {} pub fn do_nothing(_: context, _: *Button.State(*State(Context))) void {}
pub fn on_render_default(_: context, button: *Button.State(*State(Context)), theme: *const Widget.Theme) bool { pub fn on_render_default(_: context, button: *Button.State(*State(Context)), theme: *const Widget.Theme, selected: bool) bool {
const style_base = if (button.active) theme.editor_cursor else if (button.hover) theme.editor_selection else theme.editor; const style_base = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor;
const bg_alpha: c_uint = if (button.active or button.hover) nc.ALPHA_OPAQUE else nc.ALPHA_TRANSPARENT; const bg_alpha: c_uint = if (button.active or button.hover or selected) nc.ALPHA_OPAQUE else nc.ALPHA_TRANSPARENT;
try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_TRANSPARENT, bg_alpha); try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_TRANSPARENT, bg_alpha);
button.plane.erase(); button.plane.erase();
button.plane.home(); button.plane.home();
@ -49,7 +49,8 @@ pub fn create(ctx_type: type, a: std.mem.Allocator, parent: Widget, opts: Option
.menu_widget = self.menu.widget(), .menu_widget = self.menu.widget(),
.opts = opts, .opts = opts,
}; };
self.menu.on_resize_ctx = self; self.menu.ctx = self;
self.menu.on_render = State(ctx_type).on_render_menu_widgetlist;
self.menu.on_resize = State(ctx_type).on_resize_menu_widgetlist; self.menu.on_resize = State(ctx_type).on_resize_menu_widgetlist;
return self; return self;
} }
@ -59,9 +60,14 @@ pub fn State(ctx_type: type) type {
a: std.mem.Allocator, a: std.mem.Allocator,
menu: *WidgetList, menu: *WidgetList,
menu_widget: Widget, menu_widget: Widget,
opts: Options(ctx_type), opts: options_type,
selected: ?usize = null,
render_idx: usize = 0,
selected_active: bool = false,
const Self = @This(); const Self = @This();
const options_type = Options(ctx_type);
const button_type = Button.State(*Self);
pub fn deinit(self: *Self, a: std.mem.Allocator) void { pub fn deinit(self: *Self, a: std.mem.Allocator) void {
self.menu.deinit(a); self.menu.deinit(a);
@ -92,12 +98,19 @@ pub fn State(ctx_type: type) type {
return self.menu.render(theme); return self.menu.render(theme);
} }
fn on_render_menu_widgetlist(ctx: ?*anyopaque, _: *const Widget.Theme) void {
const self: *Self = @ptrCast(@alignCast(ctx));
self.render_idx = 0;
}
pub fn on_layout(self: *Self, button: *Button.State(*Self)) Widget.Layout { pub fn on_layout(self: *Self, button: *Button.State(*Self)) Widget.Layout {
return self.opts.on_layout(self.opts.ctx, button); return self.opts.on_layout(self.opts.ctx, button);
} }
pub fn on_render(self: *Self, button: *Button.State(*Self), theme: *const Widget.Theme) bool { pub fn on_render(self: *Self, button: *Button.State(*Self), theme: *const Widget.Theme) bool {
return self.opts.on_render(self.opts.ctx, button, theme); defer self.render_idx += 1;
std.debug.assert(self.render_idx < self.menu.widgets.items.len);
return self.opts.on_render(self.opts.ctx, button, theme, self.render_idx == self.selected);
} }
fn on_resize_menu_widgetlist(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) void { fn on_resize_menu_widgetlist(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) void {
@ -116,5 +129,31 @@ pub fn State(ctx_type: type) type {
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool { pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
return self.menu.walk(walk_ctx, f, &self.menu_widget); return self.menu.walk(walk_ctx, f, &self.menu_widget);
} }
pub fn count(self: *Self) usize {
return self.menu.widgets.items.len;
}
pub fn select_down(self: *Self) void {
const current = self.selected orelse {
if (self.count() > 0)
self.selected = 0;
return;
};
self.selected = @min(current + 1, self.count() - 1);
}
pub fn select_up(self: *Self) void {
if (self.selected) |current| {
self.selected = if (self.count() > 0) @min(self.count() - 1, @max(current, 1) - 1) else null;
}
}
pub fn activate_selected(self: *Self) void {
const selected = self.selected orelse return;
self.selected_active = true;
const button = self.menu.widgets.items[selected].widget.dynamic_cast(button_type) orelse return;
button.opts.on_click(button.opts.ctx, button);
}
}; };
} }

View file

@ -24,8 +24,9 @@ widgets: ArrayList(WidgetState),
layout: Layout, layout: Layout,
direction: Direction, direction: Direction,
box: ?Widget.Box = null, box: ?Widget.Box = null,
ctx: ?*anyopaque = null,
on_render: *const fn (ctx: ?*anyopaque, theme: *const Widget.Theme) void = on_render_default,
on_resize: *const fn (ctx: ?*anyopaque, self: *Self, pos_: Widget.Box) void = on_resize_default, on_resize: *const fn (ctx: ?*anyopaque, self: *Self, pos_: Widget.Box) void = on_resize_default,
on_resize_ctx: ?*anyopaque = null,
pub fn createH(a: Allocator, parent: Widget, name: [:0]const u8, layout_: Layout) !*Self { pub fn createH(a: Allocator, parent: Widget, name: [:0]const u8, layout_: Layout) !*Self {
const self: *Self = try a.create(Self); const self: *Self = try a.create(Self);
@ -125,6 +126,8 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool {
break; break;
}; };
self.on_render(self.ctx, theme);
var more = false; var more = false;
for (self.widgets.items) |*w| for (self.widgets.items) |*w|
if (w.widget.render(theme)) { if (w.widget.render(theme)) {
@ -133,6 +136,8 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool {
return more; return more;
} }
fn on_render_default(_: ?*anyopaque, _: *const Widget.Theme) void {}
pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
for (self.widgets.items) |*w| for (self.widgets.items) |*w|
if (try w.widget.send(from_, m)) if (try w.widget.send(from_, m))
@ -173,7 +178,7 @@ fn refresh_layout(self: *Self) void {
} }
pub fn resize(self: *Self, pos: Widget.Box) void { pub fn resize(self: *Self, pos: Widget.Box) void {
return self.on_resize(self.on_resize_ctx, self, pos); return self.on_resize(self.ctx, self, pos);
} }
fn on_resize_default(_: ?*anyopaque, self: *Self, pos: Widget.Box) void { fn on_resize_default(_: ?*anyopaque, self: *Self, pos: Widget.Box) void {

View file

@ -95,9 +95,9 @@ fn set_style(plane: nc.Plane, style: Widget.Theme.Style) void {
}; };
} }
fn menu_on_render(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme) bool { fn menu_on_render(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool {
const style_base = if (button.active) theme.editor_cursor else if (button.hover) theme.editor_selection else theme.editor; const style_base = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor;
const bg_alpha: c_uint = if (button.active or button.hover) nc.ALPHA_OPAQUE else nc.ALPHA_TRANSPARENT; const bg_alpha: c_uint = if (button.active or button.hover or selected) nc.ALPHA_OPAQUE else nc.ALPHA_TRANSPARENT;
try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_OPAQUE, bg_alpha); try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_OPAQUE, bg_alpha);
button.plane.erase(); button.plane.erase();
button.plane.home(); button.plane.home();
@ -107,7 +107,8 @@ fn menu_on_render(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *c
const sep = std.mem.indexOfScalar(u8, button.opts.label, ':') orelse button.opts.label.len; const sep = std.mem.indexOfScalar(u8, button.opts.label, ':') orelse button.opts.label.len;
set_style(button.plane, style_subtext); set_style(button.plane, style_subtext);
set_style(button.plane, style_text); set_style(button.plane, style_text);
_ = button.plane.print(" {s}", .{button.opts.label[0..sep]}) catch {}; const pointer = if (selected) "" else " ";
_ = button.plane.print("{s}{s}", .{ pointer, button.opts.label[0..sep] }) catch {};
set_style(button.plane, style_keybind); set_style(button.plane, style_keybind);
_ = button.plane.print("{s}", .{button.opts.label[sep + 1 ..]}) catch {}; _ = button.plane.print("{s}", .{button.opts.label[sep + 1 ..]}) catch {};
return false; return false;
@ -209,6 +210,18 @@ const cmds = struct {
pub const Target = Self; pub const Target = Self;
const Ctx = command.Context; const Ctx = command.Context;
pub fn home_menu_down(self: *Self, _: Ctx) tp.result {
self.menu.select_down();
}
pub fn home_menu_up(self: *Self, _: Ctx) tp.result {
self.menu.select_up();
}
pub fn home_menu_activate(self: *Self, _: Ctx) tp.result {
self.menu.activate_selected();
}
pub fn home_sheeran(self: *Self, _: Ctx) tp.result { pub fn home_sheeran(self: *Self, _: Ctx) tp.result {
self.fire = if (self.fire) |*fire| ret: { self.fire = if (self.fire) |*fire| ret: {
fire.deinit(); fire.deinit();

View file

@ -42,6 +42,7 @@ pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result { fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result {
return switch (evtype) { return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, modifiers), nc.event_type.PRESS => self.mapPress(keypress, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, modifiers),
else => {}, else => {},
}; };
} }
@ -90,6 +91,9 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result {
nc.key.F10 => self.cmd("theme_next", .{}), nc.key.F10 => self.cmd("theme_next", .{}),
nc.key.F11 => self.cmd("toggle_logview", .{}), nc.key.F11 => self.cmd("toggle_logview", .{}),
nc.key.F12 => self.cmd("toggle_inputview", .{}), nc.key.F12 => self.cmd("toggle_inputview", .{}),
nc.key.UP => self.cmd("home_menu_up", .{}),
nc.key.DOWN => self.cmd("home_menu_down", .{}),
nc.key.ENTER => self.cmd("home_menu_activate", .{}),
else => {}, else => {},
}, },
else => {}, else => {},

View file

@ -21,19 +21,17 @@ menu: *Menu.State(*Self),
logger: log.Logger, logger: log.Logger,
count: usize = 0, count: usize = 0,
longest: usize = 0, longest: usize = 0,
commands: Commands = undefined,
pub fn create(a: std.mem.Allocator) !tui.Mode { pub fn create(a: std.mem.Allocator) !tui.Mode {
const mv = if (tui.current().mainview.dynamic_cast(mainview)) |mv_| mv_ else return error.NotFound; const mv = if (tui.current().mainview.dynamic_cast(mainview)) |mv_| mv_ else return error.NotFound;
const self: *Self = try a.create(Self); const self: *Self = try a.create(Self);
self.* = .{ self.* = .{
.a = a, .a = a,
.menu = try Menu.create(*Self, a, tui.current().mainview, .{ .menu = try Menu.create(*Self, a, tui.current().mainview, .{ .ctx = self, .on_render = on_render_menu, .on_resize = on_resize_menu }),
.ctx = self,
.on_render = on_render_menu,
.on_resize = on_resize_menu,
}),
.logger = log.logger(@typeName(Self)), .logger = log.logger(@typeName(Self)),
}; };
try self.commands.init(self);
try tui.current().message_filters.add(MessageFilter.bind(self, receive_project_manager)); try tui.current().message_filters.add(MessageFilter.bind(self, receive_project_manager));
try project_manager.request_recent_files(); try project_manager.request_recent_files();
self.menu.resize(.{ .y = 0, .x = 25, .w = 32 }); self.menu.resize(.{ .y = 0, .x = 25, .w = 32 });
@ -46,18 +44,20 @@ pub fn create(a: std.mem.Allocator) !tui.Mode {
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.commands.deinit();
tui.current().message_filters.remove_ptr(self); tui.current().message_filters.remove_ptr(self);
if (tui.current().mainview.dynamic_cast(mainview)) |mv| if (tui.current().mainview.dynamic_cast(mainview)) |mv|
mv.floating_views.remove(self.menu.menu_widget); mv.floating_views.remove(self.menu.menu_widget);
self.a.destroy(self); self.a.destroy(self);
} }
fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme) bool { fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool {
const style_base = if (button.active) theme.editor_cursor else if (button.hover) theme.editor_selection else theme.editor_widget; const style_base = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget;
try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_TRANSPARENT, nc.ALPHA_OPAQUE); try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_OPAQUE, nc.ALPHA_OPAQUE);
button.plane.erase(); button.plane.erase();
button.plane.home(); button.plane.home();
_ = button.plane.print(" {s} ", .{button.opts.label}) catch {}; const pointer = if (selected) "" else " ";
_ = button.plane.print("{s}{s} ", .{ pointer, button.opts.label }) catch {};
return false; return false;
} }
@ -112,6 +112,7 @@ pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result { fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result {
return switch (evtype) { return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, modifiers), nc.event_type.PRESS => self.mapPress(keypress, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, modifiers),
else => {}, else => {},
}; };
} }
@ -123,6 +124,7 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result {
'J' => self.cmd("toggle_logview", .{}), 'J' => self.cmd("toggle_logview", .{}),
'Q' => self.cmd("quit", .{}), 'Q' => self.cmd("quit", .{}),
'W' => self.cmd("close_file", .{}), 'W' => self.cmd("close_file", .{}),
'E' => self.cmd("open_recent_menu_down", .{}),
else => {}, else => {},
}, },
nc.mod.CTRL | nc.mod.SHIFT => switch (keynormal) { nc.mod.CTRL | nc.mod.SHIFT => switch (keynormal) {
@ -130,6 +132,7 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result {
'R' => self.cmd("restart", .{}), 'R' => self.cmd("restart", .{}),
'L' => self.cmd_async("toggle_logview"), 'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"), 'I' => self.cmd_async("toggle_inputview"),
'E' => self.cmd("open_recent_menu_up", .{}),
else => {}, else => {},
}, },
nc.mod.ALT => switch (keynormal) { nc.mod.ALT => switch (keynormal) {
@ -143,7 +146,9 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result {
nc.key.F11 => self.cmd("toggle_logview", .{}), nc.key.F11 => self.cmd("toggle_logview", .{}),
nc.key.F12 => self.cmd("toggle_inputview", .{}), nc.key.F12 => self.cmd("toggle_inputview", .{}),
nc.key.ESC => self.cmd("exit_overlay_mode", .{}), nc.key.ESC => self.cmd("exit_overlay_mode", .{}),
nc.key.ENTER => self.cmd("exit_overlay_mode", .{}), nc.key.UP => self.cmd("open_recent_menu_up", .{}),
nc.key.DOWN => self.cmd("open_recent_menu_down", .{}),
nc.key.ENTER => self.cmd("open_recent_menu_activate", .{}),
else => {}, else => {},
}, },
else => {}, else => {},
@ -161,3 +166,21 @@ fn msg(_: *Self, text: []const u8) tp.result {
fn cmd_async(_: *Self, name_: []const u8) tp.result { fn cmd_async(_: *Self, name_: []const u8) tp.result {
return tp.self_pid().send(.{ "cmd", name_ }); return tp.self_pid().send(.{ "cmd", name_ });
} }
const Commands = command.Collection(cmds);
const cmds = struct {
pub const Target = Self;
const Ctx = command.Context;
pub fn open_recent_menu_down(self: *Self, _: Ctx) tp.result {
self.menu.select_down();
}
pub fn open_recent_menu_up(self: *Self, _: Ctx) tp.result {
self.menu.select_up();
}
pub fn open_recent_menu_activate(self: *Self, _: Ctx) tp.result {
self.menu.activate_selected();
}
};