Completely refactor widget placement code

Signed-off-by: AnErrupTion <anerruption@disroot.org>
This commit is contained in:
AnErrupTion
2026-02-08 14:53:36 +01:00
parent 8c08359e51
commit bca38856b1
9 changed files with 477 additions and 209 deletions

View File

@@ -30,6 +30,7 @@ const DisplayServer = enums.DisplayServer;
const Environment = @import("Environment.zig");
const Entry = Environment.Entry;
const Animation = @import("tui/Animation.zig");
const CenteredBox = @import("tui/components/CenteredBox.zig");
const InfoLine = @import("tui/components/InfoLine.zig");
const Session = @import("tui/components/Session.zig");
const Text = @import("tui/components/Text.zig");
@@ -63,9 +64,11 @@ const UiState = struct {
auth_fails: u64,
update: bool,
buffer: *TerminalBuffer,
labels_max_length: usize,
animation_timed_out: bool,
animation: *?Animation,
can_draw_battery: bool,
box: *CenteredBox,
info_line: *InfoLine,
animate: bool,
resolution_changed: bool,
@@ -300,11 +303,7 @@ pub fn main() !void {
.fg = config.fg,
.bg = config.bg,
.border_fg = config.border_fg,
.margin_box_h = config.margin_box_h,
.margin_box_v = config.margin_box_v,
.input_len = config.input_len,
.full_color = config.full_color,
.labels_max_length = labels_max_length,
.is_tty = true,
};
var buffer = try TerminalBuffer.init(buffer_options, &log_file, random);
@@ -321,7 +320,23 @@ pub fn main() !void {
std.posix.sigaction(std.posix.SIG.TERM, &act, null);
// Initialize components
var info_line = InfoLine.init(allocator, &buffer);
var box = CenteredBox.init(
&buffer,
config.margin_box_h,
config.margin_box_v,
(2 * config.margin_box_h) + config.input_len + 1 + labels_max_length,
7 + (2 * config.margin_box_v),
!config.hide_borders,
config.blank_box,
config.box_title,
null,
);
var info_line = InfoLine.init(
allocator,
&buffer,
box.width - 2 * box.horizontal_margin,
);
defer info_line.deinit();
if (maybe_res == null) {
@@ -365,10 +380,24 @@ pub fn main() !void {
var login: UserList = undefined;
var session = Session.init(allocator, &buffer, &login);
var session = Session.init(
allocator,
&buffer,
&login,
box.width - 2 * box.horizontal_margin - labels_max_length - 1,
config.text_in_center,
);
defer session.deinit();
login = try UserList.init(allocator, &buffer, usernames, &saved_users, &session);
login = try UserList.init(
allocator,
&buffer,
usernames,
&saved_users,
&session,
box.width - 2 * box.horizontal_margin - labels_max_length - 1,
config.text_in_center,
);
defer login.deinit();
addOtherEnvironment(&session, lang, .shell, null) catch |err| {
@@ -430,7 +459,13 @@ pub fn main() !void {
try log_file.err("sys", "no users found", .{});
}
var password = Text.init(allocator, &buffer, true, config.asterisk);
var password = Text.init(
allocator,
&buffer,
true,
config.asterisk,
box.width - 2 * box.horizontal_margin - labels_max_length - 1,
);
defer password.deinit();
var is_autologin = false;
@@ -467,9 +502,11 @@ pub fn main() !void {
.auth_fails = 0,
.update = true,
.buffer = &buffer,
.labels_max_length = labels_max_length,
.animation_timed_out = false,
.animation = &animation,
.can_draw_battery = true,
.box = &box,
.info_line = &info_line,
.animate = config.animation != .none,
.resolution_changed = false,
@@ -512,25 +549,21 @@ pub fn main() !void {
}
}
// Place components on the screen
{
buffer.drawBoxCenter(!config.hide_borders, config.blank_box);
// Position components
state.box.position(TerminalBuffer.START_POSITION);
state.info_line.label.positionY(state.box.childrenPosition());
state.session.label.positionY(state.info_line.label.childrenPosition().addY(1).addX(state.labels_max_length + 1));
state.login.label.positionY(state.session.label.childrenPosition().addY(1));
state.password.positionY(state.login.label.childrenPosition().addY(1));
const coordinates = buffer.calculateComponentCoordinates();
info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null);
session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center);
login.label.position(coordinates.x, coordinates.y + 4, coordinates.visible_length, config.text_in_center);
password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length);
switch (state.active_input) {
.info_line => info_line.label.handle(null, state.insert_mode),
.session => session.label.handle(null, state.insert_mode),
.login => login.label.handle(null, state.insert_mode),
.password => password.handle(null, state.insert_mode) catch |err| {
try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg);
try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)});
},
}
switch (state.active_input) {
.info_line => info_line.label.handle(null, state.insert_mode),
.session => session.label.handle(null, state.insert_mode),
.login => login.label.handle(null, state.insert_mode),
.password => password.handle(null, state.insert_mode) catch |err| {
try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg);
try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)});
},
}
// Initialize the animation, if any
@@ -782,7 +815,7 @@ pub fn main() !void {
if (!config.allow_empty_password and password.text.items.len == 0) {
// Let's not log this message for security reasons
try info_line.addMessage(lang.err_empty_password, config.error_bg, config.error_fg);
InfoLine.clearRendered(allocator, buffer) catch |err| {
info_line.clearRendered(allocator) catch |err| {
try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg);
try log_file.err("tui", "failed to clear info line: {s}", .{@errorName(err)});
};
@@ -792,7 +825,7 @@ pub fn main() !void {
}
try info_line.addMessage(lang.authenticating, config.bg, config.fg);
InfoLine.clearRendered(allocator, buffer) catch |err| {
info_line.clearRendered(allocator) catch |err| {
try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg);
try log_file.err("tui", "failed to clear info line: {s}", .{@errorName(err)});
};
@@ -1002,7 +1035,7 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool
state.can_draw_battery = true;
}
if (config.bigclock != .none and state.buffer.box_height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) {
if (config.bigclock != .none and state.box.height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) {
var format_buf: [16:0]u8 = undefined;
var clock_buf: [32:0]u8 = undefined;
// We need the slice/c-string returned by `bufPrintZ`.
@@ -1013,7 +1046,7 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool
if (config.bigclock_12hr) "%P" else "",
});
const xo = state.buffer.width / 2 - @min(state.buffer.width, (format.len * (bigclock.WIDTH + 1))) / 2;
const yo = (state.buffer.height - state.buffer.box_height) / 2 - bigclock.HEIGHT - 2;
const yo = (state.buffer.height - state.box.height) / 2 - bigclock.HEIGHT - 2;
const clock_str = interop.timeAsString(&clock_buf, format);
@@ -1024,14 +1057,14 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool
}
}
state.buffer.drawBoxCenter(!config.hide_borders, config.blank_box);
state.box.draw();
if (state.resolution_changed) {
const coordinates = state.buffer.calculateComponentCoordinates();
state.info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null);
state.session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center);
state.login.label.position(coordinates.x, coordinates.y + 4, coordinates.visible_length, config.text_in_center);
state.password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length);
state.box.position(TerminalBuffer.START_POSITION);
state.info_line.label.positionY(state.box.childrenPosition());
state.session.label.positionY(state.info_line.label.childrenPosition().addY(1).addX(state.labels_max_length + 1));
state.login.label.positionY(state.session.label.childrenPosition().addY(1));
state.password.positionY(state.login.label.childrenPosition().addY(1));
state.resolution_changed = false;
}
@@ -1062,11 +1095,22 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool
state.buffer.drawLabel(clock_str, state.buffer.width - @min(state.buffer.width, clock_str.len) - config.edge_margin, config.edge_margin);
}
const label_x = state.buffer.box_x + state.buffer.margin_box_h;
const label_y = state.buffer.box_y + state.buffer.margin_box_v;
state.buffer.drawLabel(lang.login, label_x, label_y + 4);
state.buffer.drawLabel(lang.password, label_x, label_y + 6);
const env = state.session.label.list.items[state.session.label.current];
state.buffer.drawLabel(
env.environment.specifier,
state.box.childrenPosition().x,
state.session.label.component_pos.y,
);
state.buffer.drawLabel(
lang.login,
state.box.childrenPosition().x,
state.login.label.component_pos.y,
);
state.buffer.drawLabel(
lang.password,
state.box.childrenPosition().x,
state.password.component_pos.y,
);
state.info_line.label.draw();
@@ -1122,13 +1166,8 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool
}
}
if (config.box_title) |title| {
state.buffer.drawConfinedLabel(title, state.buffer.box_x, state.buffer.box_y - 1, state.buffer.box_width);
}
if (config.vi_mode) {
const label_txt = if (state.insert_mode) lang.insert else lang.normal;
state.buffer.drawLabel(label_txt, state.buffer.box_x, state.buffer.box_y + state.buffer.box_height);
state.box.bottom_title = if (state.insert_mode) lang.insert else lang.normal;
}
if (!config.hide_keyboard_locks and state.can_get_lock_state) draw_lock_state: {

32
src/tui/Position.zig Normal file
View File

@@ -0,0 +1,32 @@
const Position = @This();
x: usize,
y: usize,
pub fn init(x: usize, y: usize) Position {
return .{
.x = x,
.y = y,
};
}
pub fn add(self: Position, other: Position) Position {
return .{
.x = self.x + other.x,
.y = self.y + other.y,
};
}
pub fn addX(self: Position, x: usize) Position {
return .{
.x = self.x + x,
.y = self.y,
};
}
pub fn addY(self: Position, y: usize) Position {
return .{
.x = self.x,
.y = self.y + y,
};
}

View File

@@ -7,6 +7,7 @@ const LogFile = ly_core.LogFile;
pub const termbox = @import("termbox2");
const Cell = @import("Cell.zig");
const Position = @import("Position.zig");
const TerminalBuffer = @This();
@@ -14,11 +15,7 @@ pub const InitOptions = struct {
fg: u32,
bg: u32,
border_fg: u32,
margin_box_h: u8,
margin_box_v: u8,
input_len: u8,
full_color: bool,
labels_max_length: usize,
is_tty: bool,
};
@@ -60,6 +57,8 @@ pub const Color = struct {
pub const ECOL_WHITE = 8;
};
pub const START_POSITION = Position.init(0, 0);
random: Random,
width: usize,
height: usize,
@@ -76,13 +75,6 @@ box_chars: struct {
left: u32,
right: u32,
},
labels_max_length: usize,
box_x: usize,
box_y: usize,
box_width: usize,
box_height: usize,
margin_box_v: u8,
margin_box_h: u8,
blank_cell: Cell,
full_color: bool,
termios: ?std.posix.termios,
@@ -134,13 +126,6 @@ pub fn init(options: InitOptions, log_file: *LogFile, random: Random) !TerminalB
.left = '|',
.right = '|',
},
.labels_max_length = options.labels_max_length,
.box_x = 0,
.box_y = 0,
.box_width = (2 * options.margin_box_h) + options.input_len + 1 + options.labels_max_length,
.box_height = 7 + (2 * options.margin_box_v),
.margin_box_v = options.margin_box_v,
.margin_box_h = options.margin_box_h,
.blank_cell = Cell.init(' ', options.fg, options.bg),
.full_color = options.full_color,
// Needed to reclaim the TTY after giving up its control
@@ -216,70 +201,6 @@ pub fn cascade(self: TerminalBuffer) bool {
return changed;
}
pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) void {
if (self.width < 2 or self.height < 2) return;
const x1 = (self.width - @min(self.width - 2, self.box_width)) / 2;
const y1 = (self.height - @min(self.height - 2, self.box_height)) / 2;
const x2 = (self.width + @min(self.width, self.box_width)) / 2;
const y2 = (self.height + @min(self.height, self.box_height)) / 2;
self.box_x = x1;
self.box_y = y1;
if (show_borders) {
_ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y1 - 1), self.box_chars.left_up, self.border_fg, self.bg);
_ = termbox.tb_set_cell(@intCast(x2), @intCast(y1 - 1), self.box_chars.right_up, self.border_fg, self.bg);
_ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y2), self.box_chars.left_down, self.border_fg, self.bg);
_ = termbox.tb_set_cell(@intCast(x2), @intCast(y2), self.box_chars.right_down, self.border_fg, self.bg);
var c1 = Cell.init(self.box_chars.top, self.border_fg, self.bg);
var c2 = Cell.init(self.box_chars.bottom, self.border_fg, self.bg);
for (0..self.box_width) |i| {
c1.put(x1 + i, y1 - 1);
c2.put(x1 + i, y2);
}
c1.ch = self.box_chars.left;
c2.ch = self.box_chars.right;
for (0..self.box_height) |i| {
c1.put(x1 - 1, y1 + i);
c2.put(x2, y1 + i);
}
}
if (blank_box) {
for (0..self.box_height) |y| {
for (0..self.box_width) |x| {
self.blank_cell.put(x1 + x, y1 + y);
}
}
}
}
pub fn calculateComponentCoordinates(self: TerminalBuffer) struct {
start_x: usize,
x: usize,
y: usize,
full_visible_length: usize,
visible_length: usize,
} {
const start_x = self.box_x + self.margin_box_h;
const x = start_x + self.labels_max_length + 1;
const y = self.box_y + self.margin_box_v;
const full_visible_length = self.box_x + self.box_width - self.margin_box_h - start_x;
const visible_length = self.box_x + self.box_width - self.margin_box_h - x;
return .{
.start_x = start_x,
.x = x,
.y = y,
.full_visible_length = full_visible_length,
.visible_length = visible_length,
};
}
pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize) void {
drawColorLabel(text, x, y, self.fg, self.bg);
}

