fix: scroll tile bar to keep selected tile visible

When there are more tiles than fit in the available width, the tile
bar now auto-scrolls to ensure the selected tile is always visible.
Overflow indicators (◀ ▶) show when tiles exist beyond the visible
area. Scroll offset is computed fresh each frame from tile_cat_idx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Edward Langley
2026-04-09 01:38:25 -07:00
parent 737d14a5c0
commit 4686f47026

View File

@ -52,16 +52,71 @@ impl<'a> Widget for TileBar<'a> {
None
};
let mut x = area.x + 1;
buf.set_string(area.x, area.y, " Tiles: ", Style::default().fg(Color::Gray));
x += 8;
let prefix = " Tiles: ";
let prefix_w = prefix.width() as u16;
buf.set_string(area.x, area.y, prefix, Style::default().fg(Color::Gray));
let cat_names: Vec<&str> = self.model.category_names();
for (i, cat_name) in cat_names.iter().enumerate() {
let (axis_symbol, axis_color) = TileBar::axis_display(view.axis_of(cat_name));
let label = format!(" [{cat_name} {axis_symbol}] ");
let is_selected = selected_cat_idx == Some(i);
// Compute label widths for all tiles
let labels: Vec<String> = cat_names
.iter()
.map(|cat_name| {
let (axis_symbol, _) = TileBar::axis_display(view.axis_of(cat_name));
format!(" [{cat_name} {axis_symbol}] ")
})
.collect();
let widths: Vec<u16> = labels.iter().map(|l| l.width() as u16).collect();
// Available space for tiles (after prefix)
let avail = area.width.saturating_sub(prefix_w);
// Find the minimal starting index so the selected tile is fully visible.
// We scroll by whole tiles: find the first tile to draw such that the
// selected tile fits within the available width.
let sel = selected_cat_idx.unwrap_or(0);
let mut start = 0;
loop {
// Check if selected tile is visible when starting from `start`
let mut used: u16 = 0;
let mut sel_visible = false;
for i in start..labels.len() {
if used + widths[i] > avail {
break;
}
used += widths[i];
if i == sel {
sel_visible = true;
}
}
if sel_visible || start >= sel {
break;
}
start += 1;
}
// Draw an overflow indicator if we scrolled past the beginning
let mut x = area.x + prefix_w + 1;
if start > 0 {
buf.set_string(
area.x + prefix_w,
area.y,
"",
Style::default().fg(Color::DarkGray),
);
x += 1;
}
// Render tiles from `start`
let mut last_drawn = start;
for i in start..labels.len() {
let label_w = widths[i];
if x + label_w > area.x + area.width {
break;
}
let (_, axis_color) = TileBar::axis_display(view.axis_of(cat_names[i]));
let is_selected = selected_cat_idx == Some(i);
let style = if is_selected {
Style::default()
.fg(Color::Black)
@ -71,12 +126,15 @@ impl<'a> Widget for TileBar<'a> {
Style::default().fg(axis_color)
};
let label_w = label.width() as u16;
if x + label_w > area.x + area.width {
break;
}
buf.set_string(x, area.y, &label, style);
buf.set_string(x, area.y, &labels[i], style);
x += label_w;
last_drawn = i;
}
// Draw overflow indicator if tiles remain after the visible area
if last_drawn + 1 < labels.len() && x < area.x + area.width {
buf.set_string(x, area.y, "", Style::default().fg(Color::DarkGray));
x += 1;
}
// Hint