diff --git a/src/tui/Menu.zig b/src/tui/Menu.zig index bf247af..ee1b81f 100644 --- a/src/tui/Menu.zig +++ b/src/tui/Menu.zig @@ -12,16 +12,16 @@ pub fn Options(context: type) type { ctx: Context, 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_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) void = on_resize_default, pub const Context = context; 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 { - const style_base = if (button.active) theme.editor_cursor else if (button.hover) theme.editor_selection else theme.editor; - const bg_alpha: c_uint = if (button.active or button.hover) nc.ALPHA_OPAQUE else nc.ALPHA_TRANSPARENT; + 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 or selected) theme.editor_selection else theme.editor; + 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); button.plane.erase(); 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(), .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; return self; } @@ -59,9 +60,14 @@ pub fn State(ctx_type: type) type { a: std.mem.Allocator, menu: *WidgetList, 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 options_type = Options(ctx_type); + const button_type = Button.State(*Self); pub fn deinit(self: *Self, a: std.mem.Allocator) void { self.menu.deinit(a); @@ -92,12 +98,19 @@ pub fn State(ctx_type: type) type { 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 { return self.opts.on_layout(self.opts.ctx, button); } 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 { @@ -116,5 +129,31 @@ pub fn State(ctx_type: type) type { pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool { 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); + } }; } diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index d9b57e2..fe24eed 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -24,8 +24,9 @@ widgets: ArrayList(WidgetState), layout: Layout, direction: Direction, 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_ctx: ?*anyopaque = null, pub fn createH(a: Allocator, parent: Widget, name: [:0]const u8, layout_: Layout) !*Self { const self: *Self = try a.create(Self); @@ -125,6 +126,8 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { break; }; + self.on_render(self.ctx, theme); + var more = false; for (self.widgets.items) |*w| if (w.widget.render(theme)) { @@ -133,6 +136,8 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { 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 { for (self.widgets.items) |*w| 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 { - 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 { diff --git a/src/tui/home.zig b/src/tui/home.zig index 649f98f..efa36d2 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -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 { - const style_base = if (button.active) theme.editor_cursor else if (button.hover) theme.editor_selection else theme.editor; - const bg_alpha: c_uint = if (button.active or button.hover) nc.ALPHA_OPAQUE else nc.ALPHA_TRANSPARENT; +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 or selected) theme.editor_selection else theme.editor; + 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); button.plane.erase(); 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; set_style(button.plane, style_subtext); 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); _ = button.plane.print("{s}", .{button.opts.label[sep + 1 ..]}) catch {}; return false; @@ -209,6 +210,18 @@ const cmds = struct { pub const Target = Self; 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 { self.fire = if (self.fire) |*fire| ret: { fire.deinit(); diff --git a/src/tui/mode/input/home.zig b/src/tui/mode/input/home.zig index 26baedc..c42df9b 100644 --- a/src/tui/mode/input/home.zig +++ b/src/tui/mode/input/home.zig @@ -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 { return switch (evtype) { nc.event_type.PRESS => self.mapPress(keypress, modifiers), + nc.event_type.REPEAT => self.mapPress(keypress, modifiers), else => {}, }; } @@ -90,6 +91,9 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { nc.key.F10 => self.cmd("theme_next", .{}), nc.key.F11 => self.cmd("toggle_logview", .{}), 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 => {}, diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index 5dc1204..767a7e4 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -21,19 +21,17 @@ menu: *Menu.State(*Self), logger: log.Logger, count: usize = 0, longest: usize = 0, +commands: Commands = undefined, 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 self: *Self = try a.create(Self); self.* = .{ .a = a, - .menu = try Menu.create(*Self, a, tui.current().mainview, .{ - .ctx = self, - .on_render = on_render_menu, - .on_resize = on_resize_menu, - }), + .menu = try Menu.create(*Self, a, tui.current().mainview, .{ .ctx = self, .on_render = on_render_menu, .on_resize = on_resize_menu }), .logger = log.logger(@typeName(Self)), }; + try self.commands.init(self); try tui.current().message_filters.add(MessageFilter.bind(self, receive_project_manager)); try project_manager.request_recent_files(); 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 { + self.commands.deinit(); tui.current().message_filters.remove_ptr(self); if (tui.current().mainview.dynamic_cast(mainview)) |mv| mv.floating_views.remove(self.menu.menu_widget); self.a.destroy(self); } -fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme) bool { - const style_base = if (button.active) theme.editor_cursor else if (button.hover) theme.editor_selection else theme.editor_widget; - try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_TRANSPARENT, nc.ALPHA_OPAQUE); +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 or selected) theme.editor_selection else theme.editor_widget; + try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_OPAQUE, nc.ALPHA_OPAQUE); button.plane.erase(); 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; } @@ -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 { return switch (evtype) { nc.event_type.PRESS => self.mapPress(keypress, modifiers), + nc.event_type.REPEAT => self.mapPress(keypress, modifiers), else => {}, }; } @@ -123,6 +124,7 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { 'J' => self.cmd("toggle_logview", .{}), 'Q' => self.cmd("quit", .{}), 'W' => self.cmd("close_file", .{}), + 'E' => self.cmd("open_recent_menu_down", .{}), else => {}, }, 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", .{}), 'L' => self.cmd_async("toggle_logview"), 'I' => self.cmd_async("toggle_inputview"), + 'E' => self.cmd("open_recent_menu_up", .{}), else => {}, }, 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.F12 => self.cmd("toggle_inputview", .{}), 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 => {}, @@ -161,3 +166,21 @@ fn msg(_: *Self, text: []const u8) tp.result { fn cmd_async(_: *Self, name_: []const u8) tp.result { 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(); + } +};