View File

@@ -0,0 +1,147 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Cell = @import("../Cell.zig");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const Position = @import("../Position.zig");
const termbox = TerminalBuffer.termbox;
const CenteredBox = @This();
buffer: *TerminalBuffer,
horizontal_margin: usize,
vertical_margin: usize,
width: usize,
height: usize,
show_borders: bool,
blank_box: bool,
top_title: ?[]const u8,
bottom_title: ?[]const u8,
left_pos: Position,
right_pos: Position,
children_pos: Position,
pub fn init(
buffer: *TerminalBuffer,
horizontal_margin: usize,
vertical_margin: usize,
width: usize,
height: usize,
show_borders: bool,
blank_box: bool,
top_title: ?[]const u8,
bottom_title: ?[]const u8,
) CenteredBox {
return .{
.buffer = buffer,
.horizontal_margin = horizontal_margin,
.vertical_margin = vertical_margin,
.width = width,
.height = height,
.show_borders = show_borders,
.blank_box = blank_box,
.top_title = top_title,
.bottom_title = bottom_title,
.left_pos = TerminalBuffer.START_POSITION,
.right_pos = TerminalBuffer.START_POSITION,
.children_pos = TerminalBuffer.START_POSITION,
};
}
pub fn position(self: *CenteredBox, original_pos: Position) void {
if (self.buffer.width < 2 or self.buffer.height < 2) return;
self.left_pos = Position.init(
(self.buffer.width - @min(self.buffer.width - 2, self.width)) / 2,
(self.buffer.height - @min(self.buffer.height - 2, self.height)) / 2,
).add(original_pos);
self.right_pos = Position.init(
(self.buffer.width + @min(self.buffer.width, self.width)) / 2,
(self.buffer.height + @min(self.buffer.height, self.height)) / 2,
).add(original_pos);
self.children_pos = Position.init(
self.left_pos.x + self.horizontal_margin,
self.left_pos.y + self.vertical_margin,
).add(original_pos);
}
pub fn childrenPosition(self: CenteredBox) Position {
return self.children_pos;
}
pub fn draw(self: CenteredBox) void {
if (self.show_borders) {
_ = termbox.tb_set_cell(
@intCast(self.left_pos.x - 1),
@intCast(self.left_pos.y - 1),
self.buffer.box_chars.left_up,
self.buffer.border_fg,
self.buffer.bg,
);
_ = termbox.tb_set_cell(
@intCast(self.right_pos.x),
@intCast(self.left_pos.y - 1),
self.buffer.box_chars.right_up,
self.buffer.border_fg,
self.buffer.bg,
);
_ = termbox.tb_set_cell(
@intCast(self.left_pos.x - 1),
@intCast(self.right_pos.y),
self.buffer.box_chars.left_down,
self.buffer.border_fg,
self.buffer.bg,
);
_ = termbox.tb_set_cell(
@intCast(self.right_pos.x),
@intCast(self.right_pos.y),
self.buffer.box_chars.right_down,
self.buffer.border_fg,
self.buffer.bg,
);
var c1 = Cell.init(self.buffer.box_chars.top, self.buffer.border_fg, self.buffer.bg);
var c2 = Cell.init(self.buffer.box_chars.bottom, self.buffer.border_fg, self.buffer.bg);
for (0..self.width) |i| {
c1.put(self.left_pos.x + i, self.left_pos.y - 1);
c2.put(self.left_pos.x + i, self.right_pos.y);
}
c1.ch = self.buffer.box_chars.left;
c2.ch = self.buffer.box_chars.right;
for (0..self.height) |i| {
c1.put(self.left_pos.x - 1, self.left_pos.y + i);
c2.put(self.right_pos.x, self.left_pos.y + i);
}
}
if (self.blank_box) {
for (0..self.height) |y| {
for (0..self.width) |x| {
self.buffer.blank_cell.put(self.left_pos.x + x, self.left_pos.y + y);
}
}
}
if (self.top_title) |title| {
self.buffer.drawConfinedLabel(
title,
self.left_pos.x,
self.left_pos.y - 1,
self.width,
);
}
if (self.bottom_title) |title| {
self.buffer.drawConfinedLabel(
title,
self.left_pos.x,
self.left_pos.y + self.height,
self.width,
);
}
}

View File

@@ -17,9 +17,21 @@ const Message = struct {
label: MessageLabel,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer) InfoLine {
pub fn init(
allocator: Allocator,
buffer: *TerminalBuffer,
width: usize,
) InfoLine {
return .{
.label = MessageLabel.init(allocator, buffer, drawItem, null, null),
.label = MessageLabel.init(
allocator,
buffer,
drawItem,
null,
null,
width,
true,
),
};
}
@@ -38,23 +50,31 @@ pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void {
});
}
pub fn clearRendered(allocator: Allocator, buffer: TerminalBuffer) !void {
pub fn clearRendered(self: InfoLine, allocator: Allocator) !void {
// Draw over the area
const y = buffer.box_y + buffer.margin_box_v;
const spaces = try allocator.alloc(u8, buffer.box_width);
const spaces = try allocator.alloc(u8, self.label.width - 2);
defer allocator.free(spaces);
@memset(spaces, ' ');
buffer.drawLabel(spaces, buffer.box_x, y);
self.label.buffer.drawLabel(
spaces,
self.label.component_pos.x + 2,
self.label.component_pos.y,
);
}
fn drawItem(label: *MessageLabel, message: Message, _: usize, _: usize) bool {
if (message.width == 0 or label.buffer.box_width <= message.width) return false;
fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void {
if (message.width == 0 or width <= message.width) return;
const x = label.buffer.box_x + ((label.buffer.box_width - message.width) / 2);
label.first_char_x = x + message.width;
const x_offset = if (label.text_in_center) (width - message.width) / 2 else 0;
TerminalBuffer.drawColorLabel(message.text, x, label.y, message.fg, message.bg);
return true;
label.item_width = message.width + x_offset;
TerminalBuffer.drawColorLabel(
message.text,
x + x_offset,
y,
message.fg,
message.bg,
);
}

View File

@@ -19,9 +19,23 @@ const Session = @This();
label: EnvironmentLabel,
user_list: *UserList,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, user_list: *UserList) Session {
pub fn init(
allocator: Allocator,
buffer: *TerminalBuffer,
user_list: *UserList,
width: usize,
text_in_center: bool,
) Session {
return .{
.label = EnvironmentLabel.init(allocator, buffer, drawItem, sessionChanged, user_list),
.label = EnvironmentLabel.init(
allocator,
buffer,
drawItem,
sessionChanged,
user_list,
width,
text_in_center,
),
.user_list = user_list,
};
}
@@ -55,14 +69,12 @@ fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void {
}
}
fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize) bool {
const length = @min(env.environment.name.len, label.visible_length - 3);
if (length == 0) return false;
fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize) void {
const length = @min(env.environment.name.len, width - 3);
if (length == 0) return;
const nx = if (label.text_in_center) (label.x + (label.visible_length - env.environment.name.len) / 2) else (label.x + 2);
label.first_char_x = nx + env.environment.name.len;
const x_offset = if (label.text_in_center) (width - length) / 2 else 0;
label.buffer.drawLabel(env.environment.specifier, x, y);
label.buffer.drawLabel(env.environment.name, nx, label.y);
return true;
label.item_width = length + x_offset;
label.buffer.drawLabel(env.environment.name, x + x_offset, y);
}

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const TerminalBuffer = @import("../TerminalBuffer.zig");
const Position = @import("../Position.zig");
const termbox = TerminalBuffer.termbox;
const DynamicString = std.ArrayListUnmanaged(u8);
@@ -14,13 +15,19 @@ text: DynamicString,
end: usize,
cursor: usize,
visible_start: usize,
visible_length: usize,
x: usize,
y: usize,
width: usize,
component_pos: Position,
children_pos: Position,
masked: bool,
maybe_mask: ?u32,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u32) Text {
pub fn init(
allocator: Allocator,
buffer: *TerminalBuffer,
masked: bool,
maybe_mask: ?u32,
width: usize,
) Text {
return .{
.allocator = allocator,
.buffer = buffer,
@@ -28,9 +35,9 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_m
.end = 0,
.cursor = 0,
.visible_start = 0,
.visible_length = 0,
.x = 0,
.y = 0,
.width = width,
.component_pos = TerminalBuffer.START_POSITION,
.children_pos = TerminalBuffer.START_POSITION,
.masked = masked,
.maybe_mask = maybe_mask,
};
@@ -40,10 +47,26 @@ pub fn deinit(self: *Text) void {
self.text.deinit(self.allocator);
}
pub fn position(self: *Text, x: usize, y: usize, visible_length: usize) void {
self.x = x;
self.y = y;
self.visible_length = visible_length;
pub fn positionX(self: *Text, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = original_pos.addX(self.width);
}
pub fn positionY(self: *Text, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = original_pos.addY(1);
}
pub fn positionXY(self: *Text, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = Position.init(
self.width,
1,
).add(original_pos);
}
pub fn childrenPosition(self: Text) Position {
return self.children_pos;
}
pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) !void {
@@ -79,36 +102,44 @@ pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) !
}
if (self.masked and self.maybe_mask == null) {
_ = termbox.tb_set_cursor(@intCast(self.x), @intCast(self.y));
_ = termbox.tb_set_cursor(@intCast(self.component_pos.x), @intCast(self.component_pos.y));
return;
}
_ = termbox.tb_set_cursor(@intCast(self.x + (self.cursor - self.visible_start)), @intCast(self.y));
_ = termbox.tb_set_cursor(
@intCast(self.component_pos.x + (self.cursor - self.visible_start)),
@intCast(self.component_pos.y),
);
}
pub fn draw(self: Text) void {
if (self.masked) {
if (self.maybe_mask) |mask| {
const length = @min(self.text.items.len, self.visible_length - 1);
const length = @min(self.text.items.len, self.width - 1);
if (length == 0) return;
self.buffer.drawCharMultiple(mask, self.x, self.y, length);
self.buffer.drawCharMultiple(
mask,
self.component_pos.x,
self.component_pos.y,
length,
);
}
return;
}
const length = @min(self.text.items.len, self.visible_length);
const length = @min(self.text.items.len, self.width);
if (length == 0) return;
const visible_slice = vs: {
if (self.text.items.len > self.visible_length and self.cursor < self.text.items.len) {
break :vs self.text.items[self.visible_start..(self.visible_length + self.visible_start)];
if (self.text.items.len > self.width and self.cursor < self.text.items.len) {
break :vs self.text.items[self.visible_start..(self.width + self.visible_start)];
} else {
break :vs self.text.items[self.visible_start..];
}
};
self.buffer.drawLabel(visible_slice, self.x, self.y);
self.buffer.drawLabel(visible_slice, self.component_pos.x, self.component_pos.y);
}
pub fn clear(self: *Text) void {
@@ -127,7 +158,7 @@ fn goLeft(self: *Text) void {
fn goRight(self: *Text) void {
if (self.cursor >= self.end) return;
if (self.cursor - self.visible_start == self.visible_length - 1) self.visible_start += 1;
if (self.cursor - self.visible_start == self.width - 1) self.visible_start += 1;
self.cursor += 1;
}

View File

@@ -19,9 +19,25 @@ const UserList = @This();
label: UserLabel,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList, saved_users: *SavedUsers, session: *Session) !UserList {
pub fn init(
allocator: Allocator,
buffer: *TerminalBuffer,
usernames: StringList,
saved_users: *SavedUsers,
session: *Session,
width: usize,
text_in_center: bool,
) !UserList {
var userList = UserList{
.label = UserLabel.init(allocator, buffer, drawItem, usernameChanged, session),
.label = UserLabel.init(
allocator,
buffer,
drawItem,
usernameChanged,
session,
width,
text_in_center,
),
};
for (usernames.items) |username| {
@@ -75,13 +91,12 @@ fn usernameChanged(user: User, maybe_session: ?*Session) void {
}
}
fn drawItem(label: *UserLabel, user: User, _: usize, _: usize) bool {
const length = @min(user.name.len, label.visible_length - 3);
if (length == 0) return false;
fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) void {
const length = @min(user.name.len, width - 3);
if (length == 0) return;
const x = if (label.text_in_center) (label.x + (label.visible_length - user.name.len) / 2) else (label.x + 2);
label.first_char_x = x + user.name.len;
const x_offset = if (label.text_in_center) (width - length) / 2 else 0;
label.buffer.drawLabel(user.name, x, label.y);
return true;
label.item_width = length + x_offset;
label.buffer.drawLabel(user.name, x + x_offset, y);
}

View File

@@ -1,12 +1,13 @@
const std = @import("std");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const Position = @import("../Position.zig");
pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type {
return struct {
const Allocator = std.mem.Allocator;
const ItemList = std.ArrayListUnmanaged(ItemType);
const DrawItemFn = *const fn (*Self, ItemType, usize, usize) bool;
const DrawItemFn = *const fn (*Self, ItemType, usize, usize, usize) void;
const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void;
const termbox = TerminalBuffer.termbox;
@@ -17,26 +18,34 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ
buffer: *TerminalBuffer,
list: ItemList,
current: usize,
visible_length: usize,
x: usize,
y: usize,
first_char_x: usize,
width: usize,
component_pos: Position,
children_pos: Position,
text_in_center: bool,
item_width: usize,
draw_item_fn: DrawItemFn,
change_item_fn: ?ChangeItemFn,
change_item_arg: ?ChangeItemType,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType) Self {
pub fn init(
allocator: Allocator,
buffer: *TerminalBuffer,
draw_item_fn: DrawItemFn,
change_item_fn: ?ChangeItemFn,
change_item_arg: ?ChangeItemType,
width: usize,
text_in_center: bool,
) Self {
return .{
.allocator = allocator,
.buffer = buffer,
.list = .empty,
.current = 0,
.visible_length = 0,
.x = 0,
.y = 0,
.first_char_x = 0,
.text_in_center = false,
.width = width,
.component_pos = TerminalBuffer.START_POSITION,
.children_pos = TerminalBuffer.START_POSITION,
.text_in_center = text_in_center,
.item_width = 0,
.draw_item_fn = draw_item_fn,
.change_item_fn = change_item_fn,
.change_item_arg = change_item_arg,
@@ -47,14 +56,29 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ
self.list.deinit(self.allocator);
}
pub fn position(self: *Self, x: usize, y: usize, visible_length: usize, text_in_center: ?bool) void {
self.x = x;
self.y = y;
self.visible_length = visible_length;
self.first_char_x = x + 2;
if (text_in_center) |value| {
self.text_in_center = value;
}
pub fn positionX(self: *Self, original_pos: Position) void {
self.component_pos = original_pos;
self.item_width = self.component_pos.x + 2;
self.children_pos = original_pos.addX(self.width);
}
pub fn positionY(self: *Self, original_pos: Position) void {
self.component_pos = original_pos;
self.item_width = self.component_pos.x + 2;
self.children_pos = original_pos.addY(1);
}
pub fn positionXY(self: *Self, original_pos: Position) void {
self.component_pos = original_pos;
self.item_width = self.component_pos.x + 2;
self.children_pos = Position.init(
self.width,
1,
).add(original_pos);
}
pub fn childrenPosition(self: Self) Position {
return self.children_pos;
}
pub fn addItem(self: *Self, item: ItemType) !void {
@@ -81,28 +105,51 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ
}
}
_ = termbox.tb_set_cursor(@intCast(self.first_char_x), @intCast(self.y));
_ = termbox.tb_set_cursor(
@intCast(self.component_pos.x + self.item_width + 2),
@intCast(self.component_pos.y),
);
}
pub fn draw(self: *Self) void {
if (self.list.items.len == 0) return;
_ = termbox.tb_set_cell(
@intCast(self.component_pos.x),
@intCast(self.component_pos.y),
'<',
self.buffer.fg,
self.buffer.bg,
);
_ = termbox.tb_set_cell(
@intCast(self.component_pos.x + self.width - 1),
@intCast(self.component_pos.y),
'>',
self.buffer.fg,
self.buffer.bg,
);
const current_item = self.list.items[self.current];
const x = self.buffer.box_x + self.buffer.margin_box_h;
const y = self.buffer.box_y + self.buffer.margin_box_v + 2;
const x = self.component_pos.x + 2;
const y = self.component_pos.y;
const width = self.width - 2;
const continue_drawing = @call(.auto, self.draw_item_fn, .{ self, current_item, x, y });
if (!continue_drawing) return;
_ = termbox.tb_set_cell(@intCast(self.x), @intCast(self.y), '<', self.buffer.fg, self.buffer.bg);
_ = termbox.tb_set_cell(@intCast(self.x + self.visible_length - 1), @intCast(self.y), '>', self.buffer.fg, self.buffer.bg);
@call(
.auto,
self.draw_item_fn,
.{ self, current_item, x, y, width },
);
}
fn goLeft(self: *Self) void {
self.current = if (self.current == 0) self.list.items.len - 1 else self.current - 1;
if (self.change_item_fn) |change_item_fn| {
@call(.auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg });
@call(
.auto,
change_item_fn,
.{ self.list.items[self.current], self.change_item_arg },
);
}
}
@@ -110,7 +157,11 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ
self.current = if (self.current == self.list.items.len - 1) 0 else self.current + 1;
if (self.change_item_fn) |change_item_fn| {
@call(.auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg });
@call(
.auto,
change_item_fn,
.{ self.list.items[self.current], self.change_item_arg },
);
}
}
};