feat(tabs): complete tabs widget and next_/previous_tabs

This commit is contained in:
CJ van den Berg 2025-01-24 23:26:41 +01:00
parent 41b230d17f
commit 5dd47f7248
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
4 changed files with 135 additions and 45 deletions

View file

@ -1,6 +1,8 @@
{ {
"project": { "project": {
"press": [ "press": [
["ctrl+tab", "next_tab"],
["ctrl+shift+tab", "previous_tab"],
["ctrl+shift+e", "switch_buffers"], ["ctrl+shift+e", "switch_buffers"],
["ctrl+0", "reset_fontsize"], ["ctrl+0", "reset_fontsize"],
["ctrl+plus", "adjust_fontsize", 1.0], ["ctrl+plus", "adjust_fontsize", 1.0],

View file

@ -103,6 +103,10 @@ pub fn remove_all(self: *Self) void {
self.widgets.clearRetainingCapacity(); 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 { pub fn empty(self: *const Self) bool {
return self.widgets.items.len == 0; return self.widgets.items.len == 0;
} }

View file

@ -744,6 +744,16 @@ const cmds = struct {
tui.rdr().reset_fontface(); tui.rdr().reset_fontface();
} }
pub const reset_fontface_meta = .{ .description = "Reset font to configured face" }; 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 { pub fn handle_editor_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {

View file

@ -11,6 +11,7 @@ const WidgetList = @import("../WidgetList.zig");
const Button = @import("../Button.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 { pub fn create(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler) @import("widget.zig").CreateError!Widget {
const self = try allocator.create(TabBar); const self = try allocator.create(TabBar);
@ -22,11 +23,18 @@ const TabBar = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
plane: Plane, plane: Plane,
widget_list: *WidgetList, widget_list: *WidgetList,
widget_list_widget: Widget,
event_handler: ?EventHandler, event_handler: ?EventHandler,
tab_buffers: []*Buffer = &[_]*Buffer{}, tabs: []TabBarTab = &[_]TabBarTab{},
active_buffer: ?*Buffer = null,
const Self = @This(); const Self = @This();
const TabBarTab = struct {
buffer: *Buffer,
widget: Widget,
};
fn init(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler) !Self { fn init(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler) !Self {
var w = try WidgetList.createH(allocator, parent, "tabs", .dynamic); var w = try WidgetList.createH(allocator, parent, "tabs", .dynamic);
w.ctx = w; w.ctx = w;
@ -34,142 +42,208 @@ const TabBar = struct {
.allocator = allocator, .allocator = allocator,
.plane = w.plane, .plane = w.plane,
.widget_list = w, .widget_list = w,
.widget_list_widget = w.widget(),
.event_handler = event_handler, .event_handler = event_handler,
}; };
} }
pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
self.allocator.free(self.tab_buffers); self.allocator.free(self.tabs);
self.widget_list.deinit(allocator); self.widget_list_widget.deinit(allocator);
allocator.destroy(self); allocator.destroy(self);
} }
pub fn layout(self: *Self) Widget.Layout { pub fn layout(self: *Self) Widget.Layout {
return self.widget_list.layout; return self.widget_list_widget.layout();
} }
pub fn update(self: *Self) void { pub fn update(self: *Self) void {
self.update_tabs() catch {}; self.update_tabs() catch {};
self.widget_list.resize(Widget.Box.from(self.plane)); self.widget_list_widget.resize(Widget.Box.from(self.plane));
self.widget_list.update(); self.widget_list_widget.update();
} }
pub fn render(self: *Self, theme: *const Widget.Theme) bool { 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 { pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
return self.widget_list.receive(from_, m); 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 { 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; self.plane = self.widget_list.plane;
} }
pub fn get(self: *Self, name: []const u8) ?*Widget { 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 { pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, _: *Widget) bool {
return self.widget_list.walk(ctx, f, self_w); return self.widget_list_widget.walk(ctx, f);
} }
pub fn hover(self: *Self) bool { pub fn hover(self: *Self) bool {
return self.widget_list.hover(); return self.widget_list_widget.hover();
} }
fn update_tabs(self: *Self) !void { fn update_tabs(self: *Self) !void {
self.widget_list.remove_all();
try self.update_tab_buffers(); 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; var first = true;
for (self.tab_buffers) |buffer| { for (self.tabs) |tab| {
if (first) { if (first) {
first = false; first = false;
} else { } else {
try self.widget_list.add(try self.make_spacer(1)); try self.widget_list.add(try self.make_spacer(1));
} }
// const hint = if (buffer.is_dirty()) dirty_indicator else ""; try self.widget_list.add(tab.widget);
try self.widget_list.add(try Tab.create(self, buffer.file_path, self.event_handler));
} }
} }
fn update_tab_buffers(self: *Self) !void { fn update_tab_buffers(self: *Self) !void {
const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); 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); defer self.allocator.free(buffers);
const exiting_buffers = self.tab_buffers; const existing_tabs = self.tabs;
defer self.allocator.free(exiting_buffers); defer self.allocator.free(existing_tabs);
var result: std.ArrayListUnmanaged(*Buffer) = .{}; var result: std.ArrayListUnmanaged(TabBarTab) = .{};
errdefer result.deinit(self.allocator); errdefer result.deinit(self.allocator);
// add existing tabs in original order if they still exist // add existing tabs in original order if they still exist
outer: for (exiting_buffers) |exiting_buffer| outer: for (existing_tabs) |existing_tab|
for (buffers) |buffer| if (exiting_buffer == buffer) { for (buffers) |buffer| if (existing_tab.buffer == buffer) {
if (!buffer.hidden) if (!buffer.hidden)
(try result.addOne(self.allocator)).* = buffer; (try result.addOne(self.allocator)).* = existing_tab;
continue :outer; continue :outer;
}; };
// add new tabs // add new tabs
outer: for (buffers) |buffer| { 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; continue :outer;
if (!buffer.hidden) 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 { fn make_spacer(self: @This(), comptime size: usize) !Widget {
return @import("blank.zig").Create(.{ .static = size })(self.allocator, self.widget_list.plane, null); 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 { const Tab = struct {
tabs: *TabBar, tabbar: *TabBar,
file_path: []const u8, buffer: *Buffer,
fn create( fn create(
tabs: *TabBar, tabbar: *TabBar,
file_path: []const u8, buffer: *Buffer,
event_handler: ?EventHandler, event_handler: ?EventHandler,
) !Widget { ) !Widget {
return Button.create_widget(Tab, tabs.allocator, tabs.widget_list.plane, .{ return Button.create_widget(Tab, tabbar.allocator, tabbar.widget_list.plane, .{
.ctx = .{ .tabs = tabs, .file_path = file_path }, .ctx = .{ .tabbar = tabbar, .buffer = buffer },
.label = name_from_buffer_file_path(file_path), .label = name_from_buffer(buffer),
.on_click = Tab.on_click, .on_click = Tab.on_click,
.on_click2 = Tab.on_click2,
.on_layout = Tab.layout, .on_layout = Tab.layout,
.on_render = Tab.render, .on_render = Tab.render,
// .on_receive = receive,
.on_event = event_handler, .on_event = event_handler,
}); });
} }
fn on_click(self: *@This(), _: *Button.State(@This())) void { 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 { fn on_click2(self: *@This(), _: *Button.State(@This())) void {
btn.plane.set_base_style(theme.editor); 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.erase();
btn.plane.home(); 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.fill(" ");
btn.plane.home(); 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 {}; _ = 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(btn.opts.label) catch {};
_ = btn.plane.putstr(" ") catch {}; _ = btn.plane.putstr(" ") catch {};
return false; 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); 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_begin = std.mem.lastIndexOfScalar(u8, file_path, std.fs.path.sep);
const basename = if (basename_begin) |begin| file_path[begin + 1 ..] else file_path; const basename = if (basename_begin) |begin| file_path[begin + 1 ..] else file_path;
return basename; return basename;