From 4686f47026e54fb0b05043aedd3a6bf5cb8c9e4e Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Thu, 9 Apr 2026 01:38:25 -0700 Subject: [PATCH] fix: scroll tile bar to keep selected tile visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/ui/tile_bar.rs | 82 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/src/ui/tile_bar.rs b/src/ui/tile_bar.rs index da7f433..42fe3ea 100644 --- a/src/ui/tile_bar.rs +++ b/src/ui/tile_bar.rs @@ -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 = 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 = 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