fix(ui): fix multi-level header suppression during scrolling

Implemented `show_sublabel` to ensure the first rendered entry in a
scrolled viewport always shows its full group labels.

Add regression tests for scrolling behavior in multi-level headers.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
Edward Langley
2026-06-09 21:43:14 -07:00
parent 9e02245f37
commit df9a02b2a9
+57 -6
View File
@@ -217,8 +217,7 @@ impl<'a> GridWidget<'a> {
let label = if layout.col_cats.is_empty() {
layout.col_label(ci)
} else {
let show = ci == 0 || data_col_items[ci][..=d] != data_col_items[ci - 1][..=d];
if show {
if show_sublabel(ci, col_offset, d, &data_col_items) {
data_col_items[ci][d].clone()
} else {
String::new()
@@ -340,9 +339,7 @@ impl<'a> GridWidget<'a> {
let label = if layout.row_cats.is_empty() {
layout.row_label(ri)
} else {
let show =
ri == 0 || data_row_items[ri][..=d] != data_row_items[ri - 1][..=d];
if show {
if show_sublabel(ri, row_offset, d, &data_row_items) {
data_row_items[ri][d].clone()
} else {
String::new()
@@ -649,6 +646,15 @@ pub fn compute_visible_cols(
count.max(1)
}
/// Decide whether the multi-level header sub-label at depth `d` for the data
/// entry `idx` should be rendered, or blanked because it repeats the previous
/// visible entry's prefix. `first_rendered` is the index of the first entry
/// actually on screen (the row/col scroll offset): that entry always shows its
/// full labels so a scrolled viewport keeps its group context.
fn show_sublabel(idx: usize, first_rendered: usize, d: usize, items: &[&Vec<String>]) -> bool {
idx == first_rendered || items[idx][..=d] != items[idx - 1][..=d]
}
// Re-export shared formatting functions
pub use crate::format::{format_f64, parse_number_format};
@@ -678,7 +684,7 @@ fn truncate(s: &str, max_width: usize) -> String {
mod tests {
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use super::GridWidget;
use super::{show_sublabel, GridWidget};
use crate::formula::parse_formula;
use crate::model::cell::{CellKey, CellValue};
use crate::ui::app::AppMode;
@@ -1060,4 +1066,49 @@ mod tests {
assert!(text.contains("2024"), "expected '2024' in:\n{text}");
assert!(text.contains("2025"), "expected '2025' in:\n{text}");
}
// ── Multi-level header repeat suppression ─────────────────────────────────
/// Items: [A,x], [A,y], [B,z] — multi-level header tuples.
fn sublabel_items() -> Vec<Vec<String>> {
[["A", "x"], ["A", "y"], ["B", "z"]]
.iter()
.map(|t| t.iter().map(|s| s.to_string()).collect())
.collect()
}
/// Bug (improvise-jjx): suppression compared against the entry at
/// `idx - 1` even when that entry is scrolled off-screen. The first
/// RENDERED entry must always show its full labels, otherwise scrolling
/// blanks the group label at the top of the viewport.
#[test]
fn first_rendered_entry_always_shows_sublabel_when_scrolled() {
let items = sublabel_items();
let refs: Vec<&Vec<String>> = items.iter().collect();
// Scrolled so index 1 ([A,y]) is the first visible entry. Its level-0
// prefix matches off-screen index 0 ([A,x]) — it must still show "A".
assert!(
show_sublabel(1, 1, 0, &refs),
"first rendered entry must show its label even when its prefix matches the off-screen entry above"
);
}
#[test]
fn repeated_prefix_below_first_rendered_entry_is_suppressed() {
let items = sublabel_items();
let refs: Vec<&Vec<String>> = items.iter().collect();
// Unscrolled: index 1 repeats index 0's level-0 prefix → suppressed.
assert!(!show_sublabel(1, 0, 0, &refs));
// Leaf level differs → shown.
assert!(show_sublabel(1, 0, 1, &refs));
}
#[test]
fn changed_prefix_is_always_shown() {
let items = sublabel_items();
let refs: Vec<&Vec<String>> = items.iter().collect();
assert!(show_sublabel(0, 0, 0, &refs));
assert!(show_sublabel(2, 0, 0, &refs)); // B != A
assert!(show_sublabel(2, 1, 0, &refs)); // scrolled, still differs
}
}