//! 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"); /// `notice` is a pane's transient line -- a builtin's name, a pending leader /// chord -- carried as a tagline-shaped band so it gets the tagline's height, /// small-font metrics, band offset and border instead of being a body-grid row /// wearing a tagline font. pub const Kind = enum(u8) { workspace, column, pane, notice }; pub const Hit = struct { kind: Kind, id: u16, serial: u32, col: u16, /// Which line of a taller tag the layer draws. line: u16 = 0, pixel_x: f32, pixel_y: f32, metrics: pardes.RowMetrics, }; pub const Layer = struct { kind: Kind = .workspace, id: u16 = 0, serial: u32 = 0, /// A pane's tag taller than a line is drawn a layer to a line; this is /// which one, counted from its first. line: u16 = 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, /// Vertical offset in rows (negative: above its viewport), for a band /// that is moving into place. A pixel shell draws it offset and clipped /// to the rows below the viewport's top; the grid has no such thing. slide: f32 = 0, /// How present a notice band is (1 = fully) while it fades up or /// dissolves. `bg` and the cells are already blended by it; a pixel /// shell uses it for what it draws around the band, like its rule. fade: f32 = 1, 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, .line = layer.line, .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); } /// Whether two pointer hits name the same cell of the same tag line. pub fn sameCell(a: ?Hit, b: ?Hit) bool { const first = a orelse return b == null; const second = b orelse return false; return first.kind == second.kind and first.id == second.id and first.serial == second.serial and first.line == second.line and first.col == second.col; } /// The tag column a host's pointer is at. Clamped, a point off the tag is /// brought back onto line `line` of it, for a drag that leaves it. pub fn columnAt(p: *const pardes.Pardes, kind: Kind, id: usize, supplied: ?Hit, clamp: bool, line: u16) ?u16 { var point = supplied orelse return null; if (point.kind != kind or point.id != id) return null; if (clamp) for (p.surface.tagLayers()) |*layer| { if (layer.cols == 0 or layer.kind != kind or layer.id != id or layer.serial != point.serial or layer.line != line) continue; const bw: f32 = @floatFromInt(point.metrics.body_w); const bh: f32 = @floatFromInt(point.metrics.body_h); const left = @as(f32, @floatFromInt(layer.viewport.x)) * bw; const right = @as(f32, @floatFromInt(layer.viewport.x + layer.viewport.w)) * bw; point.pixel_x = std.math.clamp(point.pixel_x, left, @max(left, right - 0.001)); point.pixel_y = (@as(f32, @floatFromInt(layer.viewport.y)) + 0.5) * bh; break; }; return if (p.reprojectTagHit(point)) |mapped| mapped.col else null; }