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:
+57
-6
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user