const std = @import("std"); const cbor = @import("cbor"); const tp = @import("thespian"); const root = @import("soft_root").root; const EventHandler = @import("EventHandler"); const Plane = @import("renderer").Plane; const Buffer = @import("Buffer"); const input = @import("input"); 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, padding: []const u8 = " ", padding_left: usize = 2, padding_right: usize = 1, clean_indicator: []const u8 = " ", clean_indicator_fg: ?colors = null, dirty_indicator: []const u8 = "", dirty_indicator_fg: ?colors = null, close_icon: []const u8 = "󰅖", close_icon_fg: colors = .Error, save_icon: []const u8 = "󰆓", save_icon_fg: ?colors = null, 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, file_type_icon: bool = true, 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); } pub 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, place_next: Placement = .atend, tab_style: Style, tab_style_bufs: [][]const u8, const Self = @This(); const Placement = union(enum) { atend, before: usize, after: usize, }; const TabBarTab = struct { buffer_ref: usize, widget: Widget, view: ?usize, }; 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.render_decoration = null; 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 { const drag_source, const drag_btn = tui.get_drag_source(); self.update_tabs(drag_source) catch {}; self.widget_list_widget.resize(Widget.Box.from(self.plane)); self.widget_list_widget.update(); for (self.widget_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| for (split.widgets.items) |*widgetstate| if (widgetstate.widget.dynamic_cast(Tab.ButtonType)) |btn| if (btn.drag_pos) |_| tui.update_drag_source(&widgetstate.widget, drag_btn); tui.refresh_hover(@src()); } 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; var buffer_ref_a: usize = undefined; var buffer_ref_b: usize = 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(.{"move_tab_next"})) { self.move_tab_next(); } else if (try m.match(.{"move_tab_previous"})) { self.move_tab_previous(); } else if (try m.match(.{ "swap_tabs", tp.extract(&buffer_ref_a), tp.extract(&buffer_ref_b) })) { self.swap_tabs(buffer_ref_a, buffer_ref_b); } else if (try m.match(.{ "place_next_tab", "after", tp.extract(&buffer_ref_a) })) { self.place_next_tab(.after, buffer_ref_a); } else if (try m.match(.{ "place_next_tab", "before", tp.extract(&buffer_ref_a) })) { self.place_next_tab(.before, buffer_ref_a); } else if (try m.match(.{ "place_next_tab", "atend" })) { self.place_next = .atend; } 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; } fn handle_event(self: *Self, from: tp.pid_ref, m: tp.message) tp.result { if (self.event_handler) |event_handler| try event_handler.send(from, m); if (try m.match(.{ "D", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.more })) { const dragging = for (self.tabs, 0..) |*tab, idx| { if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn| if (btn.drag_pos) |_| break idx; } else return; const hover_ = for (self.tabs, 0..) |*tab, idx| { if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn| if (btn.hover) break idx; } else return; if (dragging != hover_) { self.swap_tabs_by_index(dragging, hover_); if (self.tabs[dragging].widget.dynamic_cast(Tab.ButtonType)) |btn| btn.hover = false; self.update(); } } } 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: *const Self, name: []const u8) ?*const 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, drag_source: ?*Widget) !void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); if (!try self.update_tab_buffers()) return; var prev_widget_count: usize = 0; for (self.widget_list.widgets.items) |*split_widgetstate| if (split_widgetstate.widget.dynamic_cast(WidgetList)) |split| { prev_widget_count += 1; for (split.widgets.items) |_| prev_widget_count += 1; }; for (self.widget_list.widgets.items) |*split_widget| if (split_widget.widget.dynamic_cast(WidgetList)) |split| { for (split.widgets.items) |*widget| if (&widget.widget == drag_source) tui.reset_drag_context(); }; while (self.widget_list.pop()) |split_widget| if (split_widget.dynamic_cast(WidgetList)) |split| { while (split.pop()) |widget| if (widget.dynamic_cast(Tab.ButtonType) == null) widget.deinit(self.widget_list.allocator); split.deinit(self.widget_list.allocator); }; var max_view: usize = 0; for (self.tabs) |tab| max_view = @max(max_view, tab.view orelse 0); var widget_count: usize = 0; for (0..max_view + 1) |view| { var first = true; var view_widget_list = try WidgetList.createH(self.allocator, self.widget_list.plane, "split", .dynamic); try self.widget_list.add(view_widget_list.widget()); widget_count += 1; for (self.tabs) |tab| { const tab_view = tab.view orelse 0; if (tab_view != view) continue; if (first) { first = false; } else { try view_widget_list.add(try self.make_spacer(view_widget_list.plane)); widget_count += 1; } try view_widget_list.add(tab.widget); widget_count += 1; if (tab.widget.dynamic_cast(Tab.ButtonType)) |btn| { if (buffer_manager.buffer_from_ref(tab.buffer_ref)) |buffer| try btn.update_label(Tab.name_from_buffer(buffer)); } } } if (prev_widget_count != widget_count) tui.refresh_hover(@src()); } fn update_tab_buffers(self: *Self) !bool { 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) try self.place_new_tab(&result, buffer); } self.tabs = try result.toOwnedSlice(self.allocator); if (existing_tabs.len != self.tabs.len) return true; for (existing_tabs, self.tabs) |tab_a, tab_b| { if (tab_a.buffer_ref == tab_b.buffer_ref and tab_a.view == tab_b.view) continue; return true; } return false; } fn place_new_tab(self: *Self, result: *std.ArrayListUnmanaged(TabBarTab), buffer: *Buffer) !void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); const buffer_ref = buffer_manager.buffer_to_ref(buffer); const tab = try Tab.create(self, buffer_ref, &self.tab_style); const pos = switch (self.place_next) { .atend => try result.addOne(self.allocator), .before => |i| if (i < result.items.len) &(try result.addManyAt(self.allocator, i, 1))[0] else try result.addOne(self.allocator), .after => |i| if (i < result.items.len - 1) &(try result.addManyAt(self.allocator, i + 1, 1))[0] else try result.addOne(self.allocator), }; pos.* = .{ .buffer_ref = buffer_ref, .widget = tab, .view = buffer.get_last_view() }; self.place_next = .atend; } fn make_spacer(self: @This(), parent: Plane) !Widget { return spacer.create( self.allocator, parent, 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 move_tab_next(self: *Self) void { tp.trace(tp.channel.debug, .{"move_tab_next"}); for (self.tabs, 0..) |*tab, idx| if (tab.buffer_ref == self.active_buffer_ref and idx < self.tabs.len - 1) { const tmp = self.tabs[idx + 1]; self.tabs[idx + 1] = self.tabs[idx]; self.tabs[idx] = tmp; break; }; } fn move_tab_previous(self: *Self) void { tp.trace(tp.channel.debug, .{"move_tab_previous"}); for (self.tabs, 0..) |*tab, idx| if (tab.buffer_ref == self.active_buffer_ref and idx > 0) { const tmp = self.tabs[idx - 1]; self.tabs[idx - 1] = self.tabs[idx]; self.tabs[idx] = tmp; break; }; } fn swap_tabs(self: *Self, buffer_ref_a: usize, buffer_ref_b: usize) void { tp.trace(tp.channel.debug, .{ "swap_tabs", "buffers", buffer_ref_a, buffer_ref_b }); if (buffer_ref_a == buffer_ref_b) { tp.trace(tp.channel.debug, .{ "swap_tabs", "same_buffer" }); return; } const tab_a_idx = for (self.tabs, 0..) |*tab, idx| if (tab.buffer_ref == buffer_ref_a) break idx else continue else { tp.trace(tp.channel.debug, .{ "swap_tabs", "not_found", "buffer_ref_a" }); return; }; const tab_b_idx = for (self.tabs, 0..) |*tab, idx| if (tab.buffer_ref == buffer_ref_b) break idx else continue else { tp.trace(tp.channel.debug, .{ "swap_tabs", "not_found", "buffer_ref_b" }); return; }; self.swap_tabs_by_index(tab_a_idx, tab_b_idx); } fn swap_tabs_by_index(self: *Self, tab_a_idx: usize, tab_b_idx: usize) void { const tmp = self.tabs[tab_a_idx]; self.tabs[tab_a_idx] = self.tabs[tab_b_idx]; self.tabs[tab_b_idx] = tmp; const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); if (buffer_manager.buffer_from_ref(self.tabs[tab_a_idx].buffer_ref)) |buffer_a| if (buffer_manager.buffer_from_ref(self.tabs[tab_b_idx].buffer_ref)) |buffer_b| { const view_a = buffer_a.get_last_view(); const view_b = buffer_b.get_last_view(); if (view_a != view_b) { buffer_a.set_last_view(view_b); buffer_b.set_last_view(view_a); } }; tp.trace(tp.channel.debug, .{ "swap_tabs", "swapped", "indexes", tab_a_idx, tab_b_idx }); } fn place_next_tab(self: *Self, position: enum { before, after }, buffer_ref: usize) void { tp.trace(tp.channel.debug, .{ "place_next_tab", position, buffer_ref }); const tab_idx = for (self.tabs, 0..) |*tab, idx| if (tab.buffer_ref == buffer_ref) break idx else continue else { tp.trace(tp.channel.debug, .{ "place_next_tab", "not_found", buffer_ref }); return; }; self.place_next = switch (position) { .before => .{ .before = tab_idx }, .after => .{ .after = tab_idx }, }; } 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 {}; } pub fn write_state(self: *const Self, writer: *std.Io.Writer) error{WriteFailed}!void { try cbor.writeArrayHeader(writer, self.tabs.len); for (self.tabs) |tab| try cbor.writeValue(writer, ref_to_name(tab.buffer_ref)); } fn ref_to_name(buffer_ref: usize) ?[]const u8 { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); return if (buffer_manager.buffer_from_ref(buffer_ref)) |buffer| buffer.get_file_path() else null; } pub fn extract_state(self: *Self, iter: *[]const u8) !void { var iter2 = iter.*; self.allocator.free(self.tabs); self.tabs = &.{}; var result: std.ArrayListUnmanaged(TabBarTab) = .{}; errdefer result.deinit(self.allocator); var count = cbor.decodeArrayHeader(&iter2) catch return error.MatchTabArrayFailed; while (count > 0) : (count -= 1) { var buffer_name: ?[]const u8 = undefined; if (!(cbor.matchValue(&iter2, cbor.extract(&buffer_name)) catch false)) return error.MatchTabBufferNameFailed; if (buffer_name) |name| { const buffer_ref_, const buffer_view = name_to_ref_and_view(name); if (buffer_ref_) |buffer_ref| (try result.addOne(self.allocator)).* = .{ .buffer_ref = buffer_ref, .widget = try Tab.create(self, buffer_ref, &self.tab_style), .view = buffer_view, }; } } self.tabs = try result.toOwnedSlice(self.allocator); iter.* = iter2; } fn name_to_ref_and_view(buffer_name: []const u8) struct { ?usize, ?usize } { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); return if (buffer_manager.get_buffer_for_file(buffer_name)) |buffer| .{ buffer_manager.buffer_to_ref(buffer), buffer.get_last_view() } else .{ null, null }; } }; const Tab = struct { tabbar: *TabBar, buffer_ref: usize, tab_style: *const Style, close_pos: ?i32 = null, save_pos: ?i32 = null, on_event: ?EventHandler = null, const Mode = enum { active, inactive, selected }; const ButtonType = Button.Options(@This()).ButtonType; fn create( tabbar: *TabBar, buffer_ref: usize, tab_style: *const Style, ) !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 = EventHandler.bind(tabbar, TabBar.handle_event), }); } fn on_click(self: *@This(), _: *ButtonType, pos: Widget.Pos) void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); if (buffer_manager.buffer_from_ref(self.buffer_ref)) |buffer| { if (self.close_pos) |close_pos| if (pos.x == close_pos) { tp.self_pid().send(.{ "cmd", "close_buffer", .{buffer.get_file_path()} }) catch {}; return; }; if (self.save_pos) |save_pos| if (pos.x == save_pos) { tp.self_pid().send(.{ "cmd", "save_buffer", .{buffer.get_file_path()} }) catch {}; return; }; tp.self_pid().send(.{ "cmd", "navigate", .{ .file = buffer.get_file_path() } }) catch {}; } } fn on_click2(self: *@This(), _: *ButtonType, _: Widget.Pos) 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: *ButtonType, theme: *const Widget.Theme) bool { const active = self.tabbar.active_buffer_ref == self.buffer_ref; if (btn.drag_pos) |pos| { self.render_dragging(&btn.plane, theme); const anchor: Widget.Pos = btn.drag_anchor orelse .{}; var box = Widget.Box.from(btn.plane); box.y = @intCast(@max(pos.y, anchor.y) - anchor.y); box.x = @intCast(@max(pos.x, anchor.x) - anchor.x); if (tui.top_layer(box.to_layer())) |top_layer| { self.render_selected(top_layer, btn.opts.label, false, theme, active); } } else { const mode: Mode = if (btn.hover) .selected else if (active) .active else .inactive; switch (mode) { .selected => self.render_selected(&btn.plane, btn.opts.label, btn.hover, theme, active), .active => self.render_active(&btn.plane, btn.opts.label, btn.hover, theme), .inactive => self.render_inactive(&btn.plane, btn.opts.label, btn.hover, theme), } } return false; } fn render_selected(self: *@This(), plane: *Plane, label: []const u8, hover: bool, theme: *const Widget.Theme, active: bool) void { plane.set_base_style(theme.editor); plane.erase(); plane.home(); plane.set_style(.{ .fg = self.tab_style.inactive_fg.from_theme(theme), .bg = self.tab_style.inactive_bg.from_theme(theme), }); plane.fill(" "); plane.home(); if (active) { plane.set_style(.{ .fg = self.tab_style.selected_fg.from_theme(theme), .bg = self.tab_style.selected_bg.from_theme(theme), }); plane.fill(" "); plane.home(); } plane.set_style(.{ .fg = self.tab_style.selected_left_fg.from_theme(theme), .bg = self.tab_style.selected_left_bg.from_theme(theme), }); _ = plane.putstr(self.tab_style.selected_left) catch {}; plane.set_style(.{ .fg = self.tab_style.selected_fg.from_theme(theme), .bg = self.tab_style.selected_bg.from_theme(theme), }); self.render_content(plane, label, hover, self.tab_style.selected_fg.from_theme(theme), theme); plane.set_style(.{ .fg = self.tab_style.selected_right_fg.from_theme(theme), .bg = self.tab_style.selected_right_bg.from_theme(theme), }); _ = plane.putstr(self.tab_style.selected_right) catch {}; } fn render_active(self: *@This(), plane: *Plane, label: []const u8, hover: bool, theme: *const Widget.Theme) void { plane.set_base_style(theme.editor); plane.erase(); plane.home(); plane.set_style(.{ .fg = self.tab_style.inactive_fg.from_theme(theme), .bg = self.tab_style.inactive_bg.from_theme(theme), }); plane.fill(" "); plane.home(); plane.set_style(.{ .fg = self.tab_style.active_fg.from_theme(theme), .bg = self.tab_style.active_bg.from_theme(theme), }); plane.fill(" "); plane.home(); plane.set_style(.{ .fg = self.tab_style.active_left_fg.from_theme(theme), .bg = self.tab_style.active_left_bg.from_theme(theme), }); _ = plane.putstr(self.tab_style.active_left) catch {}; plane.set_style(.{ .fg = self.tab_style.active_fg.from_theme(theme), .bg = self.tab_style.active_bg.from_theme(theme), }); self.render_content(plane, label, hover, self.tab_style.active_fg.from_theme(theme), theme); plane.set_style(.{ .fg = self.tab_style.active_right_fg.from_theme(theme), .bg = self.tab_style.active_right_bg.from_theme(theme), }); _ = plane.putstr(self.tab_style.active_right) catch {}; } fn render_inactive(self: *@This(), plane: *Plane, label: []const u8, hover: bool, theme: *const Widget.Theme) void { plane.set_base_style(theme.editor); plane.erase(); plane.home(); plane.set_style(.{ .fg = self.tab_style.inactive_fg.from_theme(theme), .bg = self.tab_style.inactive_bg.from_theme(theme), }); plane.fill(" "); plane.home(); plane.set_style(.{ .fg = self.tab_style.inactive_left_fg.from_theme(theme), .bg = self.tab_style.inactive_left_bg.from_theme(theme), }); _ = plane.putstr(self.tab_style.inactive_left) catch {}; plane.set_style(.{ .fg = self.tab_style.inactive_fg.from_theme(theme), .bg = self.tab_style.inactive_bg.from_theme(theme), }); self.render_content(plane, label, hover, self.tab_style.inactive_fg.from_theme(theme), theme); plane.set_style(.{ .fg = self.tab_style.inactive_right_fg.from_theme(theme), .bg = self.tab_style.inactive_right_bg.from_theme(theme), }); _ = plane.putstr(self.tab_style.inactive_right) catch {}; } fn render_dragging(self: *@This(), plane: *Plane, theme: *const Widget.Theme) void { plane.set_base_style(theme.editor); plane.erase(); plane.home(); plane.set_style(.{ .fg = self.tab_style.inactive_fg.from_theme(theme), .bg = self.tab_style.inactive_bg.from_theme(theme), }); plane.fill(" "); plane.home(); } fn render_content(self: *@This(), plane: *Plane, label: []const u8, hover: bool, fg: ?Widget.Theme.Color, theme: *const Widget.Theme) void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); const buffer_ = buffer_manager.buffer_from_ref(self.buffer_ref); const is_dirty = if (buffer_) |buffer| buffer.is_dirty() else false; const auto_save = if (buffer_) |buffer| buffer.is_auto_save() else false; self.render_padding(plane, .left); if (self.tab_style.file_type_icon) if (buffer_) |buffer| if (buffer.file_type_icon) |icon| { const color_: ?u24 = if (buffer.file_type_color) |color| if (!(color == 0xFFFFFF or color == 0x000000 or color == 0x000001)) color else null else null; if (color_) |color| plane.set_style(.{ .fg = .{ .color = color } }); _ = plane.putstr(icon) catch {}; if (color_) |_| plane.set_style(.{ .fg = fg }); _ = plane.putstr(" ") catch {}; }; _ = plane.putstr(label) catch {}; _ = plane.putstr(" ") catch {}; self.close_pos = null; self.save_pos = null; if (hover) { if (is_dirty) { if (self.tab_style.save_icon_fg) |color| plane.set_style(.{ .fg = color.from_theme(theme) }); self.save_pos = plane.cursor_x(); _ = plane.putstr(self.tabbar.tab_style.save_icon) catch {}; } else { plane.set_style(.{ .fg = self.tab_style.close_icon_fg.from_theme(theme) }); self.close_pos = plane.cursor_x(); _ = plane.putstr(self.tabbar.tab_style.close_icon) catch {}; } } else if (is_dirty and !auto_save) { if (self.tab_style.dirty_indicator_fg) |color| plane.set_style(.{ .fg = color.from_theme(theme) }); _ = plane.putstr(self.tabbar.tab_style.dirty_indicator) catch {}; } else { if (self.tab_style.clean_indicator_fg) |color| plane.set_style(.{ .fg = color.from_theme(theme) }); _ = plane.putstr(self.tabbar.tab_style.clean_indicator) catch {}; } plane.set_style(.{ .fg = fg }); self.render_padding(plane, .right); } fn render_padding(self: *@This(), plane: *Plane, side: enum { left, right }) void { var padding: usize = switch (side) { .left => self.tab_style.padding_left, .right => self.tab_style.padding_right, }; while (padding > 0) : (padding -= 1) _ = plane.putstr(self.tab_style.padding) catch {}; } fn layout(self: *@This(), btn: *ButtonType) 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_padding = plane.egc_chunk_width(tab_style.padding, 0, 1) * (tab_style.padding_left + tab_style.padding_right); const len_file_icon: usize = if (tab_style.file_type_icon) 3 else 0; const len_close_icon = plane.egc_chunk_width(tab_style.close_icon, 0, 1); const len_dirty_indicator = if (dirty) plane.egc_chunk_width(tab_style.dirty_indicator, 0, 1) else 0; const len_dirty_close = @max(len_close_icon, len_dirty_indicator) + 1; // +1 for the leading space return len_padding + len_file_icon + len_dirty_close + 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; } fn write_state(self: *const @This(), writer: *std.Io.Writer) error{OutOfMemory}!void { try cbor.writeArrayHeader(writer, 9); try cbor.writeValue(writer, self.get_file_path()); try cbor.writeValue(writer, self.file_exists); try cbor.writeValue(writer, self.file_eol_mode); try cbor.writeValue(writer, self.hidden); try cbor.writeValue(writer, self.ephemeral); try cbor.writeValue(writer, self.meta); try cbor.writeValue(writer, self.file_type_name); } fn extract_state(self: *@This(), iter: *[]const u8) !void { _ = self; _ = iter; } }; 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, Error, Warning, Information, Hint, 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, .Error => theme.editor_error.fg, .Warning => theme.editor_warning.fg, .Information => theme.editor_information.fg, .Hint => theme.editor_hint.fg, }; } };