feat(ui): two-row input display on tall terminals (#23, ADR-0046 DA4)
On a comfortable terminal (height >= 40) the input panel shows two rows: the single logical command soft-wraps across them — the first row stops 6 columns short for the ADR-0027 validity indicator, the second uses the full width — so a medium command is fully visible without horizontal scrolling. A line longer than both rows still scrolls (DA3-style, one column each side reserved for < / > markers) to keep the cursor visible. hint_rows generalises to panel_heights(area) -> (input_rows, hint_rows): compact (<40) stays input 1 / hint 2; comfortable becomes input 2, degrading hint-then-input on tiny terminals to protect the output Min(5). render_input_panel splits into render_input_one_row (the existing DA3 path, unchanged) and render_input_two_rows, with a new expand_runs_to_cells helper placing styled cells across the rows. Tests: panel_heights geometry, two-row wrap, overflow-scroll, the indicator-stays-on-the-first-row case, and a two-row layout snapshot. Compact one-row snapshots are byte-identical (that path is untouched).
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
---
|
||||
source: src/ui.rs
|
||||
assertion_line: 2210
|
||||
expression: snapshot
|
||||
---
|
||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||
│(none yet) ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||
│ ││select * from Customers where id = 12345 and │
|
||||
│ ││ name = 'Alice Wonderland' │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ Hint ────────────────────────────────────────────╮
|
||||
│ ││`select` is SQL — available in advanced mode. │
|
||||
│ ││Switch with `mode advanced`, or prefix the line │
|
||||
│ ││with `:` to run it once. │
|
||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||
Project: Term Planner
|
||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||
@@ -513,24 +513,25 @@ const COMFORTABLE_MIN_HEIGHT: u16 = 40;
|
||||
/// measured against `src/friendly/strings/en-US.yaml`).
|
||||
const HINT_THIRD_ROW_MAX_INNER: u16 = 54;
|
||||
|
||||
/// Hint-panel content-row count as a pure function of the right
|
||||
/// column's geometry (ADR-0046 DA1/DA2) — NOT of the hint text. That is
|
||||
/// the whole point of #20: a height fixed per terminal size cannot jump
|
||||
/// as the user types. Add 2 for the panel borders.
|
||||
/// Input- and hint-panel content-row counts as a pure function of the
|
||||
/// right column's geometry (ADR-0046 DA1/DA2/DA4) — NOT of the hint or
|
||||
/// input text. That is the point of #20: heights fixed per terminal
|
||||
/// size cannot jump as the user types. Returns `(input_rows,
|
||||
/// hint_rows)`; add 2 to each for the panel borders.
|
||||
///
|
||||
/// - Compact height (`< COMFORTABLE_MIN_HEIGHT`): 2 rows.
|
||||
/// - Comfortable height: 2 rows, or 3 when the column is narrow enough
|
||||
/// (`inner < HINT_THIRD_ROW_MAX_INNER`) that the longest hint needs a
|
||||
/// third line.
|
||||
/// - Compact height (`< COMFORTABLE_MIN_HEIGHT`): input 1 row, hint 2.
|
||||
/// - Comfortable height: input 2 rows (DA4 two-row display); hint 2, or
|
||||
/// 3 when the column is narrow enough (`inner <
|
||||
/// HINT_THIRD_ROW_MAX_INNER`) that the longest hint needs a third
|
||||
/// line.
|
||||
/// - Degradation: on a terminal too short to honour the output panel's
|
||||
/// `Min(5)` plus the fixed input (3) and hint panels, the hint
|
||||
/// shrinks toward 1 so the output keeps its floor.
|
||||
///
|
||||
/// (DA4 will fold the input panel's height in here too; today the input
|
||||
/// is a fixed 3 rows, reflected in the degradation arithmetic.)
|
||||
const fn hint_rows(area: Rect) -> u16 {
|
||||
/// `Min(5)` plus both panels, the hint shrinks first, then the input,
|
||||
/// so the output keeps its floor.
|
||||
const fn panel_heights(area: Rect) -> (u16, u16) {
|
||||
let comfortable = area.height >= COMFORTABLE_MIN_HEIGHT;
|
||||
let inner_w = area.width.saturating_sub(2);
|
||||
let mut hint_c: u16 = if area.height < COMFORTABLE_MIN_HEIGHT {
|
||||
let mut input_c: u16 = if comfortable { 2 } else { 1 };
|
||||
let mut hint_c: u16 = if !comfortable {
|
||||
2
|
||||
} else if inner_w < HINT_THIRD_ROW_MAX_INNER {
|
||||
MAX_HINT_ROWS as u16
|
||||
@@ -538,11 +539,18 @@ const fn hint_rows(area: Rect) -> u16 {
|
||||
2
|
||||
};
|
||||
// Honour the output panel's Min(5) first on a very short terminal:
|
||||
// 5 (output) + 3 (input panel) + (hint_c + 2) must fit in the column.
|
||||
while 5 + 3 + (hint_c + 2) > area.height && hint_c > 1 {
|
||||
// 5 (output) + (input_c + 2) + (hint_c + 2) must fit in the column.
|
||||
// Shrink the hint first, then the input.
|
||||
while 5 + (input_c + 2) + (hint_c + 2) > area.height {
|
||||
if hint_c > 1 {
|
||||
hint_c -= 1;
|
||||
} else if input_c > 1 {
|
||||
input_c -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
hint_c
|
||||
}
|
||||
(input_c, hint_c)
|
||||
}
|
||||
|
||||
/// Word-wrap `text` to `width`, then clamp to at most `max_rows`
|
||||
@@ -603,14 +611,14 @@ fn render_right_column(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area
|
||||
// input/output panels (#20). The hint is then clamped to that fixed
|
||||
// row budget. The hint panel spans the full column width, so
|
||||
// `area.width` is its width too.
|
||||
let hint_c = hint_rows(area);
|
||||
let (input_c, hint_c) = panel_heights(area);
|
||||
let hint_lines = resolve_hint_lines(app, theme, area.width, hint_c as usize);
|
||||
|
||||
let rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(5), // Output panel
|
||||
Constraint::Length(3), // Input panel (DA4 will size this)
|
||||
Constraint::Length(input_c + 2), // Input panel (1 row, or 2 when tall)
|
||||
Constraint::Length(hint_c + 2), // Hint panel (geometry-fixed)
|
||||
])
|
||||
.split(area);
|
||||
@@ -1029,51 +1037,82 @@ fn render_input_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area:
|
||||
.title(title)
|
||||
.style(Style::default().bg(theme.bg).fg(theme.fg));
|
||||
|
||||
// Cursor block: render the character at the cursor position
|
||||
// inverted so the cursor is visible without enabling a real
|
||||
// terminal cursor.
|
||||
//
|
||||
// Per-token colouring (ADR-0022 §3 / ADR-0030 §8) in both
|
||||
// modes — `render_input_runs_in_mode` runs the highlight
|
||||
// walker with the active mode so SQL keywords / operators /
|
||||
// CASE / function calls colour correctly in Advanced mode.
|
||||
let cursor = app.input_cursor.min(app.input.len());
|
||||
|
||||
// ADR-0027 §4: the rightmost six columns of the input row
|
||||
// (a five-column label plus a one-column gap) are reserved
|
||||
// unconditionally, so the text area is always
|
||||
// `inner.width - 6` and the typed command never shifts
|
||||
// sideways when the validity indicator appears or hides.
|
||||
// unconditionally, so the first row's text area is always
|
||||
// `inner.width - 6` and the typed command never shifts sideways
|
||||
// when the validity indicator appears or hides. A two-row input
|
||||
// (DA4) lets the *second* row use the full width.
|
||||
let inner = block.inner(area);
|
||||
let text_area = Rect {
|
||||
width: inner.width.saturating_sub(6),
|
||||
..inner
|
||||
};
|
||||
|
||||
// ADR-0046 DA3: horizontally scroll a long line so the cursor stays
|
||||
// visible, rather than clipping it off the right edge silently.
|
||||
// Computed (and the offset stored) *before* the highlight spans
|
||||
// borrow `app.input`, so the `&mut app` write does not clash.
|
||||
let line_cols = app.input.chars().count();
|
||||
let cursor_col = app.input[..cursor].chars().count();
|
||||
let tw = text_area.width as usize;
|
||||
let offset = input_scroll_offset(line_cols, cursor_col, tw, app.input_scroll_offset);
|
||||
app.input_scroll_offset = offset;
|
||||
|
||||
frame.render_widget(block, area);
|
||||
|
||||
// Per-token colouring (ADR-0022 §3 / ADR-0030 §8) in both modes —
|
||||
// `render_input_runs_in_mode` runs the highlight walker with the
|
||||
// active mode so SQL keywords / operators / CASE / function calls
|
||||
// colour correctly in Advanced mode. The cursor cell is rendered
|
||||
// inverted (an empty-range run) so it is visible without a real
|
||||
// terminal cursor.
|
||||
// the highlight walker runs with the active mode so SQL keywords /
|
||||
// operators / CASE / function calls colour correctly. The cursor
|
||||
// cell is rendered inverted (an empty-range run) so it is visible
|
||||
// without a real terminal cursor.
|
||||
let mode_for_render = match effective {
|
||||
EffectiveMode::Simple => crate::mode::Mode::Simple,
|
||||
EffectiveMode::AdvancedPersistent | EffectiveMode::AdvancedOneShot => {
|
||||
crate::mode::Mode::Advanced
|
||||
}
|
||||
};
|
||||
|
||||
frame.render_widget(block, area);
|
||||
|
||||
// ADR-0046 DA3/DA4: render the single logical line across one row
|
||||
// (compact terminals) or two (comfortable, height ≥ 40), scrolling
|
||||
// horizontally in either case so the cursor stays visible.
|
||||
if inner.height >= 2 {
|
||||
render_input_two_rows(app, theme, frame, inner, text_area, cursor, mode_for_render);
|
||||
} else {
|
||||
render_input_one_row(app, theme, frame, text_area, cursor, mode_for_render);
|
||||
}
|
||||
|
||||
if let Some(severity) = app.input_indicator {
|
||||
let (indicator_label, color) = match severity {
|
||||
crate::dsl::walker::Severity::Error => ("[ERR]", theme.error),
|
||||
crate::dsl::walker::Severity::Warning => ("[WRN]", theme.warning),
|
||||
};
|
||||
let label_area = Rect {
|
||||
x: inner.x + inner.width.saturating_sub(5),
|
||||
width: 5.min(inner.width),
|
||||
..inner
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(Span::styled(
|
||||
indicator_label,
|
||||
Style::default().fg(color).add_modifier(Modifier::BOLD),
|
||||
))),
|
||||
label_area,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// One-row input rendering (ADR-0046 DA3): the single logical line is
|
||||
/// horizontally scrolled so the cursor stays visible, with `<` / `>`
|
||||
/// markers (muted) at the reserved edge columns signalling hidden
|
||||
/// content. The offset is stored *before* the highlight spans borrow
|
||||
/// `app.input`, so the `&mut app` write does not clash.
|
||||
fn render_input_one_row(
|
||||
app: &mut App,
|
||||
theme: &Theme,
|
||||
frame: &mut Frame<'_>,
|
||||
text_area: Rect,
|
||||
cursor: usize,
|
||||
mode_for_render: crate::mode::Mode,
|
||||
) {
|
||||
let line_cols = app.input.chars().count();
|
||||
let cursor_col = app.input[..cursor].chars().count();
|
||||
let tw = text_area.width as usize;
|
||||
let offset = input_scroll_offset(line_cols, cursor_col, tw, app.input_scroll_offset);
|
||||
app.input_scroll_offset = offset;
|
||||
|
||||
let runs = crate::input_render::render_input_runs_in_mode(
|
||||
&app.input,
|
||||
cursor,
|
||||
@@ -1117,25 +1156,126 @@ fn render_input_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area:
|
||||
} else {
|
||||
frame.render_widget(Paragraph::new(Line::from(spans)), text_area);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(severity) = app.input_indicator {
|
||||
let (indicator_label, color) = match severity {
|
||||
crate::dsl::walker::Severity::Error => ("[ERR]", theme.error),
|
||||
crate::dsl::walker::Severity::Warning => ("[WRN]", theme.warning),
|
||||
};
|
||||
let label_area = Rect {
|
||||
x: inner.x + inner.width.saturating_sub(5),
|
||||
width: 5.min(inner.width),
|
||||
..inner
|
||||
/// Two-row input rendering (ADR-0046 DA4): on a comfortable terminal the
|
||||
/// single logical line is soft-wrapped across two visual rows — the
|
||||
/// first row stops 6 columns short (the ADR-0027 indicator reserve), the
|
||||
/// second uses the full width. When the line overflows both rows it
|
||||
/// scrolls horizontally (one column each side reserved for `<` / `>`
|
||||
/// markers) so the cursor stays visible. `text_area` is the first
|
||||
/// (narrower) row; `inner` spans both rows.
|
||||
fn render_input_two_rows(
|
||||
app: &mut App,
|
||||
theme: &Theme,
|
||||
frame: &mut Frame<'_>,
|
||||
inner: Rect,
|
||||
text_area: Rect,
|
||||
cursor: usize,
|
||||
mode_for_render: crate::mode::Mode,
|
||||
) {
|
||||
let row0_w = text_area.width as usize; // first row reserves the indicator
|
||||
let row1_w = inner.width as usize; // second row uses the full width
|
||||
let capacity = row0_w + row1_w;
|
||||
let line_cols = app.input.chars().count();
|
||||
let cursor_col = app.input[..cursor].chars().count();
|
||||
let offset = input_scroll_offset(line_cols, cursor_col, capacity, app.input_scroll_offset);
|
||||
app.input_scroll_offset = offset;
|
||||
|
||||
let runs = crate::input_render::render_input_runs_in_mode(
|
||||
&app.input,
|
||||
cursor,
|
||||
theme,
|
||||
&app.schema_cache,
|
||||
mode_for_render,
|
||||
);
|
||||
let cells = expand_runs_to_cells(&app.input, &runs);
|
||||
let len = cells.len();
|
||||
|
||||
// Overflowing both rows reserves a marker column on each row's
|
||||
// outer edge; otherwise both rows use their full text width.
|
||||
let overflow = line_cols >= capacity;
|
||||
let row0_text_w = if overflow { row0_w.saturating_sub(1) } else { row0_w };
|
||||
let row1_text_w = if overflow { row1_w.saturating_sub(1) } else { row1_w };
|
||||
let eff_cap = row0_text_w + row1_text_w;
|
||||
|
||||
let start = offset.min(len);
|
||||
let end = (offset + eff_cap).min(len);
|
||||
let window = &cells[start..end];
|
||||
let split = row0_text_w.min(window.len());
|
||||
let to_line = |cs: &[(String, Style)]| {
|
||||
Line::from(
|
||||
cs.iter()
|
||||
.map(|(s, st)| Span::styled(s.clone(), *st))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
|
||||
let row0_x = if overflow { text_area.x + 1 } else { text_area.x };
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(Span::styled(
|
||||
indicator_label,
|
||||
Style::default().fg(color).add_modifier(Modifier::BOLD),
|
||||
))),
|
||||
label_area,
|
||||
Paragraph::new(to_line(&window[..split])),
|
||||
Rect {
|
||||
x: row0_x,
|
||||
y: inner.y,
|
||||
width: row0_text_w as u16,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
frame.render_widget(
|
||||
Paragraph::new(to_line(&window[split..])),
|
||||
Rect {
|
||||
x: inner.x,
|
||||
y: inner.y + 1,
|
||||
width: row1_text_w as u16,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
|
||||
let marker = Style::default().fg(theme.muted);
|
||||
if overflow && offset > 0 {
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled("<", marker)),
|
||||
Rect {
|
||||
x: text_area.x,
|
||||
y: inner.y,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
if overflow && end < len {
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(">", marker)),
|
||||
Rect {
|
||||
x: inner.x + inner.width.saturating_sub(1),
|
||||
y: inner.y + 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand styled runs into one owned `(grapheme, style)` cell per
|
||||
/// display column, including the inverted cursor cell (ADR-0046 DA4).
|
||||
/// The two-row renderer places cells across two visual rows and so
|
||||
/// needs them individually rather than as byte-range spans.
|
||||
fn expand_runs_to_cells(
|
||||
input: &str,
|
||||
runs: &[crate::input_render::StyledRun],
|
||||
) -> Vec<(String, Style)> {
|
||||
let mut cells = Vec::new();
|
||||
for r in runs {
|
||||
if r.byte_range.0 == r.byte_range.1 {
|
||||
// Cursor sentinel (empty range) → inverted space cell.
|
||||
cells.push((" ".to_string(), r.style));
|
||||
} else {
|
||||
for ch in input[r.byte_range.0..r.byte_range.1].chars() {
|
||||
cells.push((ch.to_string(), r.style));
|
||||
}
|
||||
}
|
||||
}
|
||||
cells
|
||||
}
|
||||
|
||||
/// Convert `StyledRun`s into ratatui `Span`s borrowed from
|
||||
@@ -1941,22 +2081,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_rows_is_geometry_driven() {
|
||||
// ADR-0046 DA1/DA2: the pure helper that the renderer and these
|
||||
// tests share. Height picks the bucket; width gates the 3rd row;
|
||||
// a tiny terminal degrades toward 1 to protect output `Min(5)`.
|
||||
let at = |w: u16, h: u16| hint_rows(Rect::new(0, 0, w, h));
|
||||
// Compact height → always 2, regardless of width.
|
||||
assert_eq!(at(90, 25), 2);
|
||||
assert_eq!(at(40, 25), 2);
|
||||
// Comfortable + wide (inner ≥ 54) → 2.
|
||||
assert_eq!(at(90, 45), 2);
|
||||
assert_eq!(at(56, 45), 2); // inner == 54 is "wide enough"
|
||||
// Comfortable + narrow (inner < 54) → 3.
|
||||
assert_eq!(at(55, 45), 3); // inner == 53
|
||||
assert_eq!(at(50, 45), 3);
|
||||
// Very short terminal degrades the hint to protect output Min(5).
|
||||
assert_eq!(at(90, 11), 1);
|
||||
fn panel_heights_are_geometry_driven() {
|
||||
// ADR-0046 DA1/DA2/DA4: the pure helper the renderer and these
|
||||
// tests share. Height picks the bucket (input 1→2, hint floor);
|
||||
// width gates the hint's 3rd row; a tiny terminal degrades hint
|
||||
// then input to protect output `Min(5)`.
|
||||
let at = |w: u16, h: u16| panel_heights(Rect::new(0, 0, w, h));
|
||||
// Compact height → input 1, hint 2, regardless of width.
|
||||
assert_eq!(at(90, 25), (1, 2));
|
||||
assert_eq!(at(40, 25), (1, 2));
|
||||
// Comfortable height → input 2; hint 2 when wide (inner ≥ 54).
|
||||
assert_eq!(at(90, 45), (2, 2));
|
||||
assert_eq!(at(56, 45), (2, 2)); // inner == 54 is "wide enough"
|
||||
// Comfortable + narrow (inner < 54) → hint 3.
|
||||
assert_eq!(at(55, 45), (2, 3)); // inner == 53
|
||||
assert_eq!(at(50, 45), (2, 3));
|
||||
// Very short terminal degrades hint first, then input, to keep
|
||||
// the output panel's Min(5).
|
||||
assert_eq!(at(90, 11), (1, 1));
|
||||
}
|
||||
|
||||
// ---- ADR-0046 DA3: input horizontal scroll -------------------
|
||||
@@ -2016,6 +2158,87 @@ mod tests {
|
||||
assert!(!out.contains("Wonderland"), "the tail must be scrolled off:\n{out}");
|
||||
}
|
||||
|
||||
// ---- ADR-0046 DA4: two-row input on tall terminals -----------
|
||||
|
||||
#[test]
|
||||
fn comfortable_terminal_wraps_input_across_two_rows() {
|
||||
// On a tall (height ≥ 40) terminal the input shows two rows, so
|
||||
// a medium command wraps instead of scrolling — the whole
|
||||
// command is visible at once, head above tail.
|
||||
let mut app = App::new();
|
||||
app.input.push_str(LONG_INPUT);
|
||||
app.input_cursor = app.input.len();
|
||||
let theme = Theme::dark();
|
||||
let out = render_to_string(&mut app, &theme, 80, 44);
|
||||
let head = out
|
||||
.lines()
|
||||
.position(|l| l.contains("select * from Customers"));
|
||||
let tail = out.lines().position(|l| l.contains("'Alice Wonderland'"));
|
||||
assert!(
|
||||
head.is_some() && tail.is_some(),
|
||||
"both head and tail are visible across two rows:\n{out}"
|
||||
);
|
||||
assert!(
|
||||
tail.unwrap() > head.unwrap(),
|
||||
"the tail wraps onto a row below the head:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_row_input_scrolls_when_it_overflows_both_rows() {
|
||||
// A narrow-but-tall terminal: two rows, but the line is longer
|
||||
// than both can hold, so it scrolls to keep the tail/cursor
|
||||
// visible with a `<` marker for the hidden head.
|
||||
let mut app = App::new();
|
||||
app.input.push_str(LONG_INPUT);
|
||||
app.input_cursor = app.input.len();
|
||||
let theme = Theme::dark();
|
||||
let out = render_to_string(&mut app, &theme, 50, 44);
|
||||
assert!(out.contains("Wonderland"), "the tail/cursor stays visible:\n{out}");
|
||||
assert!(out.contains('<'), "a left marker signals the hidden head:\n{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_row_input_keeps_the_indicator_on_the_first_row() {
|
||||
// ADR-0046 DA4 / ADR-0027: the [ERR]/[WRN] indicator stays
|
||||
// anchored to the *first* input row (whose 6-column reserve it
|
||||
// occupies); the wrapped tail on the second row is untouched.
|
||||
let mut app = App::new();
|
||||
app.input.push_str(LONG_INPUT);
|
||||
app.input_cursor = app.input.len();
|
||||
app.input_indicator = Some(crate::dsl::walker::Severity::Error);
|
||||
let theme = Theme::dark();
|
||||
let out = render_to_string(&mut app, &theme, 80, 44);
|
||||
let err_line = out
|
||||
.lines()
|
||||
.position(|l| l.contains("[ERR]"))
|
||||
.expect("indicator visible");
|
||||
let head_line = out
|
||||
.lines()
|
||||
.position(|l| l.contains("select * from Customers"))
|
||||
.expect("head visible");
|
||||
assert_eq!(
|
||||
err_line, head_line,
|
||||
"the indicator shares the first input row with the head:\n{out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("'Alice Wonderland'"),
|
||||
"the wrapped tail on the second row is intact:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_row_input_snapshot() {
|
||||
// Locks the DA4 two-row layout: head on the first (indicator-
|
||||
// reserved) row, tail on the full-width second row.
|
||||
let mut app = App::new();
|
||||
app.input.push_str(LONG_INPUT);
|
||||
app.input_cursor = app.input.len();
|
||||
let theme = Theme::dark();
|
||||
let snapshot = render_to_string(&mut app, &theme, 80, 44);
|
||||
insta::assert_snapshot!("two_row_input_dark", snapshot);
|
||||
}
|
||||
|
||||
/// Count the content rows inside the Hint panel of a rendered
|
||||
/// screen: the rows between the `╭ Hint …` title border and the
|
||||
/// next `╰…╯` bottom border.
|
||||
|
||||
Reference in New Issue
Block a user