diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index df31630..b3399a9 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -1,6 +1,8 @@ { "project": { "press": [ + ["ctrl+tab", "next_tab"], + ["ctrl+shift+tab", "previous_tab"], ["ctrl+shift+e", "switch_buffers"], ["ctrl+0", "reset_fontsize"], ["ctrl+plus", "adjust_fontsize", 1.0], diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index 3e8d546..e62b6f3 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -103,6 +103,10 @@ pub fn remove_all(self: *Self) void { self.widgets.clearRetainingCapacity(); } +pub fn pop(self: *Self) ?Widget { + return if (self.widgets.popOrNull()) |ws| ws.widget else null; +} + pub fn empty(self: *const Self) bool { return self.widgets.items.len == 0; } diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index d66bb23..437e2f4 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -744,6 +744,16 @@ const cmds = struct { tui.rdr().reset_fontface(); } pub const reset_fontface_meta = .{ .description = "Reset font to configured face" }; + + pub fn next_tab(self: *Self, _: Ctx) Result { + _ = try self.widgets_widget.msg(.{"next_tab"}); + } + pub const next_tab_meta = .{ .description = "Switch to next tab" }; + + pub fn previous_tab(self: *Self, _: Ctx) Result { + _ = try self.widgets_widget.msg(.{"previous_tab"}); + } + pub const previous_tab_meta = .{ .description = "Switch to previous tab" }; }; pub fn handle_editor_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result { diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index 03f4087..f01796f 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -10,7 +10,8 @@ const Widget = @import("../Widget.zig"); const WidgetList = @import("../WidgetList.zig"); const Button = @import("../Button.zig"); -const dirty_indicator = ""; +const dirty_indicator = " "; +const padding = " "; pub fn create(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler) @import("widget.zig").CreateError!Widget { const self = try allocator.create(TabBar); @@ -22,11 +23,18 @@ const TabBar = struct { allocator: std.mem.Allocator, plane: Plane, widget_list: *WidgetList, + widget_list_widget: Widget, event_handler: ?EventHandler, - tab_buffers: []*Buffer = &[_]*Buffer{}, + tabs: []TabBarTab = &[_]TabBarTab{}, + active_buffer: ?*Buffer = null, const Self = @This(); + const TabBarTab = struct { + buffer: *Buffer, + widget: Widget, + }; + fn init(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler) !Self { var w = try WidgetList.createH(allocator, parent, "tabs", .dynamic); w.ctx = w; @@ -34,142 +42,208 @@ const TabBar = struct { .allocator = allocator, .plane = w.plane, .widget_list = w, + .widget_list_widget = w.widget(), .event_handler = event_handler, }; } pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { - self.allocator.free(self.tab_buffers); - self.widget_list.deinit(allocator); + self.allocator.free(self.tabs); + self.widget_list_widget.deinit(allocator); allocator.destroy(self); } pub fn layout(self: *Self) Widget.Layout { - return self.widget_list.layout; + return self.widget_list_widget.layout(); } pub fn update(self: *Self) void { self.update_tabs() catch {}; - self.widget_list.resize(Widget.Box.from(self.plane)); - self.widget_list.update(); + self.widget_list_widget.resize(Widget.Box.from(self.plane)); + self.widget_list_widget.update(); } pub fn render(self: *Self, theme: *const Widget.Theme) bool { - return self.widget_list.render(theme); + self.plane.set_base_style(theme.statusbar); + self.plane.erase(); + self.plane.home(); + return self.widget_list_widget.render(theme); } - pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { - return self.widget_list.receive(from_, m); + pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { + tp.trace(tp.channel.widget, .{"receive"}); + tp.trace(tp.channel.widget, m); + + const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); + var file_path: []const u8 = undefined; + if (try m.match(.{"next_tab"})) { + self.select_next_tab(); + } else if (try m.match(.{"previous_tab"})) { + self.select_previous_tab(); + } else if (try m.match(.{ "E", "open", tp.extract(&file_path), tp.more })) { + self.active_buffer = buffer_manager.get_buffer_for_file(file_path); + } else if (try m.match(.{ "E", "close" })) { + self.active_buffer = null; + } + return false; } pub fn handle_resize(self: *Self, pos: Widget.Box) void { - self.widget_list.handle_resize(pos); + self.widget_list_widget.resize(pos); self.plane = self.widget_list.plane; } pub fn get(self: *Self, name: []const u8) ?*Widget { - return self.widget_list.get(name); + return self.widget_list_widget.get(name); } - pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, self_w: *Widget) bool { - return self.widget_list.walk(ctx, f, self_w); + pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, _: *Widget) bool { + return self.widget_list_widget.walk(ctx, f); } pub fn hover(self: *Self) bool { - return self.widget_list.hover(); + return self.widget_list_widget.hover(); } fn update_tabs(self: *Self) !void { - self.widget_list.remove_all(); try self.update_tab_buffers(); + while (self.widget_list.pop()) |widget| if (widget.dynamic_cast(Button.State(Tab)) == null) + widget.deinit(self.widget_list.allocator); var first = true; - for (self.tab_buffers) |buffer| { + for (self.tabs) |tab| { if (first) { first = false; } else { try self.widget_list.add(try self.make_spacer(1)); } - // const hint = if (buffer.is_dirty()) dirty_indicator else ""; - try self.widget_list.add(try Tab.create(self, buffer.file_path, self.event_handler)); + try self.widget_list.add(tab.widget); } } fn update_tab_buffers(self: *Self) !void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); - const buffers = try buffer_manager.list_most_recently_used(self.allocator); + const buffers = try buffer_manager.list_unordered(self.allocator); defer self.allocator.free(buffers); - const exiting_buffers = self.tab_buffers; - defer self.allocator.free(exiting_buffers); - var result: std.ArrayListUnmanaged(*Buffer) = .{}; + const existing_tabs = self.tabs; + defer self.allocator.free(existing_tabs); + var result: std.ArrayListUnmanaged(TabBarTab) = .{}; errdefer result.deinit(self.allocator); // add existing tabs in original order if they still exist - outer: for (exiting_buffers) |exiting_buffer| - for (buffers) |buffer| if (exiting_buffer == buffer) { + outer: for (existing_tabs) |existing_tab| + for (buffers) |buffer| if (existing_tab.buffer == buffer) { if (!buffer.hidden) - (try result.addOne(self.allocator)).* = buffer; + (try result.addOne(self.allocator)).* = existing_tab; continue :outer; }; // add new tabs outer: for (buffers) |buffer| { - for (result.items) |result_buffer| if (result_buffer == buffer) + for (result.items) |result_tab| if (result_tab.buffer == buffer) continue :outer; if (!buffer.hidden) - (try result.addOne(self.allocator)).* = buffer; + (try result.addOne(self.allocator)).* = .{ + .buffer = buffer, + .widget = try Tab.create(self, buffer, self.event_handler), + }; } - self.tab_buffers = try result.toOwnedSlice(self.allocator); + self.tabs = try result.toOwnedSlice(self.allocator); } fn make_spacer(self: @This(), comptime size: usize) !Widget { return @import("blank.zig").Create(.{ .static = size })(self.allocator, self.widget_list.plane, null); } + + fn select_next_tab(self: *Self) void { + tp.trace(tp.channel.widget, .{"select_next_tab"}); + var activate_next = false; + var first: ?*const TabBarTab = null; + for (self.tabs) |*tab| { + if (first == null) + first = tab; + if (activate_next) + return navigate_to_tab(tab); + if (tab.buffer == self.active_buffer) + activate_next = true; + } + if (first) |tab| + navigate_to_tab(tab); + } + + fn select_previous_tab(self: *Self) void { + tp.trace(tp.channel.widget, .{"select_previous_tab"}); + var goto: ?*const TabBarTab = if (self.tabs.len > 0) &self.tabs[self.tabs.len - 1] else null; + for (self.tabs) |*tab| { + if (tab.buffer == self.active_buffer) + break; + goto = tab; + } + if (goto) |tab| navigate_to_tab(tab); + } + + fn navigate_to_tab(tab: *const TabBarTab) void { + tp.self_pid().send(.{ "cmd", "navigate", .{ .file = tab.buffer.file_path } }) catch {}; + } }; const Tab = struct { - tabs: *TabBar, - file_path: []const u8, + tabbar: *TabBar, + buffer: *Buffer, fn create( - tabs: *TabBar, - file_path: []const u8, + tabbar: *TabBar, + buffer: *Buffer, event_handler: ?EventHandler, ) !Widget { - return Button.create_widget(Tab, tabs.allocator, tabs.widget_list.plane, .{ - .ctx = .{ .tabs = tabs, .file_path = file_path }, - .label = name_from_buffer_file_path(file_path), + return Button.create_widget(Tab, tabbar.allocator, tabbar.widget_list.plane, .{ + .ctx = .{ .tabbar = tabbar, .buffer = buffer }, + .label = name_from_buffer(buffer), .on_click = Tab.on_click, + .on_click2 = Tab.on_click2, .on_layout = Tab.layout, .on_render = Tab.render, - // .on_receive = receive, .on_event = event_handler, }); } + fn on_click(self: *@This(), _: *Button.State(@This())) void { - tp.self_pid().send(.{ "cmd", "navigate", .{ .file = self.file_path } }) catch {}; + tp.self_pid().send(.{ "cmd", "navigate", .{ .file = self.buffer.file_path } }) catch {}; } - fn render(_: *@This(), btn: *Button.State(@This()), theme: *const Widget.Theme) bool { - btn.plane.set_base_style(theme.editor); + fn on_click2(self: *@This(), _: *Button.State(@This())) void { + tp.self_pid().send(.{ "cmd", "delete_buffer", .{self.buffer.file_path} }) catch {}; + } + + fn render(self: *@This(), btn: *Button.State(@This()), theme: *const Widget.Theme) bool { + const active = self.tabbar.active_buffer == self.buffer; + btn.plane.set_base_style(theme.statusbar); btn.plane.erase(); btn.plane.home(); - btn.plane.set_style(if (btn.active) theme.editor_cursor else if (btn.hover) theme.statusbar_hover else theme.statusbar); + btn.plane.set_style(if (active) theme.editor else if (btn.hover) theme.statusbar_hover else theme.statusbar); btn.plane.fill(" "); btn.plane.home(); - btn.plane.set_style(if (btn.active) theme.editor_cursor else if (btn.hover) theme.statusbar_hover else theme.statusbar); + btn.plane.set_style(if (active) theme.editor else if (btn.hover) theme.statusbar_hover else theme.statusbar); _ = btn.plane.putstr(" ") catch {}; + if (self.buffer.is_dirty()) + _ = btn.plane.putstr(dirty_indicator) catch {}; _ = btn.plane.putstr(btn.opts.label) catch {}; _ = btn.plane.putstr(" ") catch {}; return false; } - fn layout(_: *@This(), btn: *Button.State(@This())) Widget.Layout { + fn layout(self: *@This(), btn: *Button.State(@This())) Widget.Layout { const len = btn.plane.egc_chunk_width(btn.opts.label, 0, 1); - return .{ .static = len + 2 }; + const len_dirty_indicator = btn.plane.egc_chunk_width(dirty_indicator, 0, 1); + const len_padding = btn.plane.egc_chunk_width(padding, 0, 1); + return if (self.buffer.is_dirty()) + .{ .static = len + (2 * len_padding) + len_dirty_indicator } + else + .{ .static = len + (2 * len_padding) }; } - fn name_from_buffer_file_path(file_path: []const u8) []const u8 { + fn name_from_buffer(buffer: *Buffer) []const u8 { + const file_path = buffer.file_path; const basename_begin = std.mem.lastIndexOfScalar(u8, file_path, std.fs.path.sep); const basename = if (basename_begin) |begin| file_path[begin + 1 ..] else file_path; return basename;