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:
@@ -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
|
### 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
|
The badge and the caption both render as **floating, flat filled
|
||||||
anchored to the inside of the output panel's bottom-right corner**
|
rectangles anchored to the inside of the output panel's bottom-right
|
||||||
(inset one cell from the panel's inner edge), drawn **last over the base
|
corner** (inset one cell from the panel's inner edge), drawn **last over
|
||||||
render** — after modals, so they remain visible while the load-picker
|
the base render** — after modals, so they remain visible while the
|
||||||
(the `#24` cast) or any modal is up, and with **no layout reflow**
|
load-picker (the `#24` cast) or any modal is up, and with **no layout
|
||||||
(consistent with the modal / nav-overlay precedent; honours R8).
|
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
|
The top-level `render()` does not currently know the output-panel rect
|
||||||
(it is computed inside `render_right_column`), so a **new field
|
(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
|
the caption box** (both right-aligned in the corner) so they never
|
||||||
overlap.
|
overlap.
|
||||||
|
|
||||||
**Styling — deliberately high-contrast:** **black text on a yellow
|
**Styling — deliberately high-contrast:** **bold black text on a yellow
|
||||||
background**, bold, bordered — hard to overlook, identical in light and
|
fill** — hard to overlook, identical in light and dark themes (a fixed
|
||||||
dark themes (a fixed high-contrast pair centralised in `theme.rs`, not
|
high-contrast pair centralised in `theme.rs`, not theme-derived).
|
||||||
theme-derived).
|
|
||||||
|
|
||||||
**Caption sizing (user-confirmed).** The caption is **word-wrapped to at
|
**Caption sizing (user-confirmed).** The caption is **word-wrapped to at
|
||||||
most 3 lines** within a content width of `min(40, output_inner_width −
|
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
|
4)` columns, ellipsised beyond the third line. So the caption rectangle
|
||||||
**3–5 rows** tall (1–3 text rows + 2 border), its height varying with
|
is **3–5 rows** tall (1–3 text rows + a one-cell margin top and bottom),
|
||||||
the text — a full sentence fits without forcing the author to split it,
|
its height varying with the text — a full sentence fits without forcing
|
||||||
while the 3-line cap keeps it corner-sized. The **badge** box is always
|
the author to split it, while the 3-line cap keeps it corner-sized. The
|
||||||
a single short token (`[TAB]` … `[SHIFT-TAB]`), so it is a fixed **3
|
**badge** rectangle is always a single short token (`[TAB]` …
|
||||||
rows** (1 text + 2 border), narrow.
|
`[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
|
**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)`,
|
(5 caption + 3 badge); the output panel's inner height is only `Min(5)`,
|
||||||
|
|||||||
+6
-6
@@ -11,12 +11,12 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ ╭───────╮ │
|
│ │
|
||||||
│ │ [TAB] │ │
|
│ [TAB] │
|
||||||
│ ╰───────╯ │
|
│ │
|
||||||
│ ╭─────────────────────╮ │
|
│ │
|
||||||
│ │ Completing the name │ │
|
│ Completing the name │
|
||||||
│ ╰─────────────────────╯ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ ╭─────────╮ │
|
│ │
|
||||||
│ │ [ENTER] │ │
|
│ [ENTER] │
|
||||||
│ ╰─────────╯ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ ╭───────╮ │
|
│ │
|
||||||
│ │ [TAB] │ │
|
│ [TAB] │
|
||||||
│ ╰───────╯ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ ╭──────────────────────────────────────────╮ │
|
│ │
|
||||||
│ │ Now press Tab to complete the table name │ │
|
│ Now press Tab to complete the table name │
|
||||||
│ ╰──────────────────────────────────────────╯ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ ╭──────────────────────────────────────────╮ │
|
│ │
|
||||||
│ │ This is a deliberately long step caption │ │
|
│ This is a deliberately long step caption │
|
||||||
│ │ that must wrap onto several lines and │ │
|
│ that must wrap onto several lines and │
|
||||||
│ │ then be clipped to three with an… │ │
|
│ then be clipped to three with an… │
|
||||||
│ ╰──────────────────────────────────────────╯ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
|||||||
@@ -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
|
/// A small high-contrast keystroke badge (`[TAB]`, `[ENTER]`, …) inset
|
||||||
/// one cell from the bottom-right of `area` (ADR-0047 D2/D4). When a
|
/// one cell from the bottom-right of `area` (ADR-0047 D2/D4) — the label
|
||||||
/// caption box is present (`above`), the badge sits directly on top of
|
/// on a flat yellow rectangle with a one-cell margin. When a caption box
|
||||||
/// it, right-aligned; otherwise it takes the bottom-right corner.
|
/// is present (`above`), the badge sits directly on top of it, right
|
||||||
/// Skipped rather than overflowing if it cannot fit.
|
/// 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<'_>) {
|
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 + 2; // one-cell margin each side
|
||||||
let box_w = label.chars().count() as u16 + 4;
|
let box_h = 3; // text row + a margin row above and below
|
||||||
let box_h = 3;
|
|
||||||
if box_w + 1 > area.width {
|
if box_w + 1 > area.width {
|
||||||
return;
|
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
|
area.y + area.height - box_h - 1
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let rect = Rect { x, y, width: box_w, height: box_h };
|
fill_overlay_rect(Rect { x, y, width: box_w, height: box_h }, label.to_string(), frame);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A step-caption box inset one cell from the bottom-right of `area`
|
/// 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
|
/// (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,
|
/// corner-sized width, bold black on a flat yellow rectangle. Returns
|
||||||
/// or `None` if it was too small to place (so the badge can fall back to
|
/// the rect it drew, or `None` if it was too small to place (so the
|
||||||
/// the bottom-right corner).
|
/// badge can fall back to the bottom-right corner).
|
||||||
fn render_caption_box(text: &str, area: Rect, frame: &mut Frame<'_>) -> Option<Rect> {
|
fn render_caption_box(text: &str, area: Rect, frame: &mut Frame<'_>) -> Option<Rect> {
|
||||||
// Content width capped so the box stays corner-sized; the caption
|
// Content width capped so the box stays corner-sized; the caption
|
||||||
// wraps to ≤ 3 lines and ellipsises beyond (D4).
|
// 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 {
|
if content_w < 4 {
|
||||||
return None; // output too narrow for a useful caption
|
return None; // output too narrow for a useful caption
|
||||||
}
|
}
|
||||||
let lines = clamp_wrapped(text, content_w, 3);
|
let lines = clamp_wrapped(text, content_w, 3);
|
||||||
let inner_w = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
|
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_w = inner_w as u16 + 2; // one-cell margin each side
|
||||||
let box_h = lines.len() as u16 + 2; // 2 border
|
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 {
|
if box_w + 1 > area.width || box_h + 1 > area.height {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -217,18 +224,7 @@ fn render_caption_box(text: &str, area: Rect, frame: &mut Frame<'_>) -> Option<R
|
|||||||
width: box_w,
|
width: box_w,
|
||||||
height: box_h,
|
height: box_h,
|
||||||
};
|
};
|
||||||
let body = lines
|
fill_overlay_rect(rect, lines.join("\n"), frame);
|
||||||
.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);
|
|
||||||
Some(rect)
|
Some(rect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user