From d7b48b40f122b1eabe69952e9fa8cfb5562a2a76 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 23 Jan 2025 22:32:08 +0100 Subject: [PATCH] feat(tabs): start work on a tabbar widget --- src/buffer/Buffer.zig | 1 + src/buffer/Manager.zig | 2 + src/tui/WidgetList.zig | 6 + src/tui/editor.zig | 10 +- src/tui/mode/overlay/buffer_palette.zig | 1 - src/tui/status/tabs.zig | 177 ++++++++++++++++++++++++ src/tui/status/widget.zig | 1 + 7 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 src/tui/status/tabs.zig diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 215a179..60fd470 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -39,6 +39,7 @@ file_exists: bool = true, file_eol_mode: EolMode = .lf, last_save_eol_mode: EolMode = .lf, file_utf8_sanitized: bool = false, +hidden: bool = false, undo_history: ?*UndoNode = null, redo_history: ?*UndoNode = null, diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index 673e311..1f9d84f 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -31,6 +31,7 @@ pub fn open_file(self: *Self, file_path: []const u8) Buffer.LoadFromFileError!*B break :blk buffer; }; buffer.update_last_used_time(); + buffer.hidden = false; return buffer; } @@ -44,6 +45,7 @@ pub fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) Buf break :blk buffer; }; buffer.update_last_used_time(); + buffer.hidden = false; return buffer; } diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index 2840917..9785dc4 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -97,6 +97,12 @@ pub fn remove(self: *Self, w: Widget) void { self.widgets.orderedRemove(i).widget.deinit(self.allocator); } +pub fn remove_all(self: *Self) void { + for (self.widgets.items) |*w| + w.widget.deinit(self.allocator); + self.widgets.clearRetainingCapacity(); +} + pub fn empty(self: *const Self) bool { return self.widgets.items.len == 0; } diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 5ab0100..5fc89d5 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -3786,13 +3786,21 @@ pub const Editor = struct { pub fn close_file(self: *Self, _: Context) Result { self.cancel_all_selections(); + if (self.buffer) |buffer| { + if (buffer.is_dirty()) + return tp.exit("unsaved changes"); + buffer.hidden = true; + } try self.close(); } pub const close_file_meta = .{ .description = "Close file" }; pub fn close_file_without_saving(self: *Self, _: Context) Result { self.cancel_all_selections(); - if (self.buffer) |buffer| buffer.reset_to_last_saved(); + if (self.buffer) |buffer| { + buffer.reset_to_last_saved(); + buffer.hidden = true; + } try self.close(); } pub const close_file_without_saving_meta = .{ .description = "Close file without saving" }; diff --git a/src/tui/mode/overlay/buffer_palette.zig b/src/tui/mode/overlay/buffer_palette.zig index 19a4aa1..0da6ae7 100644 --- a/src/tui/mode/overlay/buffer_palette.zig +++ b/src/tui/mode/overlay/buffer_palette.zig @@ -45,7 +45,6 @@ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { var iter = button.opts.label; if (!(cbor.matchString(&iter, &file_path) catch false)) return; tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", "navigate", .{} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); } diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig new file mode 100644 index 0000000..03f4087 --- /dev/null +++ b/src/tui/status/tabs.zig @@ -0,0 +1,177 @@ +const std = @import("std"); +const tp = @import("thespian"); + +const EventHandler = @import("EventHandler"); +const Plane = @import("renderer").Plane; +const Buffer = @import("Buffer"); + +const tui = @import("../tui.zig"); +const Widget = @import("../Widget.zig"); +const WidgetList = @import("../WidgetList.zig"); +const Button = @import("../Button.zig"); + +const dirty_indicator = ""; + +pub fn create(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler) @import("widget.zig").CreateError!Widget { + const self = try allocator.create(TabBar); + self.* = try TabBar.init(allocator, parent, event_handler); + return Widget.to(self); +} + +const TabBar = struct { + allocator: std.mem.Allocator, + plane: Plane, + widget_list: *WidgetList, + event_handler: ?EventHandler, + tab_buffers: []*Buffer = &[_]*Buffer{}, + + const Self = @This(); + + fn init(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler) !Self { + var w = try WidgetList.createH(allocator, parent, "tabs", .dynamic); + w.ctx = w; + return .{ + .allocator = allocator, + .plane = w.plane, + .widget_list = w, + .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); + allocator.destroy(self); + } + + pub fn layout(self: *Self) Widget.Layout { + return self.widget_list.layout; + } + + pub fn update(self: *Self) void { + self.update_tabs() catch {}; + self.widget_list.resize(Widget.Box.from(self.plane)); + self.widget_list.update(); + } + + pub fn render(self: *Self, theme: *const Widget.Theme) bool { + return self.widget_list.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 handle_resize(self: *Self, pos: Widget.Box) void { + self.widget_list.handle_resize(pos); + self.plane = self.widget_list.plane; + } + + pub fn get(self: *Self, name: []const u8) ?*Widget { + return self.widget_list.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 hover(self: *Self) bool { + return self.widget_list.hover(); + } + + fn update_tabs(self: *Self) !void { + self.widget_list.remove_all(); + try self.update_tab_buffers(); + var first = true; + for (self.tab_buffers) |buffer| { + 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)); + } + } + + 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); + defer self.allocator.free(buffers); + const exiting_buffers = self.tab_buffers; + defer self.allocator.free(exiting_buffers); + var result: std.ArrayListUnmanaged(*Buffer) = .{}; + 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) { + if (!buffer.hidden) + (try result.addOne(self.allocator)).* = buffer; + continue :outer; + }; + + // add new tabs + outer: for (buffers) |buffer| { + for (result.items) |result_buffer| if (result_buffer == buffer) + continue :outer; + if (!buffer.hidden) + (try result.addOne(self.allocator)).* = buffer; + } + + self.tab_buffers = 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); + } +}; + +const Tab = struct { + tabs: *TabBar, + file_path: []const u8, + + fn create( + tabs: *TabBar, + file_path: []const u8, + 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), + .on_click = Tab.on_click, + .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 {}; + } + + fn render(_: *@This(), btn: *Button.State(@This()), theme: *const Widget.Theme) bool { + btn.plane.set_base_style(theme.editor); + 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.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.putstr(" ") catch {}; + _ = btn.plane.putstr(btn.opts.label) catch {}; + _ = btn.plane.putstr(" ") catch {}; + return false; + } + + fn layout(_: *@This(), btn: *Button.State(@This())) Widget.Layout { + const len = btn.plane.egc_chunk_width(btn.opts.label, 0, 1); + return .{ .static = len + 2 }; + } + + fn name_from_buffer_file_path(file_path: []const u8) []const u8 { + 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; + } +}; diff --git a/src/tui/status/widget.zig b/src/tui/status/widget.zig index fe52436..f0e1c02 100644 --- a/src/tui/status/widget.zig +++ b/src/tui/status/widget.zig @@ -18,6 +18,7 @@ const widgets = std.static_string_map.StaticStringMap(CreateFunction).initCompti .{ "spacer", @import("blank.zig").Create(.{ .static = 1 }) }, .{ "clock", @import("clock.zig").create }, .{ "keybind", @import("keybindstate.zig").create }, + .{ "tabs", @import("tabs.zig").create }, }); pub const CreateError = error{ OutOfMemory, Exit }; pub const CreateFunction = *const fn (allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler) CreateError!Widget;