Skip to main content

max / audiofiles

Stop the search field from eating the toolbar row The field asked for available_width() - 160, but the controls after it (Clear, the scope toggle, the result count, Save, Undo, the panel toggles) need several hundred pixels, so the row overflowed and clipped everything past Undo. Measure what those controls consume and reserve it on the next frame. The cursor advances even where painting is clipped, so the measurement is right on the frame that overflows and one frame corrects it. A constant cannot hold: theme-resolved spacing and font metrics both move.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 00:22 UTC
Signed with PGP, not checked
Commit: 58c1af66de474d8c8013e81cc0f9e95b590da922
Parent: 8f810b2
1 file changed, +33 insertions, -1 deletion
@@ -29,10 +29,22 @@
29 29 )
30 30 .on_hover_text("Search (press / to focus)");
31 31
32 + // The field takes whatever the controls after it do not need. That width
33 + // cannot be known before they are drawn, so it is measured at the end of
34 + // the row and reused on the next frame: a one-frame correction nobody can
35 + // see, and it cannot drift the way the old fixed 160px budget did. That
36 + // budget predated the scope toggle, the result count, Save and the panel
37 + // toggles, so the row overflowed and clipped everything past Undo.
38 + // Theme-resolved spacing and per-platform font metrics rule out a constant.
39 + let trailing_id = ui.make_persistent_id("toolbar_trailing_width");
40 + let trailing_w = ui
41 + .data(|d| d.get_temp::<f32>(trailing_id))
42 + .unwrap_or(DEFAULT_TRAILING_WIDTH);
32 43 let search_edit = egui::TextEdit::singleline(&mut state.search.search_query)
33 44 .hint_text("Search samples... (/)")
34 - .desired_width(ui.available_width() - 160.0);
45 + .desired_width((ui.available_width() - trailing_w).max(MIN_SEARCH_WIDTH));
35 46 let resp = widgets::text_field(ui, search_edit);
47 + let trailing_start_x = ui.cursor().min.x;
36 48
37 49 if state.focus_search {
38 50 resp.request_focus();
@@ -171,9 +183,29 @@
171 183 } else {
172 184 draw_inline_panel_toggles(ui, state, detail_hidden);
173 185 }
186 +
187 + // What the trailing controls actually consumed, for the next frame's field
188 + // width. The cursor keeps advancing even where painting was clipped, so
189 + // this reads true on the very frame the row overflows, which is what lets
190 + // one frame be enough to correct it.
191 + let measured = ui.cursor().min.x - trailing_start_x;
192 + if (measured - trailing_w).abs() > 0.5 {
193 + ui.data_mut(|d| d.insert_temp(trailing_id, measured));
194 + ui.ctx().request_repaint();
195 + }
174 196 });
175 197 }
176 198
199 + /// Width assumed for the controls after the search field on the first frame,
200 + /// before the row has been measured once. Deliberately generous: too large only
201 + /// shortens the field for one frame, too small clips a control for one frame.
202 + const DEFAULT_TRAILING_WIDTH: f32 = 560.0;
203 +
204 + /// The search field never shrinks below this, however little room is left. Past
205 + /// that point the row is narrower than its own controls, which is the case the
206 + /// M-3 View-menu collapse below 900px exists to handle.
207 + const MIN_SEARCH_WIDTH: f32 = 120.0;
208 +
177 209 /// Draw the full row of toolbar panel toggles (M-3 expanded layout).
178 210 fn draw_inline_panel_toggles(ui: &mut egui::Ui, state: &mut BrowserState, detail_hidden: bool) {
179 211 if widgets::toolbar_toggle(