diff --git a/src/tui/Menu.zig b/src/tui/Menu.zig index 284dc03..813051c 100644 --- a/src/tui/Menu.zig +++ b/src/tui/Menu.zig @@ -20,7 +20,8 @@ pub fn Options(context: type) type { on_click5: *const fn (menu: **State(Context), button: *Button.State(*State(Context))) void = do_nothing_click, on_render: *const fn (ctx: context, button: *Button.State(*State(Context)), theme: *const Widget.Theme, selected: bool) bool = on_render_default, on_layout: *const fn (ctx: context, button: *Button.State(*State(Context))) Widget.Layout = on_layout_default, - on_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) void = on_resize_default, + prepare_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) Widget.Box = prepare_resize_default, + after_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) void = after_resize_default, on_scroll: ?EventHandler = null, pub const Context = context; @@ -46,23 +47,26 @@ pub fn Options(context: type) type { return .{ .static = 1 }; } - pub fn on_resize_default(_: context, state: *State(Context), box_: Widget.Box) void { + pub fn prepare_resize_default(_: context, state: *State(Context), box_: Widget.Box) Widget.Box { var box = box_; box.h = if (box_.h == 0) state.menu.widgets.items.len else box_.h; - state.container.resize(box); + return box; } + + pub fn after_resize_default(_: context, _: *State(Context), _: Widget.Box) void {} }; } pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: Options(ctx_type)) !*State(ctx_type) { const self = try allocator.create(State(ctx_type)); errdefer allocator.destroy(self); - const container = try WidgetList.createH(allocator, parent, @typeName(@This()), .dynamic); + const container = try WidgetList.createHStyled(allocator, parent, @typeName(@This()), .dynamic, Widget.Style.boxed); self.* = .{ .allocator = allocator, .menu = try WidgetList.createV(allocator, container.plane, @typeName(@This()), .dynamic), .container = container, .container_widget = container.widget(), + .frame_widget = null, .scrollbar = if (tui.config().show_scrollbars) if (opts.on_scroll) |on_scroll| (try scrollbar_v.create(allocator, parent, null, on_scroll)).dynamic_cast(scrollbar_v).? else null else @@ -72,7 +76,8 @@ pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: self.menu.ctx = self; self.menu.on_render = State(ctx_type).on_render_menu; container.ctx = self; - container.on_resize = State(ctx_type).on_resize_container; + container.prepare_resize = State(ctx_type).prepare_resize; + container.after_resize = State(ctx_type).after_resize; try container.add(self.menu.widget()); if (self.scrollbar) |sb| try container.add(sb.widget()); return self; @@ -84,6 +89,7 @@ pub fn State(ctx_type: type) type { menu: *WidgetList, container: *WidgetList, container_widget: Widget, + frame_widget: ?Widget, scrollbar: ?*scrollbar_v, opts: options_type, selected: ?usize = null, @@ -146,9 +152,14 @@ pub fn State(ctx_type: type) type { self.render_idx = 0; } - fn on_resize_container(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) void { + fn prepare_resize(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) Widget.Box { const self: *Self = @ptrCast(@alignCast(ctx)); - self.opts.on_resize(self.*.opts.ctx, self, box); + return self.opts.prepare_resize(self.*.opts.ctx, self, box); + } + + fn after_resize(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + self.opts.after_resize(self.*.opts.ctx, self, box); } pub fn on_layout(self: **Self, button: *Button.State(*Self)) Widget.Layout { @@ -170,7 +181,7 @@ pub fn State(ctx_type: type) type { } pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool { - return self.menu.walk(walk_ctx, f, &self.container_widget); + return self.menu.walk(walk_ctx, f, if (self.frame_widget) |*frame| frame else &self.container_widget); } pub fn count(self: *Self) usize { diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 0f0c96f..7de2347 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -38,6 +38,52 @@ pub const Layout = union(enum) { } }; +pub const Style = struct { + padding: Margin = margins.@"0", + inner_padding: Margin = margins.@"0", + border: Border = borders.blank, + + pub const PaddingUnit = u16; + + pub const Margin = struct { + top: PaddingUnit, + bottom: PaddingUnit, + left: PaddingUnit, + right: PaddingUnit, + }; + + pub const Border = struct { + nw: []const u8, + n: []const u8, + ne: []const u8, + e: []const u8, + se: []const u8, + s: []const u8, + sw: []const u8, + w: []const u8, + }; + + pub const margins = struct { + const @"0": Margin = .{ .top = 0, .bottom = 0, .left = 0, .right = 0 }; + const @"1": Margin = .{ .top = 1, .bottom = 1, .left = 1, .right = 1 }; + const @"2": Margin = .{ .top = 2, .bottom = 2, .left = 2, .right = 2 }; + }; + + pub const borders = struct { + const blank: Border = .{ .nw = " ", .n = " ", .ne = " ", .e = " ", .se = " ", .s = " ", .sw = " ", .w = " " }; + const box: Border = .{ .nw = "┌", .n = "─", .ne = "┐", .e = "│", .se = "┘", .s = "─", .sw = "└", .w = "│" }; + }; + + pub const default_static: @This() = .{}; + pub const default = &default_static; + + pub const boxed_static: @This() = .{ + .padding = margins.@"1", + .border = borders.box, + }; + pub const boxed = &boxed_static; +}; + pub const VTable = struct { deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void, send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool, diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index c40ff2c..4acfc3b 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -26,25 +26,35 @@ widgets: ArrayList(WidgetState), layout_: Layout, layout_empty: bool = true, direction: Direction, -box: ?Widget.Box = null, +deco_box: Widget.Box, ctx: ?*anyopaque = null, on_render: *const fn (ctx: ?*anyopaque, theme: *const Widget.Theme) void = on_render_default, after_render: *const fn (ctx: ?*anyopaque, theme: *const Widget.Theme) void = on_render_default, -on_resize: *const fn (ctx: ?*anyopaque, self: *Self, pos_: Widget.Box) void = on_resize_default, +prepare_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) Widget.Box = prepare_resize_default, +after_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) void = after_resize_default, on_layout: *const fn (ctx: ?*anyopaque, self: *Self) Widget.Layout = on_layout_default, +style: *const Widget.Style, pub fn createH(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout) error{OutOfMemory}!*Self { + return createHStyled(allocator, parent, name, layout_, Widget.Style.default); +} + +pub fn createHStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: *const Widget.Style) error{OutOfMemory}!*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); - self.* = try init(allocator, parent, name, .horizontal, layout_, Box{}); + self.* = try init(allocator, parent, name, .horizontal, layout_, Box{}, style); self.plane.hide(); return self; } pub fn createV(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout) !*Self { + return createVStyled(allocator, parent, name, layout_, Widget.Style.default); +} + +pub fn createVStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: *const Widget.Style) !*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); - self.* = try init(allocator, parent, name, .vertical, layout_, Box{}); + self.* = try init(allocator, parent, name, .vertical, layout_, Box{}, style); self.plane.hide(); return self; } @@ -57,15 +67,22 @@ pub fn createBox(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: D return self; } -fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box: Box) !Self { - return .{ - .plane = try Plane.init(&box.opts(name), parent), +fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box_: Box, style: *const Widget.Style) !Self { + var self: Self = .{ + .plane = undefined, .parent = parent, .allocator = allocator, .widgets = ArrayList(WidgetState).init(allocator), .layout_ = layout_, .direction = dir, + .style = style, + .deco_box = undefined, }; + self.deco_box = self.from_client_box(box_); + self.plane = try Plane.init(&self.deco_box.opts(name), parent); + if (self.style.padding.top > 0 and self.deco_box.y < 10) + std.log.info("init deco box: {any}", .{self.deco_box}); + return self; } pub fn widget(self: *Self) Widget { @@ -153,6 +170,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { }; self.on_render(self.ctx, theme); + self.render_decoration(theme); var more = false; for (self.widgets.items) |*w| @@ -166,6 +184,40 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { fn on_render_default(_: ?*anyopaque, _: *const Widget.Theme) void {} +fn render_decoration(self: *Self, theme: *const Widget.Theme) void { + const style = theme.editor_gutter_modified; + const plane = &self.plane; + const box = self.deco_box; + const padding = self.style.padding; + const border = self.style.border; + + plane.set_style(style); + + if (padding.top > 0 and padding.left > 0) put_at_pos(plane, 0, 0, border.nw); + if (padding.top > 0 and padding.right > 0) put_at_pos(plane, 0, box.w - 1, border.ne); + if (padding.bottom > 0 and padding.left > 0 and box.h > 0) put_at_pos(plane, box.h - 1, 0, border.sw); + if (padding.bottom > 0 and padding.right > 0 and box.h > 0) put_at_pos(plane, box.h - 1, box.w - 1, border.se); + + { + const start: usize = if (padding.left > 0) 1 else 0; + const end: usize = if (padding.right > 0 and box.w > 0) box.w - 1 else box.w; + if (padding.top > 0) for (start..end) |x| put_at_pos(plane, 0, x, border.n); + if (padding.bottom > 0) for (start..end) |x| put_at_pos(plane, box.h - 1, x, border.s); + } + + { + const start: usize = if (padding.top > 0) 1 else 0; + const end: usize = if (padding.bottom > 0 and box.h > 0) box.h - 1 else box.h; + if (padding.left > 0) for (start..end) |y| put_at_pos(plane, y, 0, border.w); + if (padding.right > 0) for (start..end) |y| put_at_pos(plane, y, box.w - 1, border.e); + } +} + +inline fn put_at_pos(plane: *Plane, y: usize, x: usize, egc: []const u8) void { + plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return; + plane.putchar(egc); +} + pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { if (try m.match(.{ "H", tp.more })) return false; @@ -176,6 +228,13 @@ pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { return false; } +fn get_size_a_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.h, + .horizontal => pos.w, + }; +} + fn get_size_a(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.h, @@ -183,6 +242,13 @@ fn get_size_a(self: *Self, pos: *Widget.Box) *usize { }; } +fn get_size_b_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.w, + .horizontal => pos.h, + }; +} + fn get_size_b(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.w, @@ -190,6 +256,13 @@ fn get_size_b(self: *Self, pos: *Widget.Box) *usize { }; } +fn get_loc_a_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.y, + .horizontal => pos.x, + }; +} + fn get_loc_a(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.y, @@ -197,6 +270,13 @@ fn get_loc_a(self: *Self, pos: *Widget.Box) *usize { }; } +fn get_loc_b_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.x, + .horizontal => pos.y, + }; +} + fn get_loc_b(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.x, @@ -205,27 +285,66 @@ fn get_loc_b(self: *Self, pos: *Widget.Box) *usize { } fn refresh_layout(self: *Self) void { - return if (self.box) |box| self.handle_resize(box); + return self.handle_resize(self.to_client_box(self.deco_box)); } -pub fn handle_resize(self: *Self, pos: Widget.Box) void { - self.on_resize(self.ctx, self, pos); +pub fn handle_resize(self: *Self, box: Widget.Box) void { + if (self.style.padding.top > 0 and self.deco_box.y < 10) + std.log.info("handle_resize deco box: {any}", .{self.deco_box}); + const client_box_ = self.prepare_resize(self.ctx, self, self.to_client_box(box)); + self.deco_box = self.from_client_box(client_box_); + if (self.style.padding.top > 0 and self.deco_box.y < 10) + std.log.info("prepare_resize deco box: {any}", .{self.deco_box}); + self.do_resize(); + self.after_resize(self.ctx, self, self.to_client_box(self.deco_box)); } -fn on_resize_default(_: ?*anyopaque, self: *Self, pos: Widget.Box) void { - self.resize(pos); +pub inline fn to_client_box(self: *const Self, box_: Widget.Box) Widget.Box { + const padding = self.style.padding; + const total_y_padding = padding.top + padding.bottom; + const total_x_padding = padding.left + padding.right; + var box = box_; + box.y += padding.top; + box.h -= if (box.h > total_y_padding) total_y_padding else box.h; + box.x += padding.left; + box.w -= if (box.w > total_x_padding) total_x_padding else box.w; + return box; } +inline fn from_client_box(self: *const Self, box_: Widget.Box) Widget.Box { + const padding = self.style.padding; + const total_y_padding = padding.top + padding.bottom; + const total_x_padding = padding.left + padding.right; + const y = if (box_.y < padding.top) padding.top else box_.y; + const x = if (box_.x < padding.left) padding.top else box_.x; + var box = box_; + box.y = y - padding.top; + box.h += total_y_padding; + box.x = x - padding.left; + box.w += total_x_padding; + return box; +} + +fn prepare_resize_default(_: ?*anyopaque, _: *Self, box: Widget.Box) Widget.Box { + return box; +} + +fn after_resize_default(_: ?*anyopaque, _: *Self, _: Widget.Box) void {} + fn on_layout_default(_: ?*anyopaque, self: *Self) Widget.Layout { return self.layout_; } -pub fn resize(self: *Self, pos_: Widget.Box) void { - self.box = pos_; - var pos = pos_; - self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; - self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; - const total = self.get_size_a(&pos).*; +pub fn resize(self: *Self, box: Widget.Box) void { + return self.handle_resize(box); +} + +fn do_resize(self: *Self) void { + const client_box = self.to_client_box(self.deco_box); + const deco_box = self.deco_box; + self.plane.move_yx(@intCast(deco_box.y), @intCast(deco_box.x)) catch return; + self.plane.resize_simple(@intCast(deco_box.h), @intCast(deco_box.w)) catch return; + const total = self.get_size_a_const(&client_box); var avail = total; var statics: usize = 0; var dynamics: usize = 0; @@ -245,7 +364,7 @@ pub fn resize(self: *Self, pos_: Widget.Box) void { const dyn_size = avail / if (dynamics > 0) dynamics else 1; const rounded: usize = if (dyn_size * dynamics < avail) avail - dyn_size * dynamics else 0; - var cur_loc: usize = self.get_loc_a(&pos).*; + var cur_loc: usize = self.get_loc_a_const(&client_box); var first = true; for (self.widgets.items) |*w| { @@ -261,8 +380,8 @@ pub fn resize(self: *Self, pos_: Widget.Box) void { self.get_loc_a(&w_pos).* = cur_loc; cur_loc += size; - self.get_size_b(&w_pos).* = self.get_size_b(&pos).*; - self.get_loc_b(&w_pos).* = self.get_loc_b(&pos).*; + self.get_size_b(&w_pos).* = self.get_size_b_const(&client_box); + self.get_loc_b(&w_pos).* = self.get_loc_b_const(&client_box); w.widget.resize(w_pos); } } diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index d284a84..499c229 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -33,7 +33,7 @@ logger: log.Logger, query_pending: bool = false, need_reset: bool = false, need_select_first: bool = true, -longest: usize = 0, +longest: usize, commands: Commands = undefined, buffer_manager: ?*BufferManager, @@ -49,7 +49,7 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, .on_render = on_render_menu, - .on_resize = on_resize_menu, + .prepare_resize = prepare_resize_menu, }), .logger = log.logger(@typeName(Self)), .inputbox = (try self.menu.add_header(try InputBox.create(*Self, self.allocator, self.menu.menu.parent, .{ @@ -57,12 +57,13 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .label = inputbox_label, }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, .buffer_manager = tui.get_buffer_manager(), + .longest = inputbox_label.len, }; try self.commands.init(self); try tui.message_filters().add(MessageFilter.bind(self, receive_project_manager)); self.query_pending = true; try project_manager.request_recent_files(max_recent_files); - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = max_menu_width() + 2 }); + self.do_resize(); try mv.floating_views.add(self.modal.widget()); try mv.floating_views.add(self.menu.container_widget); var mode = try keybind.mode("overlay/palette", allocator, .{ @@ -85,7 +86,7 @@ pub fn deinit(self: *Self) void { } inline fn menu_width(self: *Self) usize { - return @max(@min(self.longest, max_menu_width()) + 2, inputbox_label.len + 2); + return @max(@min(self.longest, max_menu_width()) + 5, inputbox_label.len + 2); } inline fn menu_pos_x(self: *Self) usize { @@ -149,8 +150,19 @@ fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: return false; } -fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); +fn prepare_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) Widget.Box { + return self.prepare_resize(); +} + +fn prepare_resize(self: *Self) Widget.Box { + const w = self.menu_width(); + const x = self.menu_pos_x(); + const h = self.menu.menu.widgets.items.len; + return .{ .y = 0, .x = x, .w = w, .h = h }; +} + +fn do_resize(self: *Self) void { + self.menu.resize(self.prepare_resize()); } fn menu_action_open_file(menu: **Menu.State(*Self), button: *Button.State(*Menu.State(*Self))) void { @@ -207,7 +219,7 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void })) { if (self.need_reset) self.reset_results(); try self.add_item(file_name, file_type, file_icon, file_color, matches); - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); + self.do_resize(); if (self.need_select_first) { self.menu.select_down(); self.need_select_first = false; @@ -224,7 +236,7 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void })) { if (self.need_reset) self.reset_results(); try self.add_item(file_name, file_type, file_icon, file_color, null); - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); + self.do_resize(); if (self.need_select_first) { self.menu.select_down(); self.need_select_first = false; diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index 1e0e18a..d85b1c9 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -58,7 +58,8 @@ pub fn Create(options: type) type { .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, .on_render = if (@hasDecl(options, "on_render_menu")) options.on_render_menu else on_render_menu, - .on_resize = on_resize_menu, + .prepare_resize = prepare_resize_menu, + .after_resize = after_resize_menu, .on_scroll = EventHandler.bind(self, Self.on_scroll), .on_click4 = mouse_click_button4, .on_click5 = mouse_click_button5, @@ -146,19 +147,32 @@ pub fn Create(options: type) type { return false; } - fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { - self.do_resize(); - // self.start_query(0) catch {}; + fn prepare_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) Widget.Box { + return self.prepare_resize(); } - fn do_resize(self: *Self) void { + fn prepare_resize(self: *Self) Widget.Box { const screen = tui.screen(); const w = @max(@min(self.longest, max_menu_width) + 2 + 1 + self.longest_hint, options.label.len + 2); const x = if (screen.w > w) (screen.w - w) / 2 else 0; self.view_rows = get_view_rows(screen); const h = @min(self.items + self.menu.header_count, self.view_rows + self.menu.header_count); - self.menu.container.resize(.{ .y = 0, .x = x, .w = w, .h = h }); + return .{ .y = 0, .x = x, .w = w, .h = h }; + } + + fn after_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { + return self.after_resize(); + } + + fn after_resize(self: *Self) void { self.update_scrollbar(); + // self.start_query(0) catch {}; + } + + fn do_resize(self: *Self) void { + const box = self.prepare_resize(); + self.menu.resize(self.menu.container.to_client_box(box)); + self.after_resize(); } fn get_view_rows(screen: Widget.Box) usize {