Add optional bigclock digit outline (closes #1030) (#1031)

## What are the changes about?

Add optional `bigclock_outline_fg`. When set, each big-clock digit gets a 1-cell outline drawn under the glyph so it stays readable over busy animations. Default is `null` (previous behavior).

When the outline is enabled, the gap under the clock is slightly larger so the stroke is not glued to the login box.

## What existing issue(s) does this resolve?

Fixes #1030

## Pre-requisites

- [x] I have tested & confirmed the changes work locally
- [x] I have read and fully adhere to the rules set in the contributing guidelines found in `CONTRIBUTING.md`

### Testing

- `zig build -Doptimize=ReleaseSafe` (zig 0.16.0)
- Greeter with `bigclock = en` and `bigclock_outline_fg = 0x20000000` over a `dur_file` animation
- Default `null` leaves stock look unchanged

### AI usage (per contributing.md)

Assisted by an LLM (xAI Grok) for exploration and drafting. I directed the design (optional outline only, no panel/box, stock layout otherwise), reviewed the Zig, fixed an `i2` overflow in the outline loop, verified the build/local greeter, and wrote this PR text.

Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1031
Reviewed-by: AnErrupTion <anerruption+codeberg@disroot.org>
This commit is contained in:
jR4dh3y 2026-08-24 15:31:52 +02:00 committed by AnErrupTion
commit bd28c0e679
4 changed files with 73 additions and 10 deletions

View file

@ -19,6 +19,12 @@ pub const CHAR_SIZE = CHAR_WIDTH * CHAR_HEIGHT;
pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#';
pub const O: u32 = 0;
const OUTLINE_DIRS = [_][2]i8{
.{ -1, -1 }, .{ -1, 0 }, .{ -1, 1 },
.{ 0, -1 }, .{ 0, 1 }, .{ 1, -1 },
.{ 1, 0 }, .{ 1, 1 },
};
// zig fmt: off
pub const LocaleChars = struct {
ZERO: [CHAR_SIZE]u21,
@ -51,6 +57,7 @@ text: []const u8,
max_width: ?usize,
fg: u32,
bg: u32,
outline_fg: ?u32 = null,
locale: BigLabelLocale,
update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void,
calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize,
@ -63,6 +70,7 @@ pub fn init(
max_width: ?usize,
fg: u32,
bg: u32,
outline_fg: ?u32,
locale: BigLabelLocale,
update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void,
calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize,
@ -75,6 +83,7 @@ pub fn init(
.max_width = max_width,
.fg = fg,
.bg = bg,
.outline_fg = outline_fg,
.locale = locale,
.update_fn = update_fn,
.calculate_timeout_fn = calculate_timeout_fn,
@ -152,23 +161,71 @@ pub fn childrenPosition(self: BigLabel) Position {
fn draw(self: *BigLabel) void {
for (self.text, 0..) |c, i| {
const clock_cell = clockCell(
c,
self.fg,
self.bg,
self.locale,
);
const x = self.component_pos.x + i * (CHAR_WIDTH + 1);
const y = self.component_pos.y;
if (self.outline_fg) |outline_fg| {
drawDigitOutline(self, c, x, y, outline_fg);
}
alphaBlit(
self.component_pos.x + i * (CHAR_WIDTH + 1),
self.component_pos.y,
x,
y,
self.buffer.width,
self.buffer.height,
clock_cell,
clockCell(c, self.fg, self.bg, self.locale),
);
}
}
fn drawDigitOutline(
self: *BigLabel,
char: u8,
base_x: usize,
base_y: usize,
outline_fg: u32,
) void {
const pattern = toBigNumber(char, self.locale);
const stroke = Cell.init(X, outline_fg, TerminalBuffer.Color.DEFAULT);
for (0..CHAR_HEIGHT) |y| {
for (0..CHAR_WIDTH) |x| {
if (pattern[y * CHAR_WIDTH + x] == O) continue;
for (OUTLINE_DIRS) |dir| {
if (offsetBy(x, dir[0])) |nx| {
if (offsetBy(y, dir[1])) |ny| {
if (nx < CHAR_WIDTH and ny < CHAR_HEIGHT and
pattern[ny * CHAR_WIDTH + nx] != O)
{
continue;
}
}
}
const sx = offsetBy(base_x + x, dir[0]) orelse continue;
const sy = offsetBy(base_y + y, dir[1]) orelse continue;
if (sx >= self.buffer.width or
sy >= self.buffer.height)
{
continue;
}
stroke.put(sx, sy) catch {};
}
}
}
}
fn offsetBy(pos: usize, delta: i8) ?usize {
if (delta < 0) {
const abs: usize = @intCast(-delta);
if (pos < abs) return null;
return pos - abs;
}
return pos + @as(usize, @intCast(delta));
}
fn update(self: *BigLabel, context: *anyopaque) !void {
if (self.update_fn) |update_fn| {
return @call(