feat: add keyboard support to menus
This commit is contained in:
		
							parent
							
								
									ad2d82ce43
								
							
						
					
					
						commit
						2c4452dd81
					
				
					 5 changed files with 107 additions and 23 deletions
				
			
		|  | @ -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); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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 => {}, | ||||
|  |  | |||
|  | @ -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(); | ||||
|     } | ||||
| }; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue