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 <noreply@anthropic.com>
This commit is contained in:
parent
23ed80a3b7
commit
2c9031bcf3
2 changed files with 111 additions and 27 deletions
41
HANDOFF.md
41
HANDOFF.md
|
|
@ -3,9 +3,35 @@
|
||||||
> Working notes for continuing this work in another session/agent (Copilot, Codex, etc.).
|
> 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.
|
> 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.
|
batched scan writes, sort_by_cached_key, panic=abort) — committed + pushed.
|
||||||
- **`src/app.rs`: desktop row virtualization** (the follow-up the perf spec
|
- **`src/app.rs`: desktop row virtualization** (the follow-up the perf spec
|
||||||
deferred; spec at
|
deferred; spec at
|
||||||
|
|
@ -31,13 +57,10 @@
|
||||||
never overcommits/clips off-screen).
|
never overcommits/clips off-screen).
|
||||||
- `cargo build --release` clean; `cargo test --release` = 134 unit + 12
|
- `cargo build --release` clean; `cargo test --release` = 134 unit + 12
|
||||||
integration, 0 failures.
|
integration, 0 failures.
|
||||||
- **Known pre-existing issue (NOT this change):** at the ~1000px window
|
- **Known pre-existing issue — FIXED in the 2026-07-09 session (see top):** at
|
||||||
minimum width, the central panel's content origin shifts left and underlaps
|
the ~1000px window minimum width the central panel content underlapped the
|
||||||
the ~290px sidebar (column 1 / card left edge clipped), because the top
|
sidebar. Root cause was the sort bar's overflowing `right_to_left` group,
|
||||||
header/toolbar row overflows and shoves the layout. Confirmed **identical on
|
not the top header as originally guessed.
|
||||||
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.
|
|
||||||
- **GUI screenshot recipe on this box (Wayland/KWin):** winit defaults to a
|
- **GUI screenshot recipe on this box (Wayland/KWin):** winit defaults to a
|
||||||
native Wayland surface that xdotool/`import` can't see. Launch with
|
native Wayland surface that xdotool/`import` can't see. Launch with
|
||||||
`env -u WAYLAND_DISPLAY WINIT_UNIX_BACKEND=x11 DISPLAY=:0` to force XWayland,
|
`env -u WAYLAND_DISPLAY WINIT_UNIX_BACKEND=x11 DISPLAY=:0` to force XWayland,
|
||||||
|
|
|
||||||
97
src/app.rs
97
src/app.rs
|
|
@ -62,7 +62,7 @@ struct SimGroup {
|
||||||
videos: Vec<SimVideo>,
|
videos: Vec<SimVideo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
enum SortMode {
|
enum SortMode {
|
||||||
Title,
|
Title,
|
||||||
DurationAsc,
|
DurationAsc,
|
||||||
|
|
@ -1517,7 +1517,10 @@ impl App {
|
||||||
fn top_bar(&mut self, ctx: &egui::Context) {
|
fn top_bar(&mut self, ctx: &egui::Context) {
|
||||||
egui::TopBottomPanel::top("top_bar").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top_bar").show(ctx, |ui| {
|
||||||
ui.add_space(2.0);
|
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.heading("Catacomb");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label("🔍");
|
ui.label("🔍");
|
||||||
|
|
@ -1629,9 +1632,26 @@ impl App {
|
||||||
self.current_screen = Screen::Settings;
|
self.current_screen = Screen::Settings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
// Right-align the status only when it fits in the remaining
|
||||||
ui.label(egui::RichText::new(&self.status).weak());
|
// 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);
|
ui.add_space(2.0);
|
||||||
});
|
});
|
||||||
|
|
@ -4169,6 +4189,35 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn video_list(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
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 {
|
if self.sidebar_view == SidebarView::Channels {
|
||||||
self.channel_grid(ctx, ui);
|
self.channel_grid(ctx, ui);
|
||||||
return;
|
return;
|
||||||
|
|
@ -4205,6 +4254,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bulk mode toolbar
|
// Bulk mode toolbar
|
||||||
|
let mut sort_inline = true;
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let label_text = if self.bulk_mode {
|
let label_text = if self.bulk_mode {
|
||||||
format!("{} videos", cards.len())
|
format!("{} videos", cards.len())
|
||||||
|
|
@ -4254,20 +4304,31 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
// Right-align the sort chips only when they actually fit in the
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::SizeDesc, "Largest");
|
// remaining row width. A right_to_left layout that runs out of
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::SizeAsc, "Smallest");
|
// room overflows LEFT past the panel edge, and egui then advances
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::DurationDesc, "Longest");
|
// the vertical cursor from that overflowed rect — shifting every
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::DurationAsc, "Shortest");
|
// row below it under the sidebar. So measure first; when narrow,
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::ChannelAsc, "Channel");
|
// the chips move to their own wrapped row below instead.
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::Title, "Title");
|
let needed = sort_chip_row_width(ui);
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::DateAsc, "Oldest");
|
sort_inline = ui.available_width() >= needed;
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::DateDesc, "Newest");
|
if sort_inline {
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::DownloadAsc, "Oldest DL");
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
ui.selectable_value(&mut self.sort_mode, SortMode::DownloadDesc, "Recent DL");
|
for (mode, label) in SORT_CHIPS.iter().rev() {
|
||||||
ui.label(egui::RichText::new("Sort:").weak());
|
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();
|
ui.separator();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue