From 758de0cdbc480d1eeff531dbc4319d47e15f71ad Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 24 Oct 2025 16:18:57 +0200 Subject: [PATCH 1/5] feat: add drag_pos and drag_anchor to Button --- src/tui/Button.zig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/tui/Button.zig b/src/tui/Button.zig index 04356d7..6d97eb2 100644 --- a/src/tui/Button.zig +++ b/src/tui/Button.zig @@ -77,6 +77,8 @@ fn State(ctx_type: type) type { plane: Plane, active: bool = false, hover: bool = false, + drag_anchor: ?Widget.Pos = null, + drag_pos: ?Widget.Pos = null, opts: Options(ctx_type), const Self = @This(); @@ -120,6 +122,7 @@ fn State(ctx_type: type) type { switch (btn_enum) { input.mouse.BUTTON1 => { self.active = true; + self.drag_anchor = self.to_rel_cursor(x, y); tui.need_render(); }, input.mouse.BUTTON4, input.mouse.BUTTON5 => { @@ -131,9 +134,12 @@ fn State(ctx_type: type) type { return true; } else if (try m.match(.{ "B", input.event.release, tp.extract(&btn), tp.any, tp.extract(&x), tp.extract(&y), tp.any, tp.any })) { self.call_click_handler(@enumFromInt(btn), self.to_rel_cursor(x, y)); + self.drag_anchor = null; + self.drag_pos = null; tui.need_render(); return true; - } else if (try m.match(.{ "D", input.event.press, tp.extract(&btn), tp.more })) { + } else if (try m.match(.{ "D", input.event.press, tp.extract(&btn), tp.any, tp.extract(&x), tp.extract(&y), tp.any, tp.any })) { + self.drag_pos = .{ .x = x, .y = y }; if (self.opts.on_event) |h| { self.active = false; h.send(from, m) catch {}; @@ -145,6 +151,8 @@ fn State(ctx_type: type) type { h.send(from, m) catch {}; } self.call_click_handler(@enumFromInt(btn), self.to_rel_cursor(x, y)); + self.drag_anchor = null; + self.drag_pos = null; tui.need_render(); return true; } else if (try m.match(.{ "H", tp.extract(&self.hover) })) { From 1f9628445eabe050dfc0a2cf126ad6ad6151fe86 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 24 Oct 2025 16:19:48 +0200 Subject: [PATCH 2/5] feat: add renderer.Layer --- src/renderer/vaxis/Layer.zig | 117 ++++++++++++++++++++++++++++++++ src/renderer/vaxis/renderer.zig | 1 + 2 files changed, 118 insertions(+) create mode 100644 src/renderer/vaxis/Layer.zig diff --git a/src/renderer/vaxis/Layer.zig b/src/renderer/vaxis/Layer.zig new file mode 100644 index 0000000..2c1b23e --- /dev/null +++ b/src/renderer/vaxis/Layer.zig @@ -0,0 +1,117 @@ +const std = @import("std"); +const vaxis = @import("vaxis"); + +pub const Plane = @import("Plane.zig"); + +const Layer = @This(); + +view: View, +y_off: i32 = 0, +x_off: i32 = 0, +plane_: Plane, + +const View = struct { + allocator: std.mem.Allocator, + screen: vaxis.Screen, + unicode: *const vaxis.Unicode, + + pub const Config = struct { + h: u16, + w: u16, + }; + + pub fn init(allocator: std.mem.Allocator, unicode: *const vaxis.Unicode, config: Config) std.mem.Allocator.Error!View { + return .{ + .allocator = allocator, + .screen = try vaxis.Screen.init(allocator, .{ + .rows = config.h, + .cols = config.w, + .x_pixel = 0, + .y_pixel = 0, + }), + .unicode = unicode, + }; + } + + pub fn deinit(self: *View) void { + self.screen.deinit(self.allocator); + } +}; + +pub const Options = struct { + y: i32 = 0, + x: i32 = 0, + h: u16 = 0, + w: u16 = 0, +}; + +pub fn init(allocator: std.mem.Allocator, unicode: *const vaxis.Unicode, opts: Options) std.mem.Allocator.Error!*Layer { + const self = try allocator.create(Layer); + self.* = .{ + .view = try View.init(allocator, unicode, .{ + .h = opts.h, + .w = opts.w, + }), + .y_off = opts.y, + .x_off = opts.x, + .plane_ = undefined, + }; + const name = "layer"; + self.plane_ = .{ + .window = self.window(), + .name_buf = undefined, + .name_len = name.len, + }; + @memcpy(self.plane_.name_buf[0..name.len], name); + return self; +} + +pub fn deinit(self: *Layer) void { + const allocator = self.view.allocator; + self.view.deinit(); + allocator.destroy(self); +} + +fn window(self: *Layer) vaxis.Window { + return .{ + .x_off = 0, + .y_off = 0, + .parent_x_off = 0, + .parent_y_off = 0, + .width = self.view.screen.width, + .height = self.view.screen.height, + .screen = &self.view.screen, + .unicode = self.view.unicode, + }; +} + +pub fn plane(self: *Layer) *Plane { + return &self.plane_; +} + +pub fn draw(self: *const Layer, plane_: Plane) void { + if (self.x_off >= plane_.window.width) return; + if (self.y_off >= plane_.window.height) return; + + const src_y = 0; + const src_x = 0; + const src_h: usize = self.view.screen.height; + const src_w = self.view.screen.width; + + const dst_dim_x: i32 = @intCast(plane_.dim_x()); + // const dst_dim_y: i32 = @intCast(plane_.dim_y()); + const dst_y = self.y_off; + const dst_x = self.x_off; + const dst_w = @min(src_w, dst_dim_x - dst_x); + // const dst_h = @min(src_h, dst_dim_y - dst_y); + + for (src_y..src_h) |src_row_| { + const src_row: i32 = @intCast(src_row_); + const src_row_offset = src_row * src_w; + const dst_row_offset = (dst_y + src_row) * plane_.window.screen.width; + @memcpy( + plane_.window.screen.buf[@intCast(dst_row_offset + dst_x)..@intCast(dst_row_offset + dst_x + dst_w)], + self.view.screen.buf[@intCast(src_row_offset + src_x)..@intCast(src_row_offset + src_w)], + ); + } +} diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 7ecf83d..b71767c 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -9,6 +9,7 @@ const builtin = @import("builtin"); const RGB = @import("color").RGB; pub const Plane = @import("Plane.zig"); +pub const Layer = @import("Layer.zig"); pub const Cell = @import("Cell.zig"); pub const CursorShape = vaxis.Cell.CursorShape; From f79d7cc156c72a9b184cf9f5c3b042bf088fd06f Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 24 Oct 2025 16:20:19 +0200 Subject: [PATCH 3/5] feat: allow conversion of Widget.Box to renderer.Layer.Options --- src/tui/Box.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tui/Box.zig b/src/tui/Box.zig index cb31862..c11b441 100644 --- a/src/tui/Box.zig +++ b/src/tui/Box.zig @@ -1,4 +1,5 @@ const Plane = @import("renderer").Plane; +const Layer = @import("renderer").Layer; const Self = @This(); @@ -35,6 +36,15 @@ pub fn from(n: Plane) Self { }; } +pub fn to_layer(self: Self) Layer.Options { + return .{ + .y = @intCast(self.y), + .x = @intCast(self.x), + .h = @intCast(self.h), + .w = @intCast(self.w), + }; +} + pub fn is_abs_coord_inside(self: Self, y: usize, x: usize) bool { return y >= self.y and y < self.y + self.h and x >= self.x and x < self.x + self.w; } From 2614e3ac7fcb78def299b54eafae576dd8d4753d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 24 Oct 2025 16:21:00 +0200 Subject: [PATCH 4/5] feat: add tui top layer rendering support --- src/tui/tui.zig | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index a5cbf37..b30b9a9 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -31,6 +31,7 @@ const Allocator = std.mem.Allocator; allocator: Allocator, rdr_: renderer, +top_layer_: ?*renderer.Layer = null, config_: @import("config"), config_bufs: [][]const u8, session_tab_width: ?usize = null, @@ -507,12 +508,25 @@ fn render(self: *Self) void { break :ret if (self.mainview_) |mv| mv.render(self.current_theme()) else false; }; + if (self.top_layer_) |top_layer_| { + const frame = tracy.initZone(@src(), .{ .name = "tui blit top layer" }); + defer frame.deinit(); + self.logger.print("top_layer: {}:{}:{}:{}", .{ + top_layer_.y_off, + top_layer_.x_off, + top_layer_.view.screen.height, + top_layer_.view.screen.width, + }); + top_layer_.draw(self.rdr_.stdplane()); + } + { const frame = tracy.initZone(@src(), .{ .name = renderer.log_name ++ " render" }); defer frame.deinit(); self.rdr_.render() catch |e| self.logger.err("render", e); tracy.frameMark(); } + self.top_layer_reset(); self.idle_frame_count = if (self.unrendered_input_events_count > 0) 0 @@ -1427,6 +1441,24 @@ fn stdplane(self: *Self) renderer.Plane { return self.rdr_.stdplane(); } +pub fn top_layer(opts: renderer.Layer.Options) ?*renderer.Plane { + const self = current(); + if (self.top_layer_) |_| return null; + self.top_layer_ = renderer.Layer.init( + self.allocator, + self.rdr_.stdplane().window.unicode, + opts, + ) catch @panic("OOM toplayer"); + return self.top_layer_.?.plane(); +} + +fn top_layer_reset(self: *Self) void { + if (self.top_layer_) |top_layer_| { + top_layer_.deinit(); + self.top_layer_ = null; + } +} + pub fn egc_chunk_width(chunk: []const u8, abs_col: usize, tab_width: usize) usize { return plane().egc_chunk_width(chunk, abs_col, tab_width); } From 27e44173c8a7abebfb05d1e356053d867cdbccbd Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 24 Oct 2025 16:21:21 +0200 Subject: [PATCH 5/5] WIP feat: make tabs draggable --- src/tui/status/tabs.zig | 165 +++++++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 71 deletions(-) diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index b9699c0..63f5155 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -463,159 +463,182 @@ const Tab = struct { fn render(self: *@This(), btn: *ButtonType, 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), + 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(), btn: *ButtonType, theme: *const Widget.Theme, active: bool) void { - btn.plane.set_base_style(theme.editor); - btn.plane.erase(); - btn.plane.home(); - btn.plane.set_style(.{ + 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), }); - btn.plane.fill(" "); - btn.plane.home(); + plane.fill(" "); + plane.home(); if (active) { - btn.plane.set_style(.{ + 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(); + plane.fill(" "); + plane.home(); } - btn.plane.set_style(.{ + 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 {}; + _ = plane.putstr(self.tab_style.selected_left) catch {}; - btn.plane.set_style(.{ + 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, self.tab_style.selected_fg.from_theme(theme), theme); + self.render_content(plane, label, hover, self.tab_style.selected_fg.from_theme(theme), theme); - btn.plane.set_style(.{ + 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 {}; + _ = plane.putstr(self.tab_style.selected_right) catch {}; } - fn render_active(self: *@This(), btn: *ButtonType, theme: *const Widget.Theme) void { - btn.plane.set_base_style(theme.editor); - btn.plane.erase(); - btn.plane.home(); - btn.plane.set_style(.{ + 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), }); - btn.plane.fill(" "); - btn.plane.home(); - btn.plane.set_style(.{ + 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), }); - btn.plane.fill(" "); - btn.plane.home(); + plane.fill(" "); + plane.home(); - btn.plane.set_style(.{ + 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 {}; + _ = plane.putstr(self.tab_style.active_left) catch {}; - btn.plane.set_style(.{ + 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, self.tab_style.active_fg.from_theme(theme), theme); + self.render_content(plane, label, hover, self.tab_style.active_fg.from_theme(theme), theme); - btn.plane.set_style(.{ + 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 {}; + _ = plane.putstr(self.tab_style.active_right) catch {}; } - fn render_inactive(self: *@This(), btn: *ButtonType, theme: *const Widget.Theme) void { - btn.plane.set_base_style(theme.editor); - btn.plane.erase(); - btn.plane.home(); - btn.plane.set_style(.{ + 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), }); - btn.plane.fill(" "); - btn.plane.home(); + plane.fill(" "); + plane.home(); - btn.plane.set_style(.{ + 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 {}; + _ = plane.putstr(self.tab_style.inactive_left) catch {}; - btn.plane.set_style(.{ + 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, self.tab_style.inactive_fg.from_theme(theme), theme); + self.render_content(plane, label, hover, self.tab_style.inactive_fg.from_theme(theme), theme); - btn.plane.set_style(.{ + 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 {}; + _ = plane.putstr(self.tab_style.inactive_right) catch {}; } - fn render_content(self: *@This(), btn: *ButtonType, fg: ?Widget.Theme.Color, theme: *const Widget.Theme) void { + 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; - self.render_padding(&btn.plane, .left); + 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| - btn.plane.set_style(.{ .fg = .{ .color = color } }); - _ = btn.plane.putstr(icon) catch {}; + plane.set_style(.{ .fg = .{ .color = color } }); + _ = plane.putstr(icon) catch {}; if (color_) |_| - btn.plane.set_style(.{ .fg = fg }); - _ = btn.plane.putstr(" ") catch {}; + plane.set_style(.{ .fg = fg }); + _ = plane.putstr(" ") catch {}; }; - _ = btn.plane.putstr(btn.opts.label) catch {}; - _ = btn.plane.putstr(" ") catch {}; + _ = plane.putstr(label) catch {}; + _ = plane.putstr(" ") catch {}; self.close_pos = null; self.save_pos = null; - if (btn.hover) { + if (hover) { if (is_dirty) { if (self.tab_style.save_icon_fg) |color| - btn.plane.set_style(.{ .fg = color.from_theme(theme) }); - self.save_pos = btn.plane.cursor_x(); - _ = btn.plane.putstr(self.tabbar.tab_style.save_icon) catch {}; + plane.set_style(.{ .fg = color.from_theme(theme) }); + self.save_pos = plane.cursor_x(); + _ = plane.putstr(self.tabbar.tab_style.save_icon) catch {}; } else { - btn.plane.set_style(.{ .fg = self.tab_style.close_icon_fg.from_theme(theme) }); - self.close_pos = btn.plane.cursor_x(); - _ = btn.plane.putstr(self.tabbar.tab_style.close_icon) catch {}; + 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) { if (self.tab_style.dirty_indicator_fg) |color| - btn.plane.set_style(.{ .fg = color.from_theme(theme) }); - _ = btn.plane.putstr(self.tabbar.tab_style.dirty_indicator) catch {}; + 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| - btn.plane.set_style(.{ .fg = color.from_theme(theme) }); - _ = btn.plane.putstr(self.tabbar.tab_style.clean_indicator) catch {}; + plane.set_style(.{ .fg = color.from_theme(theme) }); + _ = plane.putstr(self.tabbar.tab_style.clean_indicator) catch {}; } - btn.plane.set_style(.{ .fg = fg }); - self.render_padding(&btn.plane, .right); + plane.set_style(.{ .fg = fg }); + self.render_padding(plane, .right); } fn render_padding(self: *@This(), plane: *Plane, side: enum { left, right }) void {