feat(ui): flat filled rectangles for demo overlays (#22, ADR-0047 D4)

Render the keystroke badge and step caption as a solid yellow rectangle
with no border glyphs and a one-cell text margin, instead of a
rounded-border box — deliberately unlike the app's bordered panels so
the demo overlays read as a distinct, eye-catching callout. Shared
fill_overlay_rect helper (borderless Block fill + inset Paragraph).
Snapshots regenerated; ADR-0047 D4 wording updated.
This commit is contained in:
claude@clouddev1
2026-06-11 08:40:07 +00:00
parent 241f60c503
commit 2d0f4b2958
7 changed files with 76 additions and 71 deletions
+25 -16
View File
@@ -186,12 +186,21 @@ confirmed by test if live `--demo` on Windows is exercised.)*
### D4 — Both overlays are floating boxes at the output panel's inner bottom-right
The badge and the caption both render as **floating, bordered boxes
anchored to the inside of the output panel's bottom-right corner**
(inset one cell from the panel's inner edge), drawn **last over the base
render** — after modals, so they remain visible while the load-picker
(the `#24` cast) or any modal is up, and with **no layout reflow**
(consistent with the modal / nav-overlay precedent; honours R8).
The badge and the caption both render as **floating, flat filled
rectangles anchored to the inside of the output panel's bottom-right
corner** (inset one cell from the panel's inner edge), drawn **last over
the base render** — after modals, so they remain visible while the
load-picker (the `#24` cast) or any modal is up, and with **no layout
reflow** (consistent with the modal / nav-overlay precedent; honours
R8).
**Flat rectangle, not a bordered box (user decision, post-build).** The
overlays draw as a **solid yellow rectangle with no border glyphs** and
a one-cell margin around the text — deliberately *unlike* the app's
rounded-border panels, so they read as a distinct callout that "stands
out nicely" rather than as another panel. Implemented with a borderless
`Block` fill (the `paint_background` mechanism) plus a `Paragraph` inset
into a one-cell `Margin`.
The top-level `render()` does not currently know the output-panel rect
(it is computed inside `render_right_column`), so a **new field
@@ -204,19 +213,19 @@ When **both** are present, the **keystroke badge stacks directly above
the caption box** (both right-aligned in the corner) so they never
overlap.
**Styling — deliberately high-contrast:** **black text on a yellow
background**, bold, bordered — hard to overlook, identical in light and
dark themes (a fixed high-contrast pair centralised in `theme.rs`, not
theme-derived).
**Styling — deliberately high-contrast:** **bold black text on a yellow
fill** — hard to overlook, identical in light and dark themes (a fixed
high-contrast pair centralised in `theme.rs`, not theme-derived).
**Caption sizing (user-confirmed).** The caption is **word-wrapped to at
most 3 lines** within a content width of `min(40, output_inner_width
6)` columns, ellipsised beyond the third line. So the caption box is
**35 rows** tall (13 text rows + 2 border), its height varying with
the text — a full sentence fits without forcing the author to split it,
while the 3-line cap keeps it corner-sized. The **badge** box is always
a single short token (`[TAB]` `[SHIFT-TAB]`), so it is a fixed **3
rows** (1 text + 2 border), narrow.
4)` columns, ellipsised beyond the third line. So the caption rectangle
is **35 rows** tall (13 text rows + a one-cell margin top and bottom),
its height varying with the text — a full sentence fits without forcing
the author to split it, while the 3-line cap keeps it corner-sized. The
**badge** rectangle is always a single short token (`[TAB]`
`[SHIFT-TAB]`), so it is a fixed **3 rows** (1 text row + the margin),
narrow.
**Clamping (runda finding).** Stacked, the two boxes are up to 8 rows
(5 caption + 3 badge); the output panel's inner height is only `Min(5)`,
@@ -11,12 +11,12 @@ expression: snapshot
│ │
│ │
│ │
╭───────╮
[TAB]
╰───────╯
╭─────────────────────╮
Completing the name
╰─────────────────────╯
[TAB] │
Completing the name │
│ │
╰────────────────────────────────────────────────────────────────────────────────────────╯
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
@@ -14,9 +14,9 @@ expression: snapshot
│ │
│ │
│ │
╭─────────╮
[ENTER]
╰─────────╯
[ENTER] │
│ │
╰────────────────────────────────────────────────────────────────────────────────────────╯
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
@@ -14,9 +14,9 @@ expression: snapshot
│ │
│ │
│ │
╭───────╮
[TAB]
╰───────╯
[TAB] │
│ │
╰────────────────────────────────────────────────────────────────────────────────────────╯
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
@@ -14,9 +14,9 @@ expression: snapshot
│ │
│ │
│ │
╭──────────────────────────────────────────╮
Now press Tab to complete the table name
╰──────────────────────────────────────────╯
Now press Tab to complete the table name │
│ │
╰────────────────────────────────────────────────────────────────────────────────────────╯
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
@@ -12,11 +12,11 @@ expression: snapshot
│ │
│ │
│ │
╭──────────────────────────────────────────╮
This is a deliberately long step caption
that must wrap onto several lines and
then be clipped to three with an…
╰──────────────────────────────────────────╯
This is a deliberately long step caption │
that must wrap onto several lines and │
then be clipped to three with an… │
│ │
╰────────────────────────────────────────────────────────────────────────────────────────╯
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
+31 -35
View File
@@ -152,15 +152,31 @@ fn render_demo_overlays(app: &App, frame: &mut Frame<'_>) {
}
}
/// Paint a flat filled overlay rectangle — a solid yellow block with no
/// border glyphs (ADR-0047 D4) — and lay `body` inside a one-cell
/// margin. The borderless solid block is deliberately *unlike* the app's
/// bordered panels, so the demo overlays read as a distinct callout.
fn fill_overlay_rect(rect: Rect, body: String, frame: &mut Frame<'_>) {
frame.render_widget(ratatui::widgets::Clear, rect);
// `Block` with no borders fills the whole rect with the overlay
// background (same mechanism as `paint_background`).
frame.render_widget(Block::default().style(demo_overlay_style()), rect);
let inner = rect.inner(Margin {
horizontal: 1,
vertical: 1,
});
frame.render_widget(Paragraph::new(body).style(demo_overlay_style()), inner);
}
/// A small high-contrast keystroke badge (`[TAB]`, `[ENTER]`, …) inset
/// one cell from the bottom-right of `area` (ADR-0047 D2/D4). When a
/// caption box is present (`above`), the badge sits directly on top of
/// it, right-aligned; otherwise it takes the bottom-right corner.
/// Skipped rather than overflowing if it cannot fit.
/// one cell from the bottom-right of `area` (ADR-0047 D2/D4) — the label
/// on a flat yellow rectangle with a one-cell margin. When a caption box
/// is present (`above`), the badge sits directly on top of it, right
/// edges aligned; otherwise it takes the bottom-right corner. Skipped
/// rather than overflowing if it cannot fit.
fn render_badge_box(label: &str, area: Rect, above: Option<Rect>, frame: &mut Frame<'_>) {
// ` [LABEL] ` (one pad each side) inside a rounded border.
let box_w = label.chars().count() as u16 + 4;
let box_h = 3;
let box_w = label.chars().count() as u16 + 2; // one-cell margin each side
let box_h = 3; // text row + a margin row above and below
if box_w + 1 > area.width {
return;
}
@@ -180,34 +196,25 @@ fn render_badge_box(label: &str, area: Rect, above: Option<Rect>, frame: &mut Fr
area.y + area.height - box_h - 1
}
};
let rect = Rect { x, y, width: box_w, height: box_h };
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(demo_overlay_style());
let para = Paragraph::new(format!(" {label} "))
.style(demo_overlay_style())
.block(block);
frame.render_widget(ratatui::widgets::Clear, rect);
frame.render_widget(para, rect);
fill_overlay_rect(Rect { x, y, width: box_w, height: box_h }, label.to_string(), frame);
}
/// A step-caption box inset one cell from the bottom-right of `area`
/// (ADR-0047 D3/D4): the text word-wrapped to at most 3 lines within a
/// corner-sized width, bold black on yellow. Returns the rect it drew,
/// or `None` if it was too small to place (so the badge can fall back to
/// the bottom-right corner).
/// corner-sized width, bold black on a flat yellow rectangle. Returns
/// the rect it drew, or `None` if it was too small to place (so the
/// badge can fall back to the bottom-right corner).
fn render_caption_box(text: &str, area: Rect, frame: &mut Frame<'_>) -> Option<Rect> {
// Content width capped so the box stays corner-sized; the caption
// wraps to ≤ 3 lines and ellipsises beyond (D4).
let content_w = 40.min(area.width.saturating_sub(6)) as usize;
let content_w = 40.min(area.width.saturating_sub(4)) as usize;
if content_w < 4 {
return None; // output too narrow for a useful caption
}
let lines = clamp_wrapped(text, content_w, 3);
let inner_w = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
let box_w = inner_w as u16 + 4; // 2 border + 1 pad each side
let box_h = lines.len() as u16 + 2; // 2 border
let box_w = inner_w as u16 + 2; // one-cell margin each side
let box_h = lines.len() as u16 + 2; // text rows + a margin row above and below
if box_w + 1 > area.width || box_h + 1 > area.height {
return None;
}
@@ -217,18 +224,7 @@ fn render_caption_box(text: &str, area: Rect, frame: &mut Frame<'_>) -> Option<R
width: box_w,
height: box_h,
};
let body = lines
.iter()
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n");
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(demo_overlay_style());
let para = Paragraph::new(body).style(demo_overlay_style()).block(block);
frame.render_widget(ratatui::widgets::Clear, rect);
frame.render_widget(para, rect);
fill_overlay_rect(rect, lines.join("\n"), frame);
Some(rect)
}