const std = @import("std"); const tp = @import("thespian"); const root = @import("root"); 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 default_min_tabs = 2; const @"style.config" = struct { default_minimum_tabs_shown: usize = 2, dirty_indicator: []const u8 = " ", spacer: []const u8 = "|", spacer_fg: colors = .active_bg, spacer_bg: colors = .inactive_bg, bar_fg: colors = .inactive_fg, bar_bg: colors = .inactive_bg, active_fg: colors = .active_fg, active_bg: colors = .active_bg, active_left: []const u8 = "🭅", active_left_fg: colors = .active_bg, active_left_bg: colors = .inactive_bg, active_right: []const u8 = "🭐", active_right_fg: colors = .active_bg, active_right_bg: colors = .inactive_bg, inactive_fg: colors = .inactive_fg, inactive_bg: colors = .inactive_bg, inactive_left: []const u8 = " ", inactive_left_fg: colors = .inactive_fg, inactive_left_bg: colors = .inactive_bg, inactive_right: []const u8 = " ", inactive_right_fg: colors = .inactive_fg, inactive_right_bg: colors = .inactive_bg, selected_fg: colors = .active_fg, selected_bg: colors = .active_bg, selected_left: []const u8 = "🭅", selected_left_fg: colors = .active_bg, selected_left_bg: colors = .inactive_bg, selected_right: []const u8 = "🭐", selected_right_fg: colors = .active_bg, selected_right_bg: colors = .inactive_bg, include_files: []const u8 = "", }; pub const Style = @"style.config"; pub fn create(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler, arg: ?[]const u8) @import("widget.zig").CreateError!Widget { const min_tabs = if (arg) |str_size| std.fmt.parseInt(usize, str_size, 10) catch null else null; const self = try allocator.create(TabBar); errdefer allocator.destroy(self); self.* = try TabBar.init(allocator, parent, event_handler, min_tabs); return Widget.to(self); } const TabBar = struct { allocator: std.mem.Allocator, plane: Plane, widget_list: *WidgetList, widget_list_widget: Widget, event_handler: ?EventHandler, tabs: []TabBarTab = &[_]TabBarTab{}, active_buffer_ref: ?usize = null, minimum_tabs_shown: usize, tab_style: Style, tab_style_bufs: [][]const u8, const Self = @This(); const TabBarTab = struct { buffer_ref: usize, widget: Widget, }; fn init(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler, min_tabs: ?usize) !Self { var w = try WidgetList.createH(allocator, parent, "tabs", .dynamic); w.ctx = w; const tab_style, const tab_style_bufs = root.read_config(Style, allocator); return .{ .allocator = allocator, .plane = w.plane, .widget_list = w, .widget_list_widget = w.widget(), .event_handler = event_handler, .tab_style = tab_style, .tab_style_bufs = tab_style_bufs, .minimum_tabs_shown = min_tabs orelse tab_style.default_minimum_tabs_shown, }; } pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { root.free_config(self.allocator, self.tab_style_bufs); self.allocator.free(self.tabs); self.widget_list_widget.deinit(allocator); allocator.destroy(self); } pub fn layout(self: *Self) Widget.Layout { return if (self.tabs.len >= self.minimum_tabs_shown) self.widget_list_widget.layout() else .{ .static = 0 }; } pub fn update(self: *Self) void { self.update_tabs() catch {}; 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 { self.plane.set_base_style(theme.editor); self.plane.erase(); self.plane.home(); self.plane.set_style(.{ .fg = self.tab_style.bar_fg.from_theme(theme), .bg = self.tab_style.bar_bg.from_theme(theme), }); self.plane.fill(" "); self.plane.home(); return self.widget_list_widget.render(theme); } pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { 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_ref = if (buffer_manager.get_buffer_for_file(file_path)) |buffer| buffer_manager.buffer_to_ref(buffer) else null; } else if (try m.match(.{ "E", "close" })) { self.active_buffer_ref = null; } return false; } pub fn handle_resize(self: *Self, pos: Widget.Box) void { 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_widget.get(name); } 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_widget.hover(); } fn update_tabs(self: *Self) !void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); try self.update_tab_buffers(); const prev_widget_count = self.widget_list.widgets.items.len; 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.tabs) |tab| { if (first) { first = false; } else { try self.widget_list.add(try self.make_spacer()); } try self.widget_list.add(tab.widget); if (tab.widget.dynamic_cast(Button.State(Tab))) |btn| { if (buffer_manager.buffer_from_ref(tab.buffer_ref)) |buffer| try btn.update_label(Tab.name_from_buffer(buffer)); } } if (prev_widget_count != self.widget_list.widgets.items.len) tui.refresh_hover(); } 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_unordered(self.allocator); defer self.allocator.free(buffers); 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 (existing_tabs) |existing_tab| for (buffers) |buffer| if (existing_tab.buffer_ref == buffer_manager.buffer_to_ref(buffer)) { if (!buffer.hidden) (try result.addOne(self.allocator)).* = existing_tab; continue :outer; }; // add new tabs outer: for (buffers) |buffer| { for (result.items) |result_tab| if (result_tab.buffer_ref == buffer_manager.buffer_to_ref(buffer)) continue :outer; if (!buffer.hidden) { const buffer_ref = buffer_manager.buffer_to_ref(buffer); (try result.addOne(self.allocator)).* = .{ .buffer_ref = buffer_ref, .widget = try Tab.create(self, buffer_ref, &self.tab_style, self.event_handler), }; } } self.tabs = try result.toOwnedSlice(self.allocator); } fn make_spacer(self: @This()) !Widget { return spacer.create( self.allocator, self.widget_list.plane, self.tab_style.spacer, self.tab_style.spacer_fg, self.tab_style.spacer_bg, null, ); } fn select_next_tab(self: *Self) void { tp.trace(tp.channel.debug, .{"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_ref == self.active_buffer_ref) activate_next = true; } if (first) |tab| navigate_to_tab(tab); } fn select_previous_tab(self: *Self) void { tp.trace(tp.channel.debug, .{"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_ref == self.active_buffer_ref) break; goto = tab; } if (goto) |tab| navigate_to_tab(tab); } fn navigate_to_tab(tab: *const TabBarTab) void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); if (buffer_manager.buffer_from_ref(tab.buffer_ref)) |buffer| tp.self_pid().send(.{ "cmd", "navigate", .{ .file = buffer.get_file_path() } }) catch {}; } }; const Tab = struct { tabbar: *TabBar, buffer_ref: usize, tab_style: *const Style, const Mode = enum { active, inactive, selected }; fn create( tabbar: *TabBar, buffer_ref: usize, tab_style: *const Style, event_handler: ?EventHandler, ) !Widget { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); const buffer = buffer_manager.buffer_from_ref(buffer_ref); return Button.create_widget(Tab, tabbar.allocator, tabbar.widget_list.plane, .{ .ctx = .{ .tabbar = tabbar, .buffer_ref = buffer_ref, .tab_style = tab_style }, .label = if (buffer) |buf| name_from_buffer(buf) else "???", .on_click = Tab.on_click, .on_click2 = Tab.on_click2, .on_layout = Tab.layout, .on_render = Tab.render, .on_event = event_handler, }); } fn on_click(self: *@This(), _: *Button.State(@This())) void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); if (buffer_manager.buffer_from_ref(self.buffer_ref)) |buffer| tp.self_pid().send(.{ "cmd", "navigate", .{ .file = buffer.get_file_path() } }) catch {}; } fn on_click2(self: *@This(), _: *Button.State(@This())) void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); if (buffer_manager.buffer_from_ref(self.buffer_ref)) |buffer| tp.self_pid().send(.{ "cmd", "close_buffer", .{buffer.get_file_path()} }) catch {}; } fn render(self: *@This(), btn: *Button.State(@This()), theme: *const Widget.Theme) bool { const active = self.tabbar.active_buffer_ref == self.buffer_ref; const mode: Mode = if (btn.hover) .selected else if (active) .active else .inactive; switch (mode) { .selected => self.render_selected(btn, theme, active), .active => self.render_active(btn, theme), .inactive => self.render_inactive(btn, theme), } return false; } fn render_selected(self: *@This(), btn: *Button.State(@This()), theme: *const Widget.Theme, active: bool) void { btn.plane.set_base_style(theme.editor); btn.plane.erase(); btn.plane.home(); btn.plane.set_style(.{ .fg = self.tab_style.inactive_fg.from_theme(theme), .bg = self.tab_style.inactive_bg.from_theme(theme), }); btn.plane.fill(" "); btn.plane.home(); if (active) { btn.plane.set_style(.{ .fg = self.tab_style.selected_fg.from_theme(theme), .bg = self.tab_style.selected_bg.from_theme(theme), }); btn.plane.fill(" "); btn.plane.home(); } btn.plane.set_style(.{ .fg = self.tab_style.selected_left_fg.from_theme(theme), .bg = self.tab_style.selected_left_bg.from_theme(theme), }); _ = btn.plane.putstr(self.tab_style.selected_left) catch {}; btn.plane.set_style(.{ .fg = self.tab_style.selected_fg.from_theme(theme), .bg = self.tab_style.selected_bg.from_theme(theme), }); self.render_content(btn); btn.plane.set_style(.{ .fg = self.tab_style.selected_right_fg.from_theme(theme), .bg = self.tab_style.selected_right_bg.from_theme(theme), }); _ = btn.plane.putstr(self.tab_style.selected_right) catch {}; } fn render_active(self: *@This(), btn: *Button.State(@This()), theme: *const Widget.Theme) void { btn.plane.set_base_style(theme.editor); btn.plane.erase(); btn.plane.home(); btn.plane.set_style(.{ .fg = self.tab_style.inactive_fg.from_theme(theme), .bg = self.tab_style.inactive_bg.from_theme(theme), }); btn.plane.fill(" "); btn.plane.home(); btn.plane.set_style(.{ .fg = self.tab_style.active_fg.from_theme(theme), .bg = self.tab_style.active_bg.from_theme(theme), }); btn.plane.fill(" "); btn.plane.home(); btn.plane.set_style(.{ .fg = self.tab_style.active_left_fg.from_theme(theme), .bg = self.tab_style.active_left_bg.from_theme(theme), }); _ = btn.plane.putstr(self.tab_style.active_left) catch {}; btn.plane.set_style(.{ .fg = self.tab_style.active_fg.from_theme(theme), .bg = self.tab_style.active_bg.from_theme(theme), }); self.render_content(btn); btn.plane.set_style(.{ .fg = self.tab_style.active_right_fg.from_theme(theme), .bg = self.tab_style.active_right_bg.from_theme(theme), }); _ = btn.plane.putstr(self.tab_style.active_right) catch {}; } fn render_inactive(self: *@This(), btn: *Button.State(@This()), theme: *const Widget.Theme) void { btn.plane.set_base_style(theme.editor); btn.plane.erase(); btn.plane.home(); btn.plane.set_style(.{ .fg = self.tab_style.inactive_fg.from_theme(theme), .bg = self.tab_style.inactive_bg.from_theme(theme), }); btn.plane.fill(" "); btn.plane.home(); btn.plane.set_style(.{ .fg = self.tab_style.inactive_left_fg.from_theme(theme), .bg = self.tab_style.inactive_left_bg.from_theme(theme), }); _ = btn.plane.putstr(self.tab_style.inactive_left) catch {}; btn.plane.set_style(.{ .fg = self.tab_style.inactive_fg.from_theme(theme), .bg = self.tab_style.inactive_bg.from_theme(theme), }); self.render_content(btn); btn.plane.set_style(.{ .fg = self.tab_style.inactive_right_fg.from_theme(theme), .bg = self.tab_style.inactive_right_bg.from_theme(theme), }); _ = btn.plane.putstr(self.tab_style.inactive_right) catch {}; } fn render_content(self: *@This(), btn: *Button.State(@This())) void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); const is_dirty = if (buffer_manager.buffer_from_ref(self.buffer_ref)) |buffer| buffer.is_dirty() else false; if (is_dirty) _ = btn.plane.putstr(self.tabbar.tab_style.dirty_indicator) catch {}; _ = btn.plane.putstr(btn.opts.label) catch {}; } fn layout(self: *@This(), btn: *Button.State(@This())) Widget.Layout { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); const is_dirty = if (buffer_manager.buffer_from_ref(self.buffer_ref)) |buffer| buffer.is_dirty() else false; const active = self.tabbar.active_buffer_ref == self.buffer_ref; const len = btn.plane.egc_chunk_width(btn.opts.label, 0, 1); const len_padding = padding_len(btn.plane, self.tabbar.tab_style, active, is_dirty); return .{ .static = len + len_padding }; } fn padding_len(plane: Plane, tab_style: Style, active: bool, dirty: bool) usize { const len_dirty_indicator = if (dirty) plane.egc_chunk_width(tab_style.dirty_indicator, 0, 1) else 0; return len_dirty_indicator + if (active) plane.egc_chunk_width(tab_style.active_left, 0, 1) + plane.egc_chunk_width(tab_style.active_right, 0, 1) else plane.egc_chunk_width(tab_style.inactive_left, 0, 1) + plane.egc_chunk_width(tab_style.inactive_right, 0, 1); } fn name_from_buffer(buffer: *Buffer) []const u8 { const file_path = buffer.get_file_path(); if (file_path.len > 0 and file_path[0] == '*') return 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; } }; const spacer = struct { plane: Plane, layout_: Widget.Layout, on_event: ?EventHandler, content: []const u8, fg: colors, bg: colors, const Self = @This(); fn create( allocator: std.mem.Allocator, parent: Plane, content: []const u8, fg: colors, bg: colors, event_handler: ?EventHandler, ) @import("widget.zig").CreateError!Widget { const self: *Self = try allocator.create(Self); errdefer allocator.destroy(self); self.* = .{ .plane = try Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent), .layout_ = .{ .static = self.plane.egc_chunk_width(content, 0, 1) }, .on_event = event_handler, .content = content, .fg = fg, .bg = bg, }; return Widget.to(self); } pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { self.plane.deinit(); allocator.destroy(self); } pub fn layout(self: *Self) Widget.Layout { return self.layout_; } pub fn render(self: *Self, theme: *const Widget.Theme) bool { self.plane.set_base_style(theme.editor); self.plane.erase(); self.plane.home(); self.plane.set_style(.{ .fg = self.fg.from_theme(theme), .bg = self.bg.from_theme(theme), }); self.plane.fill(" "); self.plane.home(); _ = self.plane.putstr(self.content) catch {}; return false; } pub fn receive(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool { var btn: u32 = 0; if (try m.match(.{ "D", tp.any, tp.extract(&btn), tp.more })) { if (self.on_event) |h| h.send(from, m) catch {}; return true; } return false; } }; const colors = enum { default_bg, default_fg, active_bg, active_fg, inactive_bg, inactive_fg, selected_bg, selected_fg, fn from_theme(color: colors, theme: *const Widget.Theme) ?Widget.Theme.Color { return switch (color) { .default_bg => theme.editor.bg, .default_fg => theme.editor.fg, .active_bg => theme.tab_active.bg, .active_fg => theme.tab_active.fg, .inactive_bg => theme.tab_inactive.bg, .inactive_fg => theme.tab_inactive.fg, .selected_bg => theme.tab_selected.bg, .selected_fg => theme.tab_selected.fg, }; } };