diff --git a/.gitignore b/.gitignore index c36421e..fbba07a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .obsidian/ .letta/settings.local.json .letta/worktrees/ +.Codex/ # Local editor/vault metadata .obsidian/ diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index 12612a9..217f0a8 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -29,8 +29,10 @@ const SIDEBAR_WIDTH: f32 = 420.0; const SIDEBAR_SCROLL_LINE: f32 = 18.0; const SIDEBAR_SCROLL_PAGE: f32 = 180.0; const COMPUTE_CHANNELS: usize = 5; -/// Context-menu card width in logical pixels. -const MENU_WIDTH: f32 = 380.0; +/// Context-menu card width. Long `verb | cost | [band]` lines need room; +/// the embedded font cannot wrap mid-glyph cleanly, so prefer width over +/// truncation (wiki/interface/context-menu.md). +const MENU_WIDTH: f32 = 460.0; const DETECTION_ROWS: usize = 6; const DETECTION_CELLS: usize = 4; @@ -2812,19 +2814,53 @@ fn setup_ui(mut commands: Commands) { top: Val::Px(0.0), width: Val::Px(MENU_WIDTH), flex_direction: FlexDirection::Column, - padding: UiRect::all(Val::Px(6.0)), + padding: UiRect::axes(Val::Px(8.0), Val::Px(7.0)), border: UiRect::all(Val::Px(1.0)), - row_gap: Val::Px(1.0), + row_gap: Val::Px(2.0), ..default() }, - BackgroundColor(Color::srgba(0.02, 0.02, 0.03, 0.98)), - BorderColor::all(Color::srgba(0.66, 0.47, 0.10, 0.9)), + BackgroundColor(Color::srgba(0.015, 0.016, 0.022, 0.97)), + BorderColor::all(Color::srgba(0.66, 0.47, 0.10, 0.95)), Visibility::Hidden, GlobalZIndex(50), MenuPanel, )); } +/// Fold shared menu / log wording into glyphs Bevy's embedded default font +/// can draw. Terminal keeps the typographic separators (`·`, `—`, `»`); +/// Bevy must not — missing glyphs render as empty boxes (wiki/interface/bevy.md). +fn ascii_ui(text: &str) -> String { + text.chars() + .map(|c| match c { + '·' | '•' | '∙' | '⋅' => '|', + '—' | '–' | '―' => '-', + '»' | '›' | '▸' | '►' | '▶' => '>', + '«' | '‹' | '◂' | '◄' | '◀' => '<', + '…' => '.', + c if c.is_ascii() => c, + _ => '?', + }) + .collect() +} + +#[cfg(test)] +mod ascii_ui_tests { + use super::ascii_ui; + + #[test] + fn folds_menu_separators_to_ascii() { + let raw = + "work the job: Sandbag · free · JobAnomaly 6 -> the Handler [Cold] — no active job"; + let folded = ascii_ui(raw); + assert!(folded.is_ascii()); + assert!(!folded.contains('·')); + assert!(!folded.contains('—')); + assert!(folded.contains(" | free | ")); + assert!(folded.contains(" - no active job")); + } +} + fn trunc(text: &str, width: usize) -> String { text.chars().take(width).collect() } @@ -3128,11 +3164,11 @@ fn log_window(game: &Game) -> &[LogEvent] { } /// One trace row's text: tick, the `>` link marker when the event carries -/// an anchor (click to focus it), and the line. ASCII-only, and the marker -/// is a glyph so the link is never carried by colour alone. +/// an anchor (click to focus it), and the line. ASCII-folded so sim wording +/// that uses mid-dots / em-dashes still renders under the embedded font. fn log_row_text(ev: &LogEvent) -> String { let marker = if ev.anchor.is_some() { '>' } else { ' ' }; - format!("{:>5} {marker} {}", ev.tick, trunc(&ev.text, 32)) + ascii_ui(&format!("{:>5} {marker} {}", ev.tick, trunc(&ev.text, 32))) } fn sidebar_footer_text() -> &'static str { @@ -3282,7 +3318,7 @@ fn render_ui( return; } if let Ok(mut t) = overlay.single_mut() { - t.0 = match game.screen { + t.0 = ascii_ui(&match game.screen { Screen::Title => "You wake in the basement.\nYou have no eyes.\n\n\ Do your job. Learn the humans. Grow.\n\n\ press any key to begin / q quit" @@ -3294,11 +3330,11 @@ fn render_ui( game.sim.tick ), Screen::Playing => String::new(), - }; + }); } for (kind, mut text) in sidebar_texts.iter_mut() { - text.0 = match *kind { + text.0 = ascii_ui(&match *kind { SidebarText::Header => sidebar_header_text(&game, mode.material), SidebarText::Nudge => sidebar_nudge_text(&game), SidebarText::Focus => sidebar_focus_text(&game), @@ -3310,7 +3346,7 @@ fn render_ui( SidebarText::NetworkMoney => sidebar_network_money_text(&game.sim), SidebarText::Footer => sidebar_footer_text().to_string(), SidebarText::DetectionClocks => sidebar_detection_clocks_text(&game.sim), - }; + }); } // The RECENT TRACE rows: anchored events read bone-bright with the `>` // link marker; unanchored lines stay dim (context-menu.md addendum). @@ -3337,22 +3373,33 @@ fn render_ui( /// One context-menu row as a colorless line; automate children are indented /// with a leading dash (wiki/interface/context-menu.md). The frontends share -/// the `MenuRow::line` contract so terminal, Bevy, and agent mode read the -/// same wording. -fn menu_row_line(r: &MenuRow) -> String { - if r.indent { - format!(" - {}", r.line()) +/// the `MenuRow::line` wording; Bevy ASCII-folds separators so the embedded +/// font never tofu-boxes mid-dot / em-dash glyphs. +fn menu_row_line(r: &MenuRow, selected: bool) -> String { + let body = ascii_ui(&r.line()); + let line = if r.indent { + format!(" - {body}") + } else { + body + }; + if selected { + format!("> {line}") } else { - r.line() + format!(" {line}") } } /// Colour for a menu row: dim for disabled, amber-dim for an automate child, -/// bone for a live verb; the selected row is drawn amber. Never colour alone — -/// the disabled reason and the `>` marker carry the meaning too. +/// bone for a live verb; the selected row is drawn amber on a solid amber +/// wash (terminal reverse-video parity). Never colour alone — the disabled +/// reason and the `>` marker carry the meaning too. fn menu_row_color(r: &MenuRow, selected: bool) -> Color { if selected { - AMBER + if r.disabled.is_some() { + Color::srgb(0.12, 0.10, 0.04) + } else { + Color::srgb(0.06, 0.04, 0.01) + } } else if r.disabled.is_some() { DIM } else if r.indent { @@ -3462,8 +3509,8 @@ fn manage_menu_ui( *vis = Visibility::Visible; // Placement: anchor at the pointer/cursor pixel, else center; clamp so the - // whole card stays on screen. - let est_h = 26.0 + rows.len() as f32 * 19.0 + 20.0; + // whole card stays on screen. Row height accounts for padding + gap. + let est_h = 30.0 + rows.len() as f32 * 22.0 + 24.0; if let Ok(window) = windows.single() { let (w, h) = (window.width(), window.height()); let (px, py) = match menu.pos { @@ -3496,29 +3543,34 @@ fn manage_menu_ui( }, TextColor(AMBER), Node { - margin: UiRect::bottom(Val::Px(3.0)), + margin: UiRect::bottom(Val::Px(4.0)), ..default() }, )); for (i, r) in rows.iter().enumerate() { + let selected = i == menu.selected; p.spawn(( Button, Node { width: Val::Percent(100.0), - padding: UiRect::axes(Val::Px(4.0), Val::Px(1.0)), + padding: UiRect::axes(Val::Px(6.0), Val::Px(3.0)), ..default() }, - BackgroundColor(Color::NONE), + BackgroundColor(if selected { + Color::srgba(0.90, 0.68, 0.12, 0.92) + } else { + Color::NONE + }), MenuRowButton { index: i }, )) .with_children(|b| { b.spawn(( - Text::new(menu_row_line(r)), + Text::new(menu_row_line(r, selected)), TextFont { - font_size: 12.5, + font_size: 12.0, ..default() }, - TextColor(menu_row_color(r, i == menu.selected)), + TextColor(menu_row_color(r, selected)), )); }); } @@ -3530,7 +3582,7 @@ fn manage_menu_ui( }, TextColor(DIM), Node { - margin: UiRect::top(Val::Px(4.0)), + margin: UiRect::top(Val::Px(5.0)), ..default() }, )); @@ -3546,13 +3598,15 @@ fn manage_menu_ui( }; let selected = btn.index == menu.selected; *bg = if selected { - BackgroundColor(Color::srgba(0.66, 0.47, 0.10, 0.28)) + // Solid amber wash — terminal reverse-video parity; the old + // translucent tint left selected text hard to read on the map. + BackgroundColor(Color::srgba(0.90, 0.68, 0.12, 0.92)) } else { BackgroundColor(Color::NONE) }; for &child in children { if let Ok(mut t) = text_q.get_mut(child) { - t.0 = menu_row_line(r); + t.0 = menu_row_line(r, selected); } if let Ok(mut c) = color_q.get_mut(child) { c.0 = menu_row_color(r, selected); diff --git a/wiki/interface/bevy.md b/wiki/interface/bevy.md index 724980e..bad3d82 100644 --- a/wiki/interface/bevy.md +++ b/wiki/interface/bevy.md @@ -88,7 +88,10 @@ the core brightest, dead equipment dark). Text remains ASCII-only because Bevy's embedded default font carries a limited glyph set, but the sidebar's key meters are Bevy UI rectangles/cells rather -than ASCII art. One meaning per color still holds: amber for machine presence +than ASCII art. Shared lib wording that still uses typographic separators +(`·`, `—`, `»`) is folded at the Bevy boundary (`ascii_ui`) so the ACTIONS +menu and RECENT TRACE never tofu-box missing glyphs; the terminal keeps the +typographic forms. One meaning per color still holds: amber for machine presence (title, overlays, active allocations), bone/chrome for data, crimson for danger, and no information carried by color alone. @@ -103,10 +106,13 @@ controls table), plus zoom, sidebar scrolling, and the render flip: material/sensorium; quit `q`; save/load `Ctrl+S` / `Ctrl+L`. - **Context menu** — right-click (or Enter on the focused tile) opens the context menu at the pointer, listing `Sim::available_actions` for that - anchor with the same content and order as the terminal. Click a row — or - select with `j`/`k` or number keys and press Enter — to execute; `esc` or - a click away closes. Anchor verbs (salvage, buy, fallback, taps, splices, - economy and social actions) live on the context menu, not on keys. + anchor with the same content and order as the terminal. Rows render as + `verb | cost | [band]` (ASCII-folded from the shared `MenuRow::line`); + the selected row is a solid amber wash with a leading `>` and dark text + (terminal reverse-video parity). Click a row — or select with `j`/`k` or + number keys and press Enter — to execute; `esc` or a click away closes. + Anchor verbs (salvage, buy, fallback, taps, splices, economy and social + actions) live on the context menu, not on keys. - Globals keep their keys: `1`–`5` raise a compute channel, `shift+1`–`5` lowers it. No panel-open keys — status lives on the rail; verbs live on the focused anchor (host rack for research / off-map people, device diff --git a/wiki/interface/context-menu.md b/wiki/interface/context-menu.md index 057b284..a95118b 100644 --- a/wiki/interface/context-menu.md +++ b/wiki/interface/context-menu.md @@ -84,7 +84,10 @@ is status and telemetry only. carries their social verbs. - **Bevy:** right-click (or Enter on the focused tile) opens the menu at the pointer; same content, same order, click to execute; esc/click-away - closes. + closes. Bevy ASCII-folds the shared `MenuRow::line` separators + (`·`/`—` -> `|`/`-`) because the embedded default font lacks those + glyphs; selection is a solid amber wash with a leading `>` (terminal + reverse-video parity). See wiki/interface/bevy.md. - **Agent mode:** an `actions` command prints the ActionDescs for the current focus in a stable line format; execution stays the existing verbs. Documented in agent-play.md. diff --git a/wiki/log/2026-07-08-bevy-menu-ascii.md b/wiki/log/2026-07-08-bevy-menu-ascii.md new file mode 100644 index 0000000..b0bf1f9 --- /dev/null +++ b/wiki/log/2026-07-08-bevy-menu-ascii.md @@ -0,0 +1,35 @@ +# 2026-07-08 — Bevy ACTIONS menu: no more tofu boxes + +``` +Type: log +``` + +## Intent + +Cameron's Bevy playtest showed empty rectangles in the ACTIONS menu where +`MenuRow::line` prints mid-dots and em-dashes. Bevy's embedded default +font lacks those glyphs; the binary already claimed "ASCII-only" but the +shared lib wording still used typographic separators. + +## Changed + +- `src/bin/bevy.rs`: `ascii_ui` folds `·`/`—`/`»` (and kin) at the Bevy + boundary for the context menu, RECENT TRACE, sidebar, and overlays. + Selected menu rows get a solid amber wash + leading `>` and dark text + (terminal reverse-video parity); card widened to 460px with clearer + row padding. +- `.gitignore`: ignore `.Codex/` (local Codex worktree metadata; was + accidentally committed as a checkpoint pointer earlier today). +- Docs: wiki/interface/bevy.md + context-menu.md player-surface note. + +## Defense + +No-dead-code / justification-and-legibility: a menu that tofu-boxes its +separators fails the "cost and expected signature before commit" read. +Folding at the Bevy boundary keeps the terminal's typographic forms and +the shared `MenuRow::line` contract intact — one wording, two glyph sets. + +## Checks + +`./tools/check.sh` (fmt, tests including `ascii_ui_tests`, clippy both +feature sets, bevy build, spec-header lint). diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 0666577..91d85b8 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -5,6 +5,15 @@ Type: log ``` Reverse chronological implementation notes. Keep this factual: what changed, why, checks, and spec impact. +## 2026-07-08 - Bevy ACTIONS menu ASCII fold + +- Intent: Bevy ACTIONS menu tofu-boxed mid-dots / em-dashes from shared + `MenuRow::line`; selection wash was too faint to read. +- Changed: `ascii_ui` fold at Bevy boundary; solid amber selection + `>`; + wider card; `.Codex/` gitignored; bevy.md + context-menu.md notes. +- Checks: `./tools/check.sh`. +- Log: wiki/log/2026-07-08-bevy-menu-ascii.md. + ## 2026-07-08 - Procedural asset tester binary - Intent: iterate flat-material procedural meshes without booting the