feat: add menu border styles

This commit is contained in:
CJ van den Berg 2025-08-12 22:29:10 +02:00
parent ac2a7cfa83
commit 83a0adccc7
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
5 changed files with 245 additions and 43 deletions

View file

@ -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 {

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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 {