From 2c9031bcf346667ab8ef1349f21ddf2e6cf41540 Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 9 Jul 2026 03:31:56 -0700 Subject: [PATCH] fix: stop narrow windows shoving the library under the sidebar At the ~1000px window minimum, the toolbar's right-aligned sort group ran out of row width and overflowed leftward past the panel edge; egui then advances the vertical cursor from the overflowed rect, so every row below (view toggle, the whole list) shifted left and was clipped under the sidebar. The sort chips now right-align only when a font-metrics measurement says they fit, and otherwise drop to their own wrapped row. The top bar likewise wraps instead of pushing nav buttons off-screen, with the right-aligned status label width-guarded the same way (truncating fallback). Verified via live GUI screenshots at 1000x700 and 1400x800; the wide layout is unchanged. Co-Authored-By: Claude Fable 5 --- HANDOFF.md | 41 ++++++++++++++++++----- src/app.rs | 97 ++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 111 insertions(+), 27 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index fbbfce8..33d5277 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -3,9 +3,35 @@ > Working notes for continuing this work in another session/agent (Copilot, Codex, etc.). > Updated 2026-07-08. Delete or update freely; this is not a tracked spec. -## Current work (2026-07-08): desktop row virtualization — DONE, ready to commit +## Current work (2026-07-09): narrow-window layout fix — DONE, committed -- `main` is at `d4bed01` (perf pass phase 1: WAL pragmas, prepare_cached, +- `23ed80a` (desktop row virtualization, below) is committed + pushed. +- **Narrow-window layout fix** (the follow-up noted below, now RESOLVED): the + real root cause was NOT the top bar shoving the central panel — the sort + bar's `right_to_left` chip group inside `video_list`'s toolbar row overflows + LEFT when the panel is narrow, and egui advances the parent's vertical + cursor from the overflowed rect, shifting every row below it under the + sidebar. Fix in `src/app.rs`: + - `video_list`: sort chips are defined once in `SORT_CHIPS` (visual order); + `sort_chip_row_width(ui)` measures the group via font metrics and the RTL + right-aligned layout is used only when it fits (`sort_inline`), otherwise + the chips render on their own `horizontal_wrapped` row below. Wide-window + look unchanged. + - Top bar: `ui.horizontal` → `ui.horizontal_wrapped` (nav buttons wrap to a + second line instead of going off-screen); the right-aligned status label + is width-guarded the same way with a truncating-label fallback (an + overflowing RTL layout spills left over the buttons). + - `SortMode` now derives `Copy`. + - Verified by live GUI screenshots at 1000×700 and 1400×800 (scratchpad + `final2-1000.png` / `final-1400.png`); 134+12 tests pass. + - Screenshot-harness gotcha: egui renders on events; after `xdotool + windowsize` you must `windowactivate` + `windowraise` + a couple of + `mousemove --window` nudges or the capture races a stale frame (the + "empty second toolbar line" ghost). + +## Previous work (2026-07-08): desktop row virtualization — committed as `23ed80a` + +- `main` was at `d4bed01` (perf pass phase 1: WAL pragmas, prepare_cached, batched scan writes, sort_by_cached_key, panic=abort) — committed + pushed. - **`src/app.rs`: desktop row virtualization** (the follow-up the perf spec deferred; spec at @@ -31,13 +57,10 @@ never overcommits/clips off-screen). - `cargo build --release` clean; `cargo test --release` = 134 unit + 12 integration, 0 failures. -- **Known pre-existing issue (NOT this change):** at the ~1000px window - minimum width, the central panel's content origin shifts left and underlaps - the ~290px sidebar (column 1 / card left edge clipped), because the top - header/toolbar row overflows and shoves the layout. Confirmed **identical on - the pre-virtualization binary** (git-stash test), so it's not a regression. - Root cause is the non-wrapping header `ui.horizontal`; a real fix would make - that row wrap or clip. Left as a separate follow-up. +- **Known pre-existing issue — FIXED in the 2026-07-09 session (see top):** at + the ~1000px window minimum width the central panel content underlapped the + sidebar. Root cause was the sort bar's overflowing `right_to_left` group, + not the top header as originally guessed. - **GUI screenshot recipe on this box (Wayland/KWin):** winit defaults to a native Wayland surface that xdotool/`import` can't see. Launch with `env -u WAYLAND_DISPLAY WINIT_UNIX_BACKEND=x11 DISPLAY=:0` to force XWayland, diff --git a/src/app.rs b/src/app.rs index 776963d..8dfcc68 100644 --- a/src/app.rs +++ b/src/app.rs @@ -62,7 +62,7 @@ struct SimGroup { videos: Vec, } -#[derive(Clone, PartialEq)] +#[derive(Clone, Copy, PartialEq)] enum SortMode { Title, DurationAsc, @@ -1517,7 +1517,10 @@ impl App { fn top_bar(&mut self, ctx: &egui::Context) { egui::TopBottomPanel::top("top_bar").show(ctx, |ui| { ui.add_space(2.0); - ui.horizontal(|ui| { + // Wrapped so narrow windows push buttons onto a second line + // instead of off-screen (the row is wider than the ~1000px + // window minimum). + ui.horizontal_wrapped(|ui| { ui.heading("Catacomb"); ui.separator(); ui.label("🔍"); @@ -1629,9 +1632,26 @@ impl App { self.current_screen = Screen::Settings; } } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label(egui::RichText::new(&self.status).weak()); - }); + // Right-align the status only when it fits in the remaining + // row width — an overflowing right_to_left layout spills LEFT + // over the nav buttons. Otherwise show it truncated in flow. + let status = egui::RichText::new(&self.status).weak(); + let status_w = egui::WidgetText::from(status.clone()) + .into_galley( + ui, + Some(egui::TextWrapMode::Extend), + f32::INFINITY, + egui::TextStyle::Body, + ) + .size() + .x; + if ui.available_width() >= status_w + 8.0 { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(status); + }); + } else { + ui.add(egui::Label::new(status).truncate()); + } }); ui.add_space(2.0); }); @@ -4169,6 +4189,35 @@ impl App { } fn video_list(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { + // Toolbar sort chips in left-to-right visual order. + const SORT_CHIPS: [(SortMode, &str); 10] = [ + (SortMode::DownloadDesc, "Recent DL"), + (SortMode::DownloadAsc, "Oldest DL"), + (SortMode::DateDesc, "Newest"), + (SortMode::DateAsc, "Oldest"), + (SortMode::Title, "Title"), + (SortMode::ChannelAsc, "Channel"), + (SortMode::DurationAsc, "Shortest"), + (SortMode::DurationDesc, "Longest"), + (SortMode::SizeAsc, "Smallest"), + (SortMode::SizeDesc, "Largest"), + ]; + /// Measured width the inline sort group needs on the toolbar row. + fn sort_chip_row_width(ui: &egui::Ui) -> f32 { + let spacing = ui.spacing().item_spacing.x; + let pad = ui.spacing().button_padding.x * 2.0; + let text_w = |s: &str, style: egui::TextStyle| { + egui::WidgetText::from(s) + .into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, style) + .size() + .x + }; + let chips: f32 = SORT_CHIPS + .iter() + .map(|(_, label)| text_w(label, egui::TextStyle::Button) + pad + spacing) + .sum(); + chips + text_w("Sort:", egui::TextStyle::Body) + spacing + 8.0 + } if self.sidebar_view == SidebarView::Channels { self.channel_grid(ctx, ui); return; @@ -4205,6 +4254,7 @@ impl App { } // Bulk mode toolbar + let mut sort_inline = true; ui.horizontal(|ui| { let label_text = if self.bulk_mode { format!("{} videos", cards.len()) @@ -4254,20 +4304,31 @@ impl App { } } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.selectable_value(&mut self.sort_mode, SortMode::SizeDesc, "Largest"); - ui.selectable_value(&mut self.sort_mode, SortMode::SizeAsc, "Smallest"); - ui.selectable_value(&mut self.sort_mode, SortMode::DurationDesc, "Longest"); - ui.selectable_value(&mut self.sort_mode, SortMode::DurationAsc, "Shortest"); - ui.selectable_value(&mut self.sort_mode, SortMode::ChannelAsc, "Channel"); - ui.selectable_value(&mut self.sort_mode, SortMode::Title, "Title"); - ui.selectable_value(&mut self.sort_mode, SortMode::DateAsc, "Oldest"); - ui.selectable_value(&mut self.sort_mode, SortMode::DateDesc, "Newest"); - ui.selectable_value(&mut self.sort_mode, SortMode::DownloadAsc, "Oldest DL"); - ui.selectable_value(&mut self.sort_mode, SortMode::DownloadDesc, "Recent DL"); - ui.label(egui::RichText::new("Sort:").weak()); - }); + // Right-align the sort chips only when they actually fit in the + // remaining row width. A right_to_left layout that runs out of + // room overflows LEFT past the panel edge, and egui then advances + // the vertical cursor from that overflowed rect — shifting every + // row below it under the sidebar. So measure first; when narrow, + // the chips move to their own wrapped row below instead. + let needed = sort_chip_row_width(ui); + sort_inline = ui.available_width() >= needed; + if sort_inline { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + for (mode, label) in SORT_CHIPS.iter().rev() { + ui.selectable_value(&mut self.sort_mode, *mode, *label); + } + ui.label(egui::RichText::new("Sort:").weak()); + }); + } }); + if !sort_inline { + ui.horizontal_wrapped(|ui| { + ui.label(egui::RichText::new("Sort:").weak()); + for (mode, label) in SORT_CHIPS { + ui.selectable_value(&mut self.sort_mode, mode, label); + } + }); + } ui.separator();