From 1ab4c071be58033d5452f69797ca8e2c32240cff Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 15 Sep 2026 16:28:57 -0300 Subject: Separate tag text geometry from physical pane grips --- src/tag_layer.zig | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/tag_layer.zig (limited to 'src/tag_layer.zig') diff --git a/src/tag_layer.zig b/src/tag_layer.zig new file mode 100644 index 00000000..65990959 --- /dev/null +++ b/src/tag_layer.zig @@ -0,0 +1,62 @@ +//! Tag text has its own horizontal pitch. Its viewport stays in physical +//! body-grid coordinates so compact text never changes pane hit targets. +const std = @import("std"); +const pardes = @import("pardes.zig"); + +pub const Kind = enum(u8) { workspace, column, pane }; +pub const Hit = struct { + kind: Kind, + id: u16, + serial: u32, + col: u16, + pixel_x: f32, + pixel_y: f32, + metrics: pardes.RowMetrics, +}; +pub const Layer = struct { + kind: Kind = .workspace, + id: u16 = 0, + serial: u32 = 0, + viewport: pardes.Rect = .{ .x = 0, .y = 0, .w = 0, .h = 0 }, + cols: u16 = 0, + bg: [3]u8 = .{ 0, 0, 0 }, + cells: []pardes.Cell = &.{}, + cursor: ?struct { col: u16, bar: bool = false } = null, + + pub fn capacity(layer: *const Layer, metrics: pardes.RowMetrics) u16 { + return @intCast(@min(std.math.maxInt(u16), @max(1, @as(u32, layer.viewport.w) * @max(1, metrics.body_w) / @max(1, metrics.tagline_w)))); + } + + pub fn hitAt(layer: *const Layer, x: f32, y: f32, body_w: f32, body_h: f32, tagline_w: f32, tagline_h: f32) ?Hit { + if (layer.cols == 0 or !std.math.isFinite(x) or !std.math.isFinite(y)) return null; + for ([_]f32{ body_w, body_h, tagline_w, tagline_h }) |metric| + if (!std.math.isFinite(metric) or metric < 1 or metric > std.math.maxInt(u16)) return null; + const bw = @max(1, body_w); + const bh = @max(1, body_h); + const tw = std.math.clamp(tagline_w, 1, bw); + const th = std.math.clamp(tagline_h, 1, bh); + const dx = x - @as(f32, @floatFromInt(layer.viewport.x)) * bw; + const dy = y - @as(f32, @floatFromInt(layer.viewport.y)) * bh; + if (dx < 0 or dy < 0 or dx >= @as(f32, @floatFromInt(layer.viewport.w)) * bw or dy >= @as(f32, @floatFromInt(layer.viewport.h)) * bh) return null; + return .{ + .kind = layer.kind, + .id = layer.id, + .serial = layer.serial, + .col = @intFromFloat(@min(@as(f32, @floatFromInt(layer.cols - 1)), @floor(dx / tw))), + .pixel_x = x, + .pixel_y = y, + .metrics = .{ .body_w = @intFromFloat(bw), .body_h = @intFromFloat(bh), .tagline_w = @intFromFloat(tw), .tagline_h = @intFromFloat(th) }, + }; + } +}; + +test "tag text capacity uses its own pitch and excludes the physical grip" { + const layer: Layer = .{ .kind = .pane, .id = 2, .serial = 7, .viewport = .{ .x = 12, .y = 4, .w = 38, .h = 1 }, .cols = 90 }; + try std.testing.expectEqual(@as(u16, 63), layer.capacity(.{ .body_w = 10, .tagline_w = 6 })); + try std.testing.expect(layer.hitAt(119.99, 85, 10, 20, 6, 12) == null); + try std.testing.expectEqual(@as(u16, 0), layer.hitAt(120, 85, 10, 20, 6, 12).?.col); + try std.testing.expectEqual(@as(u16, 50), layer.hitAt(423, 85, 10, 20, 6, 12).?.col); + try std.testing.expect(layer.hitAt(500, 85, 10, 20, 6, 12) == null); + try std.testing.expect(layer.hitAt(120, 85, std.math.nan(f32), 20, 6, 12) == null); + try std.testing.expect(layer.hitAt(120, 85, 10, 20, 0, 12) == null); +} -- cgit v1.3