diff --git a/.trinity/seals/t27os_T27osEditors.json b/.trinity/seals/t27os_T27osEditors.json new file mode 100644 index 0000000000..1792d3ce5e --- /dev/null +++ b/.trinity/seals/t27os_T27osEditors.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+339a2d31b", + "closure": [ + { + "spec": "specs/t27os/session_store.t27", + "spec_hash": "sha256:7d05d7dda14587458a77291c3d587e3f08516d8e678f0a4bd06dcaab7f6d1f2f" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:c884fccdcb816ab5c48fd289bd04515452873c300889d29149cd7481abcf8939", + "gen_hash_rust": "sha256:cac70330d6d304ab3800f870ca5114f10bb2f5d1e029c4de28d2f7232f5a92ca", + "gen_hash_verilog": "sha256:82a39ca4aa16b7d3c37d5fa37f7ac3aeecfde29d0d31030fa23726ef1235eacd", + "gen_hash_zig": "sha256:4dd5942ad00da4c9bc011cb373421e8da83eb6cb98a8ff1bdb1caafb5e98fe78", + "module": "T27osEditors", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-20T07:42:07Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:fc3608c5131f7fae21d6084102a019fc0425117ecf7544ec1eebfa8155d74f72", + "spec_path": "specs/t27os/editors.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 30, + "total": 30, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27os_T27osTermPane.json b/.trinity/seals/t27os_T27osTermPane.json new file mode 100644 index 0000000000..75b2650dd8 --- /dev/null +++ b/.trinity/seals/t27os_T27osTermPane.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+339a2d31b", + "closure": [ + { + "spec": "specs/t27os/pty_proto.t27", + "spec_hash": "sha256:fe9dee96f99eccaa6273124bb9022a66ca916bc9a0f6c3f415c2f4b6ab964ab1" + }, + { + "spec": "specs/t27os/windows.t27", + "spec_hash": "sha256:eddbb0bab395f5d23c89d2e43ee081a2e0c0281ff112151aa61c7fab0ee875af" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:97ceec65b02689bcecdc7a3c8e9cdd359f34d6d8cd073c7d846ea7134205b604", + "gen_hash_rust": "sha256:30d38401423690741c966e8f2067738639bc403e5d32df756d4a0fe16e35ce24", + "gen_hash_verilog": "sha256:a53251a619ef71e8be2dd0de46530dcabd56f70af9701b8c3a81649725a66a10", + "gen_hash_zig": "sha256:0edf236094c91e001fc45a83c938e113451211b31853e0c1d366134693ad4116", + "module": "T27osTermPane", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-20T07:42:12Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:50a9ff94e1c09c8e4d3ac9e2199c871ee00d821cd3e5d9c5b12fc5b994f5ba4e", + "spec_path": "specs/t27os/term_pane.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 10, + "total": 10, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/specs/t27os/editors.t27 b/specs/t27os/editors.t27 index 9df66e5928..4370eb1b84 100644 --- a/specs/t27os/editors.t27 +++ b/specs/t27os/editors.t27 @@ -426,10 +426,11 @@ test only_an_absolute_clean_directory_is_handed_over { // ---- the OPEN IN strip at the foot of the KEYS panel: the installed editors as a grid of text chips ---- /// Three chips of 88 pt fill the 268 pt of the Queen/KEYS panel (QP_W 300 less two QP_PAD): "Antigravity IDE" -/// and "VS Code Insiders" fit one chip at 9 pt, where four 66 pt chips overlapped. +/// and "VS Code Insiders" fit one chip at 9 pt, where four 66 pt chips overlapped. A chip row is 14 pt (it was +/// 16): two rows, the status row and the OPEN IN row then fit under the nine provider rows of a 540 pt window. pub const ED_COLS : u32 = 3; pub const ED_SLOT_W : f64 = 88.0; -pub const ED_SLOT_H : f64 = 16.0; +pub const ED_SLOT_H : f64 = 14.0; pub const ED_NONE : u32 = 255; /// The editor (index into the table) that is the k-th installed one in `mask`; ED_NONE when there are fewer. @@ -486,6 +487,143 @@ pub fn ed_hit(x: f64, y: f64, l: f64, pad: f64, top: f64, count: u32) -> u32 { return ED_NONE; } +// ---- the foot of the KEYS panel: the status row, the OPEN IN row, then the chips, one band each ---- +// Owner, 2026-10-20: the claude status line was drawn on top of the second row of chips. The foot starts at +// `top`, the bottom edge of the last provider row, and stacks rows that never share a y band: +// the status row FT_LINE_H tall, its baseline FT_BASE below the band's top; +// the OPEN IN row FT_LINE_H tall, same baseline; +// the chips ED_COLS to a row, ED_SLOT_H tall, baseline ED_BASE below the slot's top. +// How many chip rows there are comes from the chip count. A row that does not fit above the bottom edge `sh` +// is not drawn, and its chips cannot be clicked (foot_chips): at 960 x 540 six chips are two rows. + +pub const FT_LINE_H : f64 = 12.0; +pub const FT_BASE : f64 = 9.0; +pub const ED_BASE : f64 = 10.0; +/// The ink of a 9 to 9.5 pt line reaches at most FT_ASC above its baseline and FT_DESC below it. +pub const FT_ASC : f64 = 7.0; +pub const FT_DESC : f64 = 2.5; + +/// Chip rows that `count` chips make, ED_COLS to a row. +pub fn strip_rows(count: u32) -> u32 { + return (count + ED_COLS - 1) / ED_COLS; +} + +pub fn foot_status_top(top: f64) -> f64 { + return top; +} + +pub fn foot_head_top(top: f64) -> f64 { + return top + FT_LINE_H; +} + +/// The strip's top edge: the first chip slot (ed_slot_y, ed_hit take this as `top`). +pub fn foot_chips_top(top: f64) -> f64 { + return top + 2.0 * FT_LINE_H; +} + +/// The status row's band lies inside the panel. +pub fn foot_status_fits(top: f64, sh: f64) -> bool { + return top + FT_LINE_H <= sh; +} + +/// Whole chip rows between the strip's top edge and the bottom edge `sh`. +pub fn foot_rows_fit(top: f64, sh: f64) -> u32 { + const room : f64 = sh - foot_chips_top(top); + if (room < ED_SLOT_H) { return 0; } + return @as(u32, @intFromFloat(room / ED_SLOT_H)); +} + +/// Chip rows drawn: all of them when they fit, else as many as fit. +pub fn foot_rows(top: f64, sh: f64, count: u32) -> u32 { + const want : u32 = strip_rows(count); + const fit : u32 = foot_rows_fit(top, sh); + if (want < fit) { return want; } + return fit; +} + +/// Chips drawn, and the only ones a click can land on. +pub fn foot_chips(top: f64, sh: f64, count: u32) -> u32 { + const shown : u32 = foot_rows(top, sh, count) * ED_COLS; + if (count < shown) { return count; } + return shown; +} + +/// The bottom edge of the last chip row drawn (the strip's top when there is none). +pub fn foot_bottom(top: f64, sh: f64, count: u32) -> f64 { + return foot_chips_top(top) + @as(f64, @floatFromInt(foot_rows(top, sh, count))) * ED_SLOT_H; +} + +test the_foot_rows_come_from_the_chip_count { + assert strip_rows(0) == 0; + assert strip_rows(1) == 1; + assert strip_rows(ED_COLS) == 1; + assert strip_rows(ED_COLS + 1) == 2; + assert strip_rows(6) == 2; + assert strip_rows(EDITOR_COUNT) == 9; + // with room, every chip is drawn: the rows are the count over ED_COLS, rounded up + assert foot_rows(487.0, 1440.0, 6) == 2; + assert foot_rows(487.0, 1440.0, 7) == 3; + assert foot_chips(487.0, 1440.0, 7) == 7; + assert foot_chips(487.0, 1440.0, EDITOR_COUNT) == EDITOR_COUNT; + assert foot_rows(487.0, 1440.0, 0) == 0; +} + +test no_two_text_rows_of_the_foot_share_a_y_band { + const top : f64 = 487.0; + // a line's ink band is [baseline - FT_ASC, baseline + FT_DESC]; each band ends before the next begins + assert foot_status_top(top) + FT_BASE - FT_ASC >= top; + assert foot_status_top(top) + FT_BASE + FT_DESC <= foot_head_top(top) + FT_BASE - FT_ASC; + assert foot_head_top(top) + FT_BASE + FT_DESC <= foot_chips_top(top) + ED_BASE - FT_ASC; + // and two chip rows in a row: the next slot's ink starts after this one's ends + assert ED_BASE + FT_DESC <= ED_SLOT_H + ED_BASE - FT_ASC; + assert ED_SLOT_H >= FT_ASC + FT_DESC; + // a chip's baseline is inside its slot, and the slots do not overlap + assert ED_BASE + FT_DESC <= ED_SLOT_H; + assert ed_slot_y(foot_chips_top(top), ED_COLS) == ed_slot_y(foot_chips_top(top), 0) + ED_SLOT_H; + // the foot's three kinds of row are in the order status, OPEN IN, chips + assert foot_status_top(top) < foot_head_top(top); + assert foot_head_top(top) < foot_chips_top(top); +} + +test the_foot_never_draws_below_the_panel_and_hides_whole_rows_only { + const top : f64 = 487.0; + var n : u32 = 0; + while (n <= EDITOR_COUNT) { + // 540 is the window's own height, 720 a taller window, 1440 a full screen + const h0 : f64 = 540.0; + const h1 : f64 = 720.0; + const h2 : f64 = 1440.0; + assert foot_bottom(top, h0, n) <= h0; + assert foot_bottom(top, h1, n) <= h1; + assert foot_bottom(top, h2, n) <= h2; + // a chip that is drawn belongs to a whole row, and a row that is hidden hides all its chips + assert foot_chips(top, h0, n) <= n; + assert foot_chips(top, h0, n) <= foot_rows(top, h0, n) * ED_COLS; + // more room never shows fewer chips + assert foot_chips(top, h1, n) >= foot_chips(top, h0, n); + assert foot_chips(top, h2, n) >= foot_chips(top, h1, n); + // in full screen every one of the 27 editors fits + assert foot_chips(top, h2, n) == n; + n = n + 1; + } +} + +test a_chip_in_a_row_that_is_not_drawn_cannot_be_clicked { + const top : f64 = 487.0; + const l : f64 = 660.0; + const ct : f64 = foot_chips_top(top); + // 540 high: nine chips would be three rows, two fit, so six are drawn and clickable + assert foot_chips(top, 540.0, 9) == 6; + assert ed_hit(l + 16.0 + 3.0, ct + 3.0, l, 16.0, ct, foot_chips(top, 540.0, 9)) == 0; + assert ed_hit(l + 16.0 + 3.0, ct + ED_SLOT_H + 3.0, l, 16.0, ct, foot_chips(top, 540.0, 9)) == ED_COLS; + assert ed_hit(l + 16.0 + 3.0, ct + 2.0 * ED_SLOT_H + 3.0, l, 16.0, ct, foot_chips(top, 540.0, 9)) == ED_NONE; + // in full screen the same click is the seventh chip + assert ed_hit(l + 16.0 + 3.0, ct + 2.0 * ED_SLOT_H + 3.0, l, 16.0, ct, foot_chips(top, 1440.0, 9)) == 2 * ED_COLS; + // a click on the status row or on the OPEN IN row is on no chip + assert ed_hit(l + 16.0 + 3.0, foot_status_top(top) + 3.0, l, 16.0, ct, 6) == ED_NONE; + assert ed_hit(l + 16.0 + 3.0, foot_head_top(top) + 3.0, l, 16.0, ct, 6) == ED_NONE; +} + test the_kth_installed_editor_skips_the_missing_ones { assert nth_installed(0, 0) == ED_NONE; assert nth_installed(1 + 4 + 16, 0) == 0; diff --git a/specs/t27os/glass_window.t27 b/specs/t27os/glass_window.t27 index 2ecbc021af..abf38dd2b3 100644 --- a/specs/t27os/glass_window.t27 +++ b/specs/t27os/glass_window.t27 @@ -141,6 +141,15 @@ use t27os::editors::esc_twice; use t27os::editors::ed_slot_x; use t27os::editors::ed_slot_y; use t27os::editors::ed_hit; +use t27os::editors::ED_BASE; +use t27os::editors::FT_BASE; +use t27os::editors::FT_ASC; +use t27os::editors::FT_DESC; +use t27os::editors::foot_status_top; +use t27os::editors::foot_head_top; +use t27os::editors::foot_chips_top; +use t27os::editors::foot_status_fits; +use t27os::editors::foot_chips; use t27os::editors::open_word; use t27os::editors::OPEN_NONE; use t27os::editors::OPEN_OK; @@ -1058,6 +1067,21 @@ pub fn content_x(ws: u32, collapsed: bool) -> f64 { return side_w(ws, collapsed) + 1.0; } +/// P. The right edge of the content area: the window's edge, or the left edge of the right-hand panel (Queen, +/// DOCTOR or KEYS) while one is open, so the terminal's grid ends where the panel starts and nothing of it +/// sits under the panel (owner, 2026-10-20: the terminal's text showed through the KEYS rows). +pub fn content_right(sw: f64, panel: bool) -> f64 { + if (panel) { return qp_left(sw); } + return sw; +} + +/// P. The width the terminal's grid is laid out in: from the content's left edge to content_right, never negative. +pub fn content_w(sw: f64, ws: u32, collapsed: bool, panel: bool) -> f64 { + const w : f64 = content_right(sw, panel) - content_x(ws, collapsed); + if (w < 0.0) { return 0.0; } + return w; +} + /// W. True when x is over the sidebar's rows (expanded workspace, sidebar shown). pub fn in_side(x: f64, ws: u32, collapsed: bool) -> bool { return ws == WS_EXPANDED and collapsed == false and x >= 12.0 and x <= SIDE_W - 12.0; @@ -1195,6 +1219,34 @@ pub fn card_at(x: f64, y: f64, sw: f64, sh: f64, ws: u32, collapsed: bool) -> u3 return CARD_SLOTS; } +/// T. A terminal pane covers the content area: a chat whose agent opens one, or the editor view, on the chat +/// screen (`topen` is the pane the window shows, PANE_NONE when none). +pub fn term_up(screen: bool, topen: u32) -> bool { + return screen and topen != PANE_NONE; +} + +/// T. The terminal's rectangle, as draw_term lays it out: from the content's left edge to content_right, +/// from under the header to the bottom edge (half open: the header's hairline and the window's edge are out). +pub fn on_term(x: f64, y: f64, sw: f64, sh: f64, ws: u32, collapsed: bool, panel: bool) -> bool { + const l : f64 = content_x(ws, collapsed); + const t : f64 = WS_TOP + HEADER_H; + return x >= l and x < l + content_w(sw, ws, collapsed, panel) and y >= t and y < sh; +} + +/// T. A click that belongs to the terminal: it lands on the rectangle of a pane that is up. The pane takes +/// it (the cursor goes solid) and nothing under it reacts -- the honeycomb cell there used to open an agent +/// chooser when the input row was clicked (owner, 2026-10-20). +pub fn term_click(up: bool, x: f64, y: f64, sw: f64, sh: f64, ws: u32, collapsed: bool, panel: bool) -> bool { + return up and on_term(x, y, sw, sh, ws, collapsed, panel); +} + +/// C. The card slot a click or the cursor sees: none while a terminal pane is up (the cards are not drawn then, +/// the pane covers them), else the slot under (x, y). +pub fn card_live(up: bool, x: f64, y: f64, sw: f64, sh: f64, ws: u32, collapsed: bool) -> u32 { + if (up) { return CARD_SLOTS; } + return card_at(x, y, sw, sh, ws, collapsed); +} + /// C. Cards arrive one after another: slot k shows CASCADE of the fade later than slot k - 1. pub fn card_alpha(ma: f64, k: u32) -> f64 { return clamp01((ma - CASCADE * @as(f64, @floatFromInt(k))) / (1.0 - CASCADE * 8.0)); @@ -1579,14 +1631,14 @@ fn side_row(c: u64, y: f64, h: f64, selected: bool, hover: bool, a: f64) -> void /// C. Chat workspace: sidebar (unless collapsed), divider, content with header, divider and cards. /// K. Compact (ws 1): no sidebar, the OPEN DASHBOARD bar instead of the header. Forced dashboard (ws 2): /// the expanded layout with a zero-width sidebar and the Close Dashboard button in the header. -fn draw_chat(c: u64, kit: [48]u64, shown: u32, task: u32, space: u32, sp: u32, ss: u64, cur: u32, a: f64, collapsed: bool, mx: f64, my: f64, sw: f64, sh: f64, ws: u32, dlabel: u64, dlit: bool) -> void { +fn draw_chat(c: u64, kit: [48]u64, shown: u32, task: u32, space: u32, sp: u32, ss: u64, cur: u32, a: f64, collapsed: bool, mx: f64, my: f64, sw: f64, sh: f64, ws: u32, dlabel: u64, dlit: bool, full: bool, tup: bool) -> void { const cx : f64 = content_x(ws, collapsed); if (ws == WS_DASHBOARD and collapsed == false) { cg_fill_rgba(c, 1.0, 1.0, 1.0, LINE_WHITE * a); cg_fill_rect(c, 0.0, WS_TOP, HAIR, sh - WS_TOP); } if (ws == WS_EXPANDED and collapsed == false) { - cg_fill_rgba(c, 0.0, 0.0, 0.0, STRICT_SIDE * a); + cg_fill_rgba(c, 0.0, 0.0, 0.0, surface_alpha(SURF_SIDEBAR, full) * a); cg_fill_rect(c, 0.0, WS_TOP, SIDE_W, sh - WS_TOP); cg_fill_rgba(c, 1.0, 1.0, 1.0, LINE_WHITE * a); cg_fill_rect(c, SIDE_W, WS_TOP, HAIR, sh - WS_TOP); @@ -1683,9 +1735,10 @@ fn draw_chat(c: u64, kit: [48]u64, shown: u32, task: u32, space: u32, sp: u32, s } r = r + 1; } - // W. the foot: a hairline, then OPEN SOURCE T27, which opens the repository (windows.t27 REPO_URL) - cg_fill_rgba(c, 0.0, 0.0, 0.0, a); - cg_fill_rect(c, 0.0, sh - FOOT_H, SIDE_W, FOOT_H); + // W. the foot: a hairline, then OPEN SOURCE T27, which opens the repository (windows.t27 REPO_URL). + // Its ground hides a long list of chats under it; in a window it is the sidebar's glass again, not + // an opaque black strip + draw_ground(c, 0.0, sh - FOOT_H, SIDE_W, FOOT_H, SURF_SIDEBAR, full, a); cg_fill_rgba(c, 1.0, 1.0, 1.0, LINE_WHITE * a); cg_fill_rect(c, 12.0, sh - FOOT_H, SIDE_W - 24.0, HAIR); var la : f64 = LABEL_WHITE; @@ -1701,14 +1754,16 @@ fn draw_chat(c: u64, kit: [48]u64, shown: u32, task: u32, space: u32, sp: u32, s cg_fill_rgba(c, 1.0, 1.0, 1.0, da * a); draw_text(c, kit, dlabel, SIDE_W - 14.0 - DOC_LINK_W + 22.0, sh - 13.0, 9.5, WEIGHT_SEMIBOLD, false); } - cg_fill_rgba(c, 0.0, 0.0, 0.0, STRICT_CONTENT * a); + cg_fill_rgba(c, 0.0, 0.0, 0.0, surface_alpha(SURF_CONTENT, full) * a); cg_fill_rect(c, cx, WS_TOP, sw - cx, sh - WS_TOP); if (ws == WS_COMPACT) { draw_dash_bar(c, kit, a, sw, on_open_dash(mx, my, sw, ws)); } else { draw_header(c, kit, shown, a, collapsed, mx, my, sw, ws); } - draw_cards(c, kit, task, a, collapsed, mx, my, sw, sh, ws); + // the cards are the screen of a chat with no pane; with a terminal pane up they would show through its + // glass, so they are not drawn + if (tup == false) { draw_cards(c, kit, task, a, collapsed, mx, my, sw, sh, ws); } } /// C. The 44 pt header: sidebar.left toggle, Close Dashboard when forced open, the title, the divider. @@ -2020,9 +2075,9 @@ fn draw_mid(c: u64, kit: [48]u64, s: []const u8, n: usize, kx: f64, base: f64, s /// title bar below is the chat's; the header names the petal and, for a home, the home and its role; the body /// is the 3 x 3 honeycomb with the cards homes.t27 lists for that home (agents, widget counts, courses). A petal /// that is no home shows the nine empty cells (petal_menu: every menu empty). -fn draw_home(c: u64, kit: [48]u64, p: u32, a: f64, mx: f64, my: f64, sw: f64, sh: f64) -> void { +fn draw_home(c: u64, kit: [48]u64, p: u32, a: f64, mx: f64, my: f64, sw: f64, sh: f64, full: bool) -> void { const h : u32 = home_of_petal(p); - cg_fill_rgba(c, 0.0, 0.0, 0.0, STRICT_CONTENT * a); + cg_fill_rgba(c, 0.0, 0.0, 0.0, surface_alpha(SURF_CONTENT, full) * a); cg_fill_rect(c, 0.0, WS_TOP, sw, sh - WS_TOP); const base : f64 = WS_TOP + 0.5 * HEADER_H + 4.2; cg_fill_rgba(c, 1.0, 1.0, 1.0, TEXT_WHITE * a); @@ -2102,6 +2157,65 @@ pub fn tint_swap(full: bool, was_full: bool) -> bool { return full != was_full; } +/// F. Surfaces (owner, 2026-10-20): in a window every surface is a darker tint over the same matte black glass +/// -- the behind-window blur under the layer tint -- and none is opaque; in full screen every surface is +/// opaque black #000000, as the frame is (backdrop_alpha). One fn decides for all of them. +pub const SURF_SIDEBAR : u32 = 0; +pub const SURF_CONTENT : u32 = 1; +pub const SURF_TERMINAL : u32 = 2; +pub const SURF_PANEL : u32 = 3; +/// F. The terminal is drawn over the content's STRICT_CONTENT (0.35): 0.15 more brings the two to 0.4475, +/// the sidebar's 0.45 (the same look as the sidebar, which the owner called glass). +pub const TERMINAL_TINT : f64 = 0.15; +/// F. The right-hand panels (Queen, DOCTOR, KEYS) are the elevated surface of trios (0.58): their small +/// type needs more ground than the sidebar's. +pub const PANEL_BLACK : f64 = 0.58; + +/// F. The alpha of a surface's black fill: its glass tint in a window, 1.0 (opaque #000000) in full screen. +pub fn surface_alpha(kind: u32, full: bool) -> f64 { + if (full) { return 1.0; } + if (kind == SURF_SIDEBAR) { return STRICT_SIDE; } + if (kind == SURF_CONTENT) { return STRICT_CONTENT; } + if (kind == SURF_TERMINAL) { return TERMINAL_TINT; } + return PANEL_BLACK; +} + +/// F. A surface that is not opaque must first wipe what was drawn under it, as the backdrop does, or the +/// cards and the last frame show through it. +pub fn surface_clears(kind: u32, full: bool) -> bool { + return surface_alpha(kind, full) < 1.0; +} + +/// F. A black surface over a rectangle: wiped first when it is glass (and fully shown: during the 0.25 s +/// cross-fade `a` is below 1 and the wipe waits, so nothing pops), then the tint times `a`. +fn draw_ground(c: u64, x: f64, y: f64, w: f64, h: f64, kind: u32, full: bool, a: f64) -> void { + if (surface_clears(kind, full) and a >= 1.0) { + cg_blend(c, BLEND_CLEAR); + cg_fill_rect(c, x, y, w, h); + cg_blend(c, BLEND_NORMAL); + } + const s : f64 = surface_alpha(kind, full) * a; + if (s > 0.0) { + cg_fill_rgba(c, 0.0, 0.0, 0.0, s); + cg_fill_rect(c, x, y, w, h); + } +} + +/// F. The ground under a right-hand panel, drawn once before it: the whole column from the top of the +/// workspace to the bottom edge, wiped to the glass and tinted (opaque black in full screen). The wipe reaches +/// BACK_BLEED past the right and bottom edges, where a fractional pixel would keep part of the last frame. +fn draw_panel_ground(c: u64, sw: f64, sh: f64, full: bool) -> void { + const l : f64 = qp_left(sw); + draw_ground(c, l, WS_TOP, sw - l + BACK_BLEED, sh - WS_TOP + BACK_BLEED, SURF_PANEL, full, 1.0); +} + +/// F. A plate under a panel's button, so a long line of the row under it does not show through the button: the +/// panel's own glass wiped and tinted again, not an opaque black box. In full screen the wiped pixel shows +/// the layer's opaque black (tint_alpha(true) == 1.0), so the plate is #000000 there as well. +fn draw_plate(c: u64, x: f64, y: f64, w: f64, h: f64) -> void { + draw_ground(c, x, y, w, h, SURF_PANEL, false, 1.0); +} + /// F. The background: clear (BLEND_CLEAR) in a window, one opaque black fill over the whole frame in full screen. fn draw_backdrop(c: u64, sw: f64, sh: f64, full: bool) -> void { if (backdrop_clears(full)) { @@ -2124,8 +2238,7 @@ fn draw_queen_panel(c: u64, kit: [48]u64, sw: f64, sh: f64, mx: f64, my: f64, vi const l : f64 = qp_left(sw); const t : f64 = WS_TOP; const x : f64 = l + QP_PAD; - cg_fill_rgba(c, 0.0, 0.0, 0.0, 0.94); - cg_fill_rect(c, l, t, QP_W, sh - t); + // the ground is draw_panel_ground's, drawn once before the panel cg_fill_rgba(c, 1.0, 1.0, 1.0, LINE_WHITE); cg_fill_rect(c, l, t, HAIR, sh - t); // the crown and QUEEN @@ -2359,13 +2472,16 @@ fn term_banner(st: []u32, cells: []u32, cwd: u64) -> bool { return true; } -/// The pane: black content area under the header, one monospace line per grid row, a white -/// block cursor with its character redrawn in black. -fn draw_term(c: u64, kit: [48]u64, st: []const u32, cells: []const u32, curon: bool, a: f64, collapsed: bool, sw: f64, sh: f64, ws: u32) -> void { +/// The pane: the content area under the header, one monospace line per grid row, a white block cursor with +/// its character redrawn in black. Its default background is a surface (surface_alpha SURF_TERMINAL): glass in +/// a window, opaque #000000 in full screen. The program's colours are dropped by vt.t27 (SGR is read and +/// discarded), so no cell has a background of its own: the one filled cell is the cursor. The area ends at +/// the left edge of an open right-hand panel (content_right), where the grid also ends. +fn draw_term(c: u64, kit: [48]u64, st: []const u32, cells: []const u32, curon: bool, a: f64, collapsed: bool, sw: f64, sh: f64, ws: u32, full: bool, panel: bool) -> void { const ox : f64 = content_x(ws, collapsed); const oy : f64 = WS_TOP + HEADER_H; - cg_fill_rgba(c, 0.0, 0.0, 0.0, a); - cg_fill_rect(c, ox, oy + HAIR, sw - ox, sh - oy - HAIR); + cg_fill_rgba(c, 0.0, 0.0, 0.0, surface_alpha(SURF_TERMINAL, full) * a); + cg_fill_rect(c, ox, oy + HAIR, content_w(sw, ws, collapsed, panel), sh - oy - HAIR); const cols : u32 = st[ST_COLS]; const rows : u32 = st[ST_ROWS]; var lb : [1600]u8 = undefined; @@ -2486,13 +2602,12 @@ fn status_dot(st: u32) -> u32 { return DOT_ALERT; } -/// One of the three buttons of a row: a black plate, a rounded outline, the word centred; `on` draws it -/// filled (the armed DELETE), `live` false draws it dim (nothing to delete or check). +/// One of the three buttons of a row: a plate of the panel's glass, a rounded outline, the word centred; `on` +/// draws it filled (the armed DELETE), `live` false draws it dim (nothing to delete or check). fn draw_key_btn(c: u64, kit: [48]u64, bx: f64, by: f64, word: u32, on: bool, live: bool, over: bool) -> void { var w : [24]u8 = undefined; const n : usize = csv_field(KEYS_WORDS, word, w[0..24]); - cg_fill_rgba(c, 0.0, 0.0, 0.0, 1.0); - cg_fill_rect(c, bx - 2.0, by - 2.0, KY_BTN_W + 4.0, KY_BTN_H + 4.0); + draw_plate(c, bx - 2.0, by - 2.0, KY_BTN_W + 4.0, KY_BTN_H + 4.0); round_rect(c, bx, by, KY_BTN_W, KY_BTN_H, 5.0); var ink : f64 = TEXT_WHITE; if (on) { @@ -2516,8 +2631,7 @@ fn draw_keys_panel(c: u64, kit: [48]u64, sw: f64, sh: f64, mx: f64, my: f64, sto const t : f64 = WS_TOP; const x : f64 = l + QP_PAD; var w : [48]u8 = undefined; - cg_fill_rgba(c, 0.0, 0.0, 0.0, 0.94); - cg_fill_rect(c, l, t, QP_W, sh - t); + // the ground is draw_panel_ground's, drawn once before the panel cg_fill_rgba(c, 1.0, 1.0, 1.0, LINE_WHITE); cg_fill_rect(c, l, t, HAIR, sh - t); cg_fill_rgba(c, 1.0, 1.0, 1.0, TEXT_WHITE); @@ -2587,34 +2701,36 @@ fn draw_keys_panel(c: u64, kit: [48]u64, sw: f64, sh: f64, mx: f64, my: f64, sto var wy : [96]u8 = undefined; n = profile_why(sm, bad_mask(kst[0..9]), wy[0..96]); cg_fill_rgba(c, 1.0, 1.0, 1.0, LABEL_WHITE); - draw_bytes(c, kit, wy[0..96], n, x, sh - 14.0, 9.5, 0.0, false); - // the OPEN IN strip: the editors found on this Mac (editors.t27), one chip each, below the rows - const ec : u32 = strip_count(kemask, ktpick); - if (ec > 0) { - const top : f64 = ky_row_top(t, PROVIDER_COUNT) + 4.0; - if (top + 14.0 + ED_SLOT_H <= sh) { - n = csv_field(KEYS_WORDS, 10, w[0..48]); - cg_fill_rgba(c, 1.0, 1.0, 1.0, LABEL_WHITE); - const hw : f64 = draw_bytes(c, kit, w[0..48], n, x, top + 8.0, 9.5, 0.0, false); - // why the last click did not open an editor (editors.t27 open_word); nothing after an open that started - if (kewhy != OPEN_NONE and kewhy != OPEN_OK) { - n = open_word(kewhy, w[0..48]); - cg_fill_rgba(c, 1.0, 1.0, 1.0, TEXT_WHITE); - draw_bytes(c, kit, w[0..48], n, x + hw + 8.0, top + 8.0, 9.5, 0.0, false); - } - const hv : u32 = ed_hit(mx, my, l, QP_PAD, top + 14.0, ec); - var k : u32 = 0; - while (k < ec) { - n = strip_label(strip_chip(kemask, ktpick, k), w[0..48]); - var ea : f64 = MUTED_WHITE; - if (hv == k) { ea = TEXT_WHITE; } - cg_fill_rgba(c, 1.0, 1.0, 1.0, ea); - const cx : f64 = ed_slot_x(l, QP_PAD, k); - const cy : f64 = ed_slot_y(top + 14.0, k); - const cw : f64 = draw_bytes(c, kit, w[0..48], n, cx, cy + 11.0, 9.0, 0.0, false); - if (hv == k) { cg_fill_rect(c, cx, cy + 13.0, cw, HAIR); } - k = k + 1; - } + // the foot (editors.t27 foot_*): the status row, then OPEN IN, then the chips, each in a band of its own + // below the provider rows, so no two text rows share a y band (the status line sat on the chips) + const ft : f64 = ky_row_top(t, PROVIDER_COUNT); + if (foot_status_fits(ft, sh)) { + draw_bytes(c, kit, wy[0..96], n, x, foot_status_top(ft) + FT_BASE, 9.5, 0.0, false); + } + // the OPEN IN strip: the editors found on this Mac (editors.t27), one chip each; only whole rows that fit + const nc : u32 = foot_chips(ft, sh, strip_count(kemask, ktpick)); + if (nc > 0) { + n = csv_field(KEYS_WORDS, 10, w[0..48]); + cg_fill_rgba(c, 1.0, 1.0, 1.0, LABEL_WHITE); + const hw : f64 = draw_bytes(c, kit, w[0..48], n, x, foot_head_top(ft) + FT_BASE, 9.5, 0.0, false); + // why the last click did not open an editor (editors.t27 open_word); nothing after an open that started + if (kewhy != OPEN_NONE and kewhy != OPEN_OK) { + n = open_word(kewhy, w[0..48]); + cg_fill_rgba(c, 1.0, 1.0, 1.0, TEXT_WHITE); + draw_bytes(c, kit, w[0..48], n, x + hw + 8.0, foot_head_top(ft) + FT_BASE, 9.5, 0.0, false); + } + const hv : u32 = ed_hit(mx, my, l, QP_PAD, foot_chips_top(ft), nc); + var k : u32 = 0; + while (k < nc) { + n = strip_label(strip_chip(kemask, ktpick, k), w[0..48]); + var ea : f64 = MUTED_WHITE; + if (hv == k) { ea = TEXT_WHITE; } + cg_fill_rgba(c, 1.0, 1.0, 1.0, ea); + const cx : f64 = ed_slot_x(l, QP_PAD, k); + const cy : f64 = ed_slot_y(foot_chips_top(ft), k); + const cw : f64 = draw_bytes(c, kit, w[0..48], n, cx, cy + ED_BASE, 9.0, 0.0, false); + if (hv == k) { cg_fill_rect(c, cx, cy + ED_BASE + 2.0, cw, HAIR); } + k = k + 1; } } } @@ -2635,12 +2751,14 @@ fn draw(c: u64, kit: [48]u64, frame: u32, state: u32, hov: u32, ma: f64, shown: // the screen of petal `shown`: the chat workspace for the CHAT petal (index 2), a home tab for t27a .. // t27u, an empty frame with the petal's name for every other petal if (shown == PETAL_CHAT) { - draw_chat(c, kit, shown, task, space, sp, ss, cur, ma, collapsed, mx, my, sw, sh, ws, dlabel, dp); - if (topen != PANE_NONE) { draw_term(c, kit, tst, tcells, tcur, ma, collapsed, sw, sh, ws); } + draw_chat(c, kit, shown, task, space, sp, ss, cur, ma, collapsed, mx, my, sw, sh, ws, dlabel, dp, full, topen != PANE_NONE); + if (topen != PANE_NONE) { draw_term(c, kit, tst, tcells, tcur, ma, collapsed, sw, sh, ws, full, qp or dp or kyp); } } else { - draw_home(c, kit, shown, ma, mx, my, sw, sh); + draw_home(c, kit, shown, ma, mx, my, sw, sh, full); } } + // exactly one right-hand panel is drawn (the Queen's, else DOCTOR's, else KEYS'): its ground first + if (qp or dp or kyp) { draw_panel_ground(c, sw, sh, full); } if (qp) { draw_queen_panel(c, kit, sw, sh, mx, my, view_of(menu_open(state), shown), space, ss, cur, ring, n, now, sug, god); } if (dp and qp == false) { draw_doctor_panel(c, kit, dk, sw, sh, mx, my, darmed, doff, dgo, dlive); } if (kyp and qp == false and dp == false) { draw_keys_panel(c, kit, sw, sh, mx, my, kstored, kmask, kmlen, kst, kfail, kopen, now, kedit, kbuf, klen, karm, kbusy, kemask, kewhy, ktpick); } @@ -3223,8 +3341,7 @@ fn draw_doctor_panel(c: u64, kit: [48]u64, dk: [128]u64, sw: f64, sh: f64, mx: f const l : f64 = qp_left(sw); const t : f64 = WS_TOP; const x : f64 = l + QP_PAD; - cg_fill_rgba(c, 0.0, 0.0, 0.0, 0.94); - cg_fill_rect(c, l, t, QP_W, sh - t); + // the ground is draw_panel_ground's, drawn once before the panel cg_fill_rgba(c, 1.0, 1.0, 1.0, LINE_WHITE); cg_fill_rect(c, l, t, HAIR, sh - t); cg_fill_rgba(c, 1.0, 1.0, 1.0, TEXT_WHITE); @@ -3284,9 +3401,8 @@ fn draw_doctor_panel(c: u64, kit: [48]u64, dk: [128]u64, sw: f64, sh: f64, mx: f const over : bool = on_doc_btn(mx, my, l, QP_W, QP_PAD, t, r); var word : u32 = DL_REMOVE; if (armed == @as(u32, @intCast(ri))) { word = DL_CONFIRM; } - // a black plate first, so a long line under the button does not show through it - cg_fill_rgba(c, 0.0, 0.0, 0.0, 1.0); - cg_fill_rect(c, bx - 6.0, by - 2.0, DOC_BTN_W + 6.0, DOC_BTN_H + 4.0); + // a plate of the panel's glass first, so a long line under the button does not show through it + draw_plate(c, bx - 6.0, by - 2.0, DOC_BTN_W + 6.0, DOC_BTN_H + 4.0); round_rect(c, bx, by, DOC_BTN_W, DOC_BTN_H, 5.0); if (word == DL_CONFIRM) { cg_fill_rgba(c, 1.0, 1.0, 1.0, TEXT_WHITE); @@ -3929,8 +4045,8 @@ pub fn main() -> void { } } else { karm = KY_NONE; - const ktop : f64 = ky_row_top(WS_TOP, PROVIDER_COUNT) + 4.0 + 14.0; - const keh : u32 = ed_hit(mx, my, kl, QP_PAD, ktop, strip_count(kemask, ktpick)); + const kft : f64 = ky_row_top(WS_TOP, PROVIDER_COUNT); + const keh : u32 = ed_hit(mx, my, kl, QP_PAD, foot_chips_top(kft), foot_chips(kft, sh, strip_count(kemask, ktpick))); if (keh != ED_NONE) { // the editor opens the folder of the space on screen (home when it is unbound) var kdirs : u64 = dk_at(dk, dk[DK_SLOTS], @as(u64, space)); @@ -3967,6 +4083,11 @@ pub fn main() -> void { darmed = ARMED_NONE; } } + } else if (term_click(term_up(screen, topen), mx, my, sw, sh, ws, collapsed, qp or dp or kyp)) { + // T. a click on the terminal pane is the pane's: its cursor goes solid and nothing under it + // reacts (no card, no petal, no bar); every control the chain below answers lies outside + // on_term (the title bar, the header, the dashboard bar, the sidebar), or is a card + tsince = 0; } else if (on_home_button(mx, my, sw)) { ctl = C_HOME; state = on_back(state); @@ -4002,9 +4123,9 @@ pub fn main() -> void { if (session_count(ss) > before) { cur = before; task = nk; } ctl = C_NEW_CHAT; carg = nk; - } else if (screen and card_at(mx, my, sw, sh, ws, collapsed) != CARD_SLOTS) { + } else if (screen and card_live(term_up(screen, topen), mx, my, sw, sh, ws, collapsed) != CARD_SLOTS) { // A. a tap on a cell opens that agent's chat here, or a first one if there is none - task = card_at(mx, my, sw, sh, ws, collapsed); + task = card_live(term_up(screen, topen), mx, my, sw, sh, ws, collapsed); ctl = C_CELL; carg = task; cur = SESSION_NONE; @@ -4384,9 +4505,10 @@ pub fn main() -> void { if (editor_view_stays(eview, screen, cur, ecur, space, esp) == false) { eview = false; } const want : u32 = view_key(eview, cur != SESSION_NONE and opens_pane(session_agent(ss, cur)), cur); if (screen and want != PANE_NONE) { - const tox : f64 = content_x(ws, collapsed); const toy : f64 = WS_TOP + HEADER_H; - const tw : f64 = sw - tox; + // the grid ends at the left edge of an open right-hand panel: the pty gets the narrower size + // through the resize below, and nothing of the terminal is under the panel + const tw : f64 = content_w(sw, ws, collapsed, qp or dp or kyp); const th : f64 = sh - toy; // a drag changes the size every frame: the pane takes it once it has held TP_SETTLE frames, so // the grid and the ring replay run once per resize instead of once per frame @@ -4523,7 +4645,7 @@ pub fn main() -> void { if (hscreen and on_nav_back(mx, my, full)) { on_control = true; } if (screen and (on_nav_back(mx, my, full) or on_side_toggle(mx, my, ws, collapsed))) { on_control = true; } if (screen and (on_open_dash(mx, my, sw, ws) or on_close_dash(mx, my, ws, collapsed))) { on_control = true; } - if (screen and ma >= 1.0 and card_at(mx, my, sw, sh, ws, collapsed) != CARD_SLOTS) { on_control = true; } + if (screen and ma >= 1.0 and card_live(term_up(screen, topen), mx, my, sw, sh, ws, collapsed) != CARD_SLOTS) { on_control = true; } if (screen and ma >= 1.0 and in_side(mx, ws, collapsed) and (row_at(WS_TOP, GROUP_QUEEN, spaces_shown(sp), 1, my) == 0 or row_at(WS_TOP, GROUP_SPACES, spaces_shown(sp), spaces_shown(sp), my) != ROW_NONE or row_at(WS_TOP, GROUP_AGENTS, spaces_shown(sp), sessions_in(ss, space), my) != ROW_NONE)) { on_control = true; } if (screen and on_new_chat(mx, my, sw, ws)) { on_control = true; } if (screen and (on_repo_link(mx, my, sh, ws, collapsed) or (in_side(mx, ws, collapsed) and on_space_plus(mx, my) and new_space(sp) != SPACE_NONE))) { on_control = true; } @@ -5043,6 +5165,154 @@ test the_editor_view_shares_the_terminal_areas_key_space_with_the_chat_panes { assert view_key(true, false, SESSION_NONE) == VIEW_EDITOR; } +test every_surface_is_glass_in_a_window_and_opaque_black_in_full_screen { + // owner, 2026-10-20: a window is matte black glass all over -- each surface a darker tint over the blur, + // none opaque -- and full screen is opaque #000000 all over + assert surface_alpha(SURF_SIDEBAR, false) == 0.45; + assert surface_alpha(SURF_CONTENT, false) == 0.35; + assert surface_alpha(SURF_TERMINAL, false) == 0.15; + assert surface_alpha(SURF_PANEL, false) == 0.58; + assert surface_alpha(SURF_SIDEBAR, true) == 1.0; + assert surface_alpha(SURF_CONTENT, true) == 1.0; + assert surface_alpha(SURF_TERMINAL, true) == 1.0; + assert surface_alpha(SURF_PANEL, true) == 1.0; + var k : u32 = SURF_SIDEBAR; + while (k <= SURF_PANEL) { + // never opaque in a window, always opaque in full screen, and then the same as the frame + assert surface_alpha(k, false) < 1.0; + assert surface_alpha(k, false) > 0.0; + assert surface_clears(k, false); + assert surface_alpha(k, true) == backdrop_alpha(true); + assert surface_clears(k, true) == false; + k = k + 1; + } + // the terminal is drawn over the content: together they are the sidebar's tone (0.4475 against 0.45) + const both : f64 = 1.0 - (1.0 - surface_alpha(SURF_CONTENT, false)) * (1.0 - surface_alpha(SURF_TERMINAL, false)); + assert @abs(both - surface_alpha(SURF_SIDEBAR, false)) < 0.01; + // a right-hand panel is the darkest surface, and still glass + assert surface_alpha(SURF_PANEL, false) > surface_alpha(SURF_SIDEBAR, false); + assert surface_alpha(SURF_PANEL, false) < 1.0; + // a plate wiped inside a panel in full screen shows the layer, which is opaque black there + assert tint_alpha(true) == 1.0; +} + +test an_open_panel_takes_its_width_from_the_terminal { + const sw : f64 = 960.0; + // no panel: the content runs to the window's edge, as it always did + assert content_right(sw, false) == sw; + assert content_w(sw, WS_EXPANDED, false, false) == sw - content_x(WS_EXPANDED, false); + // a panel (Queen, DOCTOR or KEYS, all QP_W wide): the content ends at the panel's left edge + assert content_right(sw, true) == qp_left(sw); + assert content_right(sw, true) == sw - QP_W; + assert content_w(sw, WS_EXPANDED, false, true) == content_w(sw, WS_EXPANDED, false, false) - QP_W; + assert content_w(sw, WS_EXPANDED, false, true) == 387.0; + assert content_w(sw, WS_EXPANDED, true, true) == 660.0; + // the grid the pty is resized to is narrower, and its last cell ends before the panel begins + assert tp_cols(content_w(sw, WS_EXPANDED, false, true)) < tp_cols(content_w(sw, WS_EXPANDED, false, false)); + assert tp_cols(content_w(sw, WS_EXPANDED, false, true)) == 49; + assert tp_cell_x(content_x(WS_EXPANDED, false), tp_cols(content_w(sw, WS_EXPANDED, false, true))) <= qp_left(sw); + // full screen: wider window, same rule + assert content_w(1920.0, WS_EXPANDED, false, true) == 1920.0 - QP_W - content_x(WS_EXPANDED, false); + // a window too narrow for the panel leaves no negative width + assert content_w(200.0, WS_COMPACT, false, true) == 0.0; +} + +test the_keys_foot_gives_the_status_the_open_in_row_and_each_chip_row_a_band_at_960_by_540 { + const ft : f64 = ky_row_top(WS_TOP, PROVIDER_COUNT); + const last : f64 = ky_row_top(WS_TOP, PROVIDER_COUNT - 1); + // the foot starts below the last provider row's buttons and plates + assert last + KY_BTN_DY + KY_BTN_H + 2.0 <= foot_status_top(ft); + // the bench Mac's six chips (T27 OS, Cursor, Xcode, Antigravity, Antigravity IDE, ZCode) are two rows + assert foot_status_fits(ft, H_PT); + assert foot_chips(ft, H_PT, 6) == 6; + assert foot_chips_top(ft) + 2.0 * ED_SLOT_H <= H_PT; + // the second row of chips (the screenshot's Antigravity row) is below the status line's ink, not on it + const status_base : f64 = foot_status_top(ft) + FT_BASE; + const row1_base : f64 = ed_slot_y(foot_chips_top(ft), ED_COLS) + ED_BASE; + assert row1_base - FT_ASC > status_base + FT_DESC; + // status, OPEN IN, chip row 0, chip row 1: each baseline is a full ink height below the one before + const head_base : f64 = foot_head_top(ft) + FT_BASE; + const row0_base : f64 = ed_slot_y(foot_chips_top(ft), 0) + ED_BASE; + assert head_base - FT_ASC > status_base + FT_DESC; + assert row0_base - FT_ASC > head_base + FT_DESC; + assert row1_base - FT_ASC > row0_base + FT_DESC; + // a taller window or a full screen shows every chip of a full table, and still nothing under the bottom edge + assert foot_chips(ft, 900.0, EDITOR_COUNT) == EDITOR_COUNT; + assert foot_chips(ft, 1440.0, EDITOR_COUNT) == EDITOR_COUNT; + assert foot_bottom(ft, 900.0, EDITOR_COUNT) <= 900.0; + // the window's own height holds six chips of a longer table: whole rows only, never a partial one + assert foot_chips(ft, H_PT, EDITOR_COUNT) == 6; +} + +/// Test helper: how many sampled points of the terminal's rectangle (every 12 pt) a window control answers at. +/// The title bar, the header (toggle, Close Dashboard, "+"), the compact dashboard bar, the sidebar and a +/// right-hand panel all lie outside that rectangle, so this is 0 in every layout. The home logo's petals need +/// the home screen, where no pane is up. +fn controls_under_term(sw: f64, sh: f64, ws: u32, collapsed: bool, panel: bool) -> u32 { + var hits : u32 = 0; + const l : f64 = content_x(ws, collapsed); + const r : f64 = l + content_w(sw, ws, collapsed, panel); + var y : f64 = WS_TOP + HEADER_H; + while (y < sh) { + var x : f64 = l; + while (x < r) { + if (on_home_button(x, y, sw) or on_full_button(x, y, sw) or on_dock_button(x, y, sw)) { hits = hits + 1; } + if (on_nav_back(x, y, false) or on_nav_back(x, y, true)) { hits = hits + 1; } + if (on_open_dash(x, y, sw, ws) or on_close_dash(x, y, ws, collapsed)) { hits = hits + 1; } + if (on_side_toggle(x, y, ws, collapsed) or on_new_chat(x, y, sw, ws)) { hits = hits + 1; } + // the sidebar's rows and its "+" answer only where in_side says the sidebar is + if (in_side(x, ws, collapsed)) { hits = hits + 1; } + if (on_repo_link(x, y, sh, ws, collapsed) or on_doctor_link(x, y, sh, ws, collapsed) or on_keys_foot(x, y, sh, ws, collapsed)) { hits = hits + 1; } + if (panel and on_panel(x, y, sw, WS_TOP)) { hits = hits + 1; } + x = x + 12.0; + } + y = y + 12.0; + } + return hits; +} + +test a_click_on_a_terminal_pane_is_the_panes_and_nothing_under_it_answers { + // the bug: the input row of a Claude Code pane lies over a honeycomb cell, and the click opened an agent chat + const sw : f64 = W_PT; + const sh : f64 = H_PT; + const bl : f64 = body_left(sw, WS_EXPANDED, false); + const bt : f64 = body_top(WS_EXPANDED); + const bw : f64 = body_w(sw, WS_EXPANDED, false); + const bh : f64 = body_h(sw, sh, WS_EXPANDED, false); + const cx : f64 = hex_c(4, 0, bl, bt, bw, bh); + const cy : f64 = hex_c(4, 1, bl, bt, bw, bh); + assert card_at(cx, cy, sw, sh, WS_EXPANDED, false) == 4; + assert on_term(cx, cy, sw, sh, WS_EXPANDED, false, false); + // with a pane up the click is the pane's and no card answers it, in the click and under the cursor + assert term_click(term_up(true, 2), cx, cy, sw, sh, WS_EXPANDED, false, false); + assert card_live(term_up(true, 2), cx, cy, sw, sh, WS_EXPANDED, false) == CARD_SLOTS; + // the editor view is a pane too + assert term_up(true, VIEW_EDITOR); + assert term_click(term_up(true, VIEW_EDITOR), cx, cy, sw, sh, WS_EXPANDED, false, false); + // with no pane the card answers as before, and the pane takes nothing + assert term_up(true, PANE_NONE) == false; + assert term_up(false, 2) == false; + assert term_click(term_up(true, PANE_NONE), cx, cy, sw, sh, WS_EXPANDED, false, false) == false; + assert card_live(term_up(true, PANE_NONE), cx, cy, sw, sh, WS_EXPANDED, false) == 4; + // the rectangle is the terminal's: right of the sidebar, under the header, short of an open panel + assert on_term(content_x(WS_EXPANDED, false) + 1.0, WS_TOP + HEADER_H + 1.0, sw, sh, WS_EXPANDED, false, false); + assert on_term(content_x(WS_EXPANDED, false) - 2.0, WS_TOP + HEADER_H + 1.0, sw, sh, WS_EXPANDED, false, false) == false; + assert on_term(500.0, WS_TOP + HEADER_H - 1.0, sw, sh, WS_EXPANDED, false, false) == false; + assert on_term(500.0, sh, sw, sh, WS_EXPANDED, false, false) == false; + assert on_term(sw - 1.0, 300.0, sw, sh, WS_EXPANDED, false, false); + assert on_term(sw - 1.0, 300.0, sw, sh, WS_EXPANDED, false, true) == false; + assert on_term(qp_left(sw) - 1.0, 300.0, sw, sh, WS_EXPANDED, false, true); + // the sidebar's rows, the header's buttons, the title bar, the dashboard bar: none is inside the rectangle + assert controls_under_term(sw, sh, WS_EXPANDED, false, false) == 0; + assert controls_under_term(sw, sh, WS_EXPANDED, false, true) == 0; + assert controls_under_term(sw, sh, WS_EXPANDED, true, false) == 0; + assert controls_under_term(1728.0, 1117.0, WS_EXPANDED, false, false) == 0; + assert controls_under_term(1728.0, 1117.0, WS_EXPANDED, false, true) == 0; + assert controls_under_term(700.0, 900.0, WS_DASHBOARD, false, false) == 0; + assert controls_under_term(PANEL_W, 900.0, WS_COMPACT, false, false) == 0; + assert controls_under_term(PANEL_W, 900.0, WS_COMPACT, false, true) == 0; +} + test chat_is_the_third_petal_and_cmd_3_opens_it { assert PETAL_CHAT == 2; assert petal_for_key(3, MOD_COMMAND) == PETAL_CHAT;