max / audiofiles
7 files changed,
+169 insertions,
-58 deletions
| @@ -299,7 +299,7 @@ fn handle_keyboard(ctx: &egui::Context, state: &mut BrowserState) { | |||
| 299 | 299 | // Tab: focus the detail-panel tag input (opens the detail panel if hidden). | |
| 300 | 300 | if input.key_pressed(egui::Key::Tab) && !input.modifiers.shift { | |
| 301 | 301 | if !state.detail_visible { | |
| 302 | - | state.detail_visible = true; | |
| 302 | + | state.set_detail_visible(true); | |
| 303 | 303 | } | |
| 304 | 304 | state.focus_tag_input = true; | |
| 305 | 305 | return; | |
| @@ -393,11 +393,11 @@ fn handle_keyboard(ctx: &egui::Context, state: &mut BrowserState) { | |||
| 393 | 393 | } | |
| 394 | 394 | // "S" toggles sidebar | |
| 395 | 395 | if input.key_pressed(egui::Key::S) { | |
| 396 | - | state.sidebar_visible = !state.sidebar_visible; | |
| 396 | + | state.toggle_sidebar(); | |
| 397 | 397 | } | |
| 398 | 398 | // "D" toggles detail panel | |
| 399 | 399 | if input.key_pressed(egui::Key::D) && !shift { | |
| 400 | - | state.detail_visible = !state.detail_visible; | |
| 400 | + | state.toggle_detail(); | |
| 401 | 401 | } | |
| 402 | 402 | // Shift+F: find similar | |
| 403 | 403 | if shift && input.key_pressed(egui::Key::F) { |
| @@ -346,7 +346,17 @@ impl BrowserState { | |||
| 346 | 346 | vfs_list = backend.list_vfs()?; | |
| 347 | 347 | } | |
| 348 | 348 | ||
| 349 | - | let contents = backend.list_children_enriched(vfs_list[0].id, None) | |
| 349 | + | // Restore the last-selected vault by id (indices shift as vaults are | |
| 350 | + | // added/removed), so the initial contents below load for the right vault. | |
| 351 | + | let current_vfs_idx = backend | |
| 352 | + | .get_config("current_vfs_id") | |
| 353 | + | .ok() | |
| 354 | + | .flatten() | |
| 355 | + | .and_then(|s| s.parse::<i64>().ok()) | |
| 356 | + | .and_then(|id| vfs_list.iter().position(|v| v.id.as_i64() == id)) | |
| 357 | + | .unwrap_or(0); | |
| 358 | + | ||
| 359 | + | let contents = backend.list_children_enriched(vfs_list[current_vfs_idx].id, None) | |
| 350 | 360 | .unwrap_or_else(|e| { error!("Failed to load initial contents: {e}"); Vec::new() }); | |
| 351 | 361 | let all_tags = backend.list_all_tags() | |
| 352 | 362 | .unwrap_or_else(|e| { warn!("Failed to load tags: {e}"); Vec::new() }); | |
| @@ -403,11 +413,20 @@ impl BrowserState { | |||
| 403 | 413 | .join("audiofiles") | |
| 404 | 414 | }); | |
| 405 | 415 | ||
| 416 | + | // Restore shell layout state (persisted on toggle). Sidebar/detail | |
| 417 | + | // default to shown, filter panel to closed. | |
| 418 | + | let sidebar_visible = | |
| 419 | + | backend.get_config("sidebar_visible").ok().flatten().as_deref() != Some("0"); | |
| 420 | + | let detail_visible = | |
| 421 | + | backend.get_config("detail_visible").ok().flatten().as_deref() != Some("0"); | |
| 422 | + | let filter_panel_open = | |
| 423 | + | backend.get_config("filter_panel_open").ok().flatten().as_deref() == Some("1"); | |
| 424 | + | ||
| 406 | 425 | Ok(Self { | |
| 407 | 426 | data_dir: data_dir.to_path_buf(), | |
| 408 | 427 | backend, | |
| 409 | 428 | vfs_list: Arc::new(vfs_list), | |
| 410 | - | current_vfs_idx: 0, | |
| 429 | + | current_vfs_idx, | |
| 411 | 430 | current_dir: None, | |
| 412 | 431 | breadcrumb: Vec::new(), | |
| 413 | 432 | contents: Arc::new(contents), | |
| @@ -418,13 +437,13 @@ impl BrowserState { | |||
| 418 | 437 | selected_analysis: None, | |
| 419 | 438 | selected_waveform: None, | |
| 420 | 439 | tag_input: String::new(), | |
| 421 | - | detail_visible: true, | |
| 422 | - | sidebar_visible: true, | |
| 440 | + | detail_visible, | |
| 441 | + | sidebar_visible, | |
| 423 | 442 | sort_column: SortColumn::Name, | |
| 424 | 443 | sort_direction: SortDirection::Ascending, | |
| 425 | 444 | search_query: String::new(), | |
| 426 | 445 | search_filter: SearchFilter::default(), | |
| 427 | - | filter_panel_open: false, | |
| 446 | + | filter_panel_open, | |
| 428 | 447 | collection_filter_name_input: String::new(), | |
| 429 | 448 | filter_tag_input: String::new(), | |
| 430 | 449 | similarity_search_hash: None, |
| @@ -249,10 +249,50 @@ impl BrowserState { | |||
| 249 | 249 | self.breadcrumb.clear(); | |
| 250 | 250 | self.selection.clear(); | |
| 251 | 251 | self.similarity_search_hash = None; | |
| 252 | - | self.similarity_source_name = None; | |
| 252 | + | self.similarity_source_name = None; | |
| 253 | + | // Persist the selection by VFS id (not index — indices shift when | |
| 254 | + | // vaults are added or removed) so it restores on next launch. | |
| 255 | + | let _ = self.backend.set_config( | |
| 256 | + | "current_vfs_id", | |
| 257 | + | &self.vfs_list[self.current_vfs_idx].id.as_i64().to_string(), | |
| 258 | + | ); | |
| 253 | 259 | self.refresh_contents(); | |
| 254 | 260 | self.refresh_collections(); | |
| 255 | 261 | self.status = format!("Switched to: {}", self.vfs_list[self.current_vfs_idx].name); | |
| 256 | 262 | } | |
| 257 | 263 | } | |
| 264 | + | ||
| 265 | + | /// Toggle the left sidebar and persist the choice across restarts. | |
| 266 | + | pub fn toggle_sidebar(&mut self) { | |
| 267 | + | self.sidebar_visible = !self.sidebar_visible; | |
| 268 | + | let _ = self.backend.set_config( | |
| 269 | + | "sidebar_visible", | |
| 270 | + | if self.sidebar_visible { "1" } else { "0" }, | |
| 271 | + | ); | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | /// Toggle the right detail panel and persist the choice across restarts. | |
| 275 | + | pub fn toggle_detail(&mut self) { | |
| 276 | + | self.set_detail_visible(!self.detail_visible); | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | /// Set detail-panel visibility and persist it (used by both the explicit | |
| 280 | + | /// toggle and the auto-show on selection so the stored state never drifts). | |
| 281 | + | pub fn set_detail_visible(&mut self, visible: bool) { | |
| 282 | + | if self.detail_visible != visible { | |
| 283 | + | self.detail_visible = visible; | |
| 284 | + | let _ = self | |
| 285 | + | .backend | |
| 286 | + | .set_config("detail_visible", if visible { "1" } else { "0" }); | |
| 287 | + | } | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | /// Toggle the filter panel and persist the choice across restarts. | |
| 291 | + | pub fn toggle_filter_panel(&mut self) { | |
| 292 | + | self.filter_panel_open = !self.filter_panel_open; | |
| 293 | + | let _ = self.backend.set_config( | |
| 294 | + | "filter_panel_open", | |
| 295 | + | if self.filter_panel_open { "1" } else { "0" }, | |
| 296 | + | ); | |
| 297 | + | } | |
| 258 | 298 | } |
| @@ -6,6 +6,15 @@ use crate::state::BrowserState; | |||
| 6 | 6 | use super::theme; | |
| 7 | 7 | use super::widgets; | |
| 8 | 8 | ||
| 9 | + | /// Debounce numeric range filters: re-run the search only when a DragValue | |
| 10 | + | /// drag finishes or the field loses keyboard focus, not on every intermediate | |
| 11 | + | /// tick. The filter value still updates live on `.changed()` (so the widget | |
| 12 | + | /// shows the new number); only the (potentially heavy) query is deferred to | |
| 13 | + | /// the settle point. | |
| 14 | + | fn requery_now(resp: &egui::Response) -> bool { | |
| 15 | + | resp.drag_stopped() || resp.lost_focus() | |
| 16 | + | } | |
| 17 | + | ||
| 9 | 18 | /// Render the per-section "[clear]" mini-button used by every active filter | |
| 10 | 19 | /// section. Returns true on click. The wrapping `if active` lives at the call | |
| 11 | 20 | /// site so each section's clear semantics stay local. | |
| @@ -46,19 +55,21 @@ pub fn draw_filter_panel(ui: &mut egui::Ui, state: &mut BrowserState) { | |||
| 46 | 55 | let mut min = state.search_filter.bpm_min.unwrap_or(0.0); | |
| 47 | 56 | let mut max = state.search_filter.bpm_max.unwrap_or(300.0); | |
| 48 | 57 | ui.label("Min"); | |
| 49 | - | if ui.add(egui::DragValue::new(&mut min).speed(1.0).range(0.0..=300.0)).changed() { | |
| 58 | + | let r = ui.add(egui::DragValue::new(&mut min).speed(1.0).range(0.0..=300.0)); | |
| 59 | + | if r.changed() { | |
| 50 | 60 | if min > max { max = min; } | |
| 51 | 61 | state.search_filter.bpm_min = if min > 0.0 { Some(min) } else { None }; | |
| 52 | 62 | state.search_filter.bpm_max = if max < 300.0 { Some(max) } else { None }; | |
| 53 | - | changed = true; | |
| 54 | 63 | } | |
| 64 | + | if requery_now(&r) { changed = true; } | |
| 55 | 65 | ui.label("Max"); | |
| 56 | - | if ui.add(egui::DragValue::new(&mut max).speed(1.0).range(0.0..=300.0)).changed() { | |
| 66 | + | let r = ui.add(egui::DragValue::new(&mut max).speed(1.0).range(0.0..=300.0)); | |
| 67 | + | if r.changed() { | |
| 57 | 68 | if max < min { min = max; } | |
| 58 | 69 | state.search_filter.bpm_max = if max < 300.0 { Some(max) } else { None }; | |
| 59 | 70 | state.search_filter.bpm_min = if min > 0.0 { Some(min) } else { None }; | |
| 60 | - | changed = true; | |
| 61 | 71 | } | |
| 72 | + | if requery_now(&r) { changed = true; } | |
| 62 | 73 | }); | |
| 63 | 74 | }); | |
| 64 | 75 | ||
| @@ -73,19 +84,21 @@ pub fn draw_filter_panel(ui: &mut egui::Ui, state: &mut BrowserState) { | |||
| 73 | 84 | let mut min = state.search_filter.duration_min.unwrap_or(0.0); | |
| 74 | 85 | let mut max = state.search_filter.duration_max.unwrap_or(600.0); | |
| 75 | 86 | ui.label("Min"); | |
| 76 | - | if ui.add(egui::DragValue::new(&mut min).speed(0.1).range(0.0..=600.0)).changed() { | |
| 87 | + | let r = ui.add(egui::DragValue::new(&mut min).speed(0.1).range(0.0..=600.0)); | |
| 88 | + | if r.changed() { | |
| 77 | 89 | if min > max { max = min; } | |
| 78 | 90 | state.search_filter.duration_min = if min > 0.0 { Some(min) } else { None }; | |
| 79 | 91 | state.search_filter.duration_max = if max < 600.0 { Some(max) } else { None }; | |
| 80 | - | changed = true; | |
| 81 | 92 | } | |
| 93 | + | if requery_now(&r) { changed = true; } | |
| 82 | 94 | ui.label("Max"); | |
| 83 | - | if ui.add(egui::DragValue::new(&mut max).speed(0.1).range(0.0..=600.0)).changed() { | |
| 95 | + | let r = ui.add(egui::DragValue::new(&mut max).speed(0.1).range(0.0..=600.0)); | |
| 96 | + | if r.changed() { | |
| 84 | 97 | if max < min { min = max; } | |
| 85 | 98 | state.search_filter.duration_max = if max < 600.0 { Some(max) } else { None }; | |
| 86 | 99 | state.search_filter.duration_min = if min > 0.0 { Some(min) } else { None }; | |
| 87 | - | changed = true; | |
| 88 | 100 | } | |
| 101 | + | if requery_now(&r) { changed = true; } | |
| 89 | 102 | }); | |
| 90 | 103 | }); | |
| 91 | 104 | ||
| @@ -100,19 +113,21 @@ pub fn draw_filter_panel(ui: &mut egui::Ui, state: &mut BrowserState) { | |||
| 100 | 113 | let mut min = state.search_filter.peak_db_min.unwrap_or(-96.0); | |
| 101 | 114 | let mut max = state.search_filter.peak_db_max.unwrap_or(0.0); | |
| 102 | 115 | ui.label("Min"); | |
| 103 | - | if ui.add(egui::DragValue::new(&mut min).speed(0.5).range(-96.0..=0.0).suffix(" dB")).changed() { | |
| 116 | + | let r = ui.add(egui::DragValue::new(&mut min).speed(0.5).range(-96.0..=0.0).suffix(" dB")); | |
| 117 | + | if r.changed() { | |
| 104 | 118 | if min > max { max = min; } | |
| 105 | 119 | state.search_filter.peak_db_min = if min > -96.0 { Some(min) } else { None }; | |
| 106 | 120 | state.search_filter.peak_db_max = if max < 0.0 { Some(max) } else { None }; | |
| 107 | - | changed = true; | |
| 108 | 121 | } | |
| 122 | + | if requery_now(&r) { changed = true; } | |
| 109 | 123 | ui.label("Max"); | |
| 110 | - | if ui.add(egui::DragValue::new(&mut max).speed(0.5).range(-96.0..=0.0).suffix(" dB")).changed() { | |
| 124 | + | let r = ui.add(egui::DragValue::new(&mut max).speed(0.5).range(-96.0..=0.0).suffix(" dB")); | |
| 125 | + | if r.changed() { | |
| 111 | 126 | if max < min { min = max; } | |
| 112 | 127 | state.search_filter.peak_db_max = if max < 0.0 { Some(max) } else { None }; | |
| 113 | 128 | state.search_filter.peak_db_min = if min > -96.0 { Some(min) } else { None }; | |
| 114 | - | changed = true; | |
| 115 | 129 | } | |
| 130 | + | if requery_now(&r) { changed = true; } | |
| 116 | 131 | }); | |
| 117 | 132 | }); | |
| 118 | 133 |
| @@ -247,28 +247,32 @@ fn draw_subscription_section( | |||
| 247 | 247 | ); | |
| 248 | 248 | ui.add_space(theme::space::SM); | |
| 249 | 249 | ||
| 250 | - | if let Some(cap) = draw_cap_picker( | |
| 251 | - | ui, | |
| 252 | - | state, | |
| 253 | - | &pricing, | |
| 254 | - | BillingInterval::Annual, | |
| 255 | - | "Subscribe (annual)", | |
| 256 | - | ) { | |
| 257 | - | state.sync.checkout_loading = true; | |
| 258 | - | state.sync.checkout_loading_at = Some(std::time::Instant::now()); | |
| 259 | - | sync.subscribe(cap, BillingInterval::Annual); | |
| 260 | - | } | |
| 261 | - | ui.add_space(theme::space::XS); | |
| 262 | - | if let Some(cap) = draw_cap_picker( | |
| 263 | - | ui, | |
| 264 | - | state, | |
| 265 | - | &pricing, | |
| 266 | - | BillingInterval::Monthly, | |
| 267 | - | "Subscribe (monthly)", | |
| 268 | - | ) { | |
| 269 | - | state.sync.checkout_loading = true; | |
| 270 | - | state.sync.checkout_loading_at = Some(std::time::Instant::now()); | |
| 271 | - | sync.subscribe(cap, BillingInterval::Monthly); | |
| 250 | + | // One cap slider, then annual/monthly checkout buttons for that | |
| 251 | + | // single chosen cap — two priced choices, not two sliders that | |
| 252 | + | // secretly share a value. | |
| 253 | + | let cap_bytes = draw_cap_slider(ui, state, &pricing); | |
| 254 | + | ui.add_space(theme::space::SM); | |
| 255 | + | ||
| 256 | + | if state.sync.checkout_loading { | |
| 257 | + | ui.horizontal(|ui| { | |
| 258 | + | ui.spinner(); | |
| 259 | + | ui.label(egui::RichText::new("Opening browser...").weak()); | |
| 260 | + | }); | |
| 261 | + | } else { | |
| 262 | + | let annual = format_cents(pricing.quote_cents(cap_bytes, BillingInterval::Annual)); | |
| 263 | + | let monthly = format_cents(pricing.quote_cents(cap_bytes, BillingInterval::Monthly)); | |
| 264 | + | ui.horizontal(|ui| { | |
| 265 | + | if widgets::primary_button(ui, &format!("Subscribe annual \u{2014} {annual}/yr")).clicked() { | |
| 266 | + | state.sync.checkout_loading = true; | |
| 267 | + | state.sync.checkout_loading_at = Some(std::time::Instant::now()); | |
| 268 | + | sync.subscribe(cap_bytes, BillingInterval::Annual); | |
| 269 | + | } | |
| 270 | + | if widgets::secondary_button(ui, &format!("Monthly \u{2014} {monthly}/mo")).clicked() { | |
| 271 | + | state.sync.checkout_loading = true; | |
| 272 | + | state.sync.checkout_loading_at = Some(std::time::Instant::now()); | |
| 273 | + | sync.subscribe(cap_bytes, BillingInterval::Monthly); | |
| 274 | + | } | |
| 275 | + | }); | |
| 272 | 276 | } | |
| 273 | 277 | } else { | |
| 274 | 278 | ui.label(egui::RichText::new("Loading pricing...").weak()); | |
| @@ -659,6 +663,23 @@ fn draw_ready( | |||
| 659 | 663 | } | |
| 660 | 664 | } | |
| 661 | 665 | ||
| 666 | + | /// Draw just the storage-cap slider (GiB, logarithmic) plus a cap-size label, | |
| 667 | + | /// clamping the working value to the pricing range. Returns the chosen cap in | |
| 668 | + | /// bytes. Used by the subscribe view (one slider feeding two checkout buttons). | |
| 669 | + | fn draw_cap_slider(ui: &mut egui::Ui, state: &mut BrowserState, pricing: &AppPricing) -> i64 { | |
| 670 | + | let min_gib = (pricing.min_cap_bytes / GIB).max(1); | |
| 671 | + | let max_gib = (pricing.max_cap_bytes / GIB).max(min_gib); | |
| 672 | + | state.sync.cap_picker_gib = state.sync.cap_picker_gib.clamp(min_gib, max_gib); | |
| 673 | + | ui.add( | |
| 674 | + | egui::Slider::new(&mut state.sync.cap_picker_gib, min_gib..=max_gib) | |
| 675 | + | .logarithmic(true) | |
| 676 | + | .text("GiB"), | |
| 677 | + | ); | |
| 678 | + | let cap_bytes = state.sync.cap_picker_gib * GIB; | |
| 679 | + | ui.label(egui::RichText::new(format_cap(cap_bytes)).strong()); | |
| 680 | + | cap_bytes | |
| 681 | + | } | |
| 682 | + | ||
| 662 | 683 | /// Cap-picker widget: slider in GiB + live price preview + action button. | |
| 663 | 684 | /// Used both for initial subscribe and for queuing a cap change on an active | |
| 664 | 685 | /// subscription. The slider's working value lives on `BrowserState::sync` so |
| @@ -138,7 +138,7 @@ fn draw_inline_panel_toggles( | |||
| 138 | 138 | detail_hidden: bool, | |
| 139 | 139 | ) { | |
| 140 | 140 | if widgets::toolbar_toggle(ui, "Sidebar", state.sidebar_visible, "Toggle sidebar (S)", None) { | |
| 141 | - | state.sidebar_visible = !state.sidebar_visible; | |
| 141 | + | state.toggle_sidebar(); | |
| 142 | 142 | } | |
| 143 | 143 | ||
| 144 | 144 | // M-13: the Detail toggle conveys "active but hidden" via a muted colour | |
| @@ -161,7 +161,7 @@ fn draw_inline_panel_toggles( | |||
| 161 | 161 | .on_hover_text(detail_tooltip) | |
| 162 | 162 | .clicked() | |
| 163 | 163 | { | |
| 164 | - | state.detail_visible = !state.detail_visible; | |
| 164 | + | state.toggle_detail(); | |
| 165 | 165 | } | |
| 166 | 166 | ||
| 167 | 167 | if widgets::toolbar_toggle(ui, "Edit", state.edit.show_window, "Toggle sample editor (E)", None) { | |
| @@ -184,7 +184,7 @@ fn draw_inline_panel_toggles( | |||
| 184 | 184 | "Toggle filter panel".to_string() | |
| 185 | 185 | }; | |
| 186 | 186 | if widgets::toolbar_toggle(ui, "Filters", state.filter_panel_open, &hover, show_count.then_some(filter_count)) { | |
| 187 | - | state.filter_panel_open = !state.filter_panel_open; | |
| 187 | + | state.toggle_filter_panel(); | |
| 188 | 188 | } | |
| 189 | 189 | } | |
| 190 | 190 | ||
| @@ -205,7 +205,7 @@ fn draw_view_menu(ui: &mut egui::Ui, state: &mut BrowserState, detail_hidden: bo | |||
| 205 | 205 | .button(format!("{}Sidebar (S)", active_dot(state.sidebar_visible))) | |
| 206 | 206 | .clicked() | |
| 207 | 207 | { | |
| 208 | - | state.sidebar_visible = !state.sidebar_visible; | |
| 208 | + | state.toggle_sidebar(); | |
| 209 | 209 | ui.close(); | |
| 210 | 210 | } | |
| 211 | 211 | let detail_label = if detail_hidden { | |
| @@ -214,7 +214,7 @@ fn draw_view_menu(ui: &mut egui::Ui, state: &mut BrowserState, detail_hidden: bo | |||
| 214 | 214 | format!("{}Detail (D)", active_dot(state.detail_visible)) | |
| 215 | 215 | }; | |
| 216 | 216 | if ui.button(detail_label).clicked() { | |
| 217 | - | state.detail_visible = !state.detail_visible; | |
| 217 | + | state.toggle_detail(); | |
| 218 | 218 | ui.close(); | |
| 219 | 219 | } | |
| 220 | 220 | if ui | |
| @@ -248,7 +248,7 @@ fn draw_view_menu(ui: &mut egui::Ui, state: &mut BrowserState, detail_hidden: bo | |||
| 248 | 248 | format!("{}Filters", active_dot(state.filter_panel_open)) | |
| 249 | 249 | }; | |
| 250 | 250 | if ui.button(filters_label).clicked() { | |
| 251 | - | state.filter_panel_open = !state.filter_panel_open; | |
| 251 | + | state.toggle_filter_panel(); | |
| 252 | 252 | ui.close(); | |
| 253 | 253 | } | |
| 254 | 254 | }); |
| @@ -432,6 +432,22 @@ pub fn filter_section<R>( | |||
| 432 | 432 | ||
| 433 | 433 | // --- Selectable rows --------------------------------------------------------- | |
| 434 | 434 | ||
| 435 | + | /// Render a selectable label that truncates with an ellipsis instead of | |
| 436 | + | /// expanding the row (which would force the sidebar wider than its clip range), | |
| 437 | + | /// and shows the full `full_text` on hover so a clipped name is still readable. | |
| 438 | + | fn selectable_truncating( | |
| 439 | + | ui: &mut egui::Ui, | |
| 440 | + | active: bool, | |
| 441 | + | rich: egui::RichText, | |
| 442 | + | full_text: &str, | |
| 443 | + | ) -> egui::Response { | |
| 444 | + | let prev = ui.style().wrap_mode; | |
| 445 | + | ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); | |
| 446 | + | let resp = ui.selectable_label(active, rich); | |
| 447 | + | ui.style_mut().wrap_mode = prev; | |
| 448 | + | resp.on_hover_text(full_text) | |
| 449 | + | } | |
| 450 | + | ||
| 435 | 451 | /// Primary selectable list row. | |
| 436 | 452 | /// | |
| 437 | 453 | /// Active state renders the label as `strong()` + `accent_blue`. Inactive state | |
| @@ -440,11 +456,11 @@ pub fn filter_section<R>( | |||
| 440 | 456 | pub fn selectable_row(ui: &mut egui::Ui, active: bool, label: impl Into<String>) -> egui::Response { | |
| 441 | 457 | let text = label.into(); | |
| 442 | 458 | let rich = if active { | |
| 443 | - | egui::RichText::new(text).strong().color(theme::accent_blue()) | |
| 459 | + | egui::RichText::new(text.clone()).strong().color(theme::accent_blue()) | |
| 444 | 460 | } else { | |
| 445 | - | egui::RichText::new(text).color(theme::text_primary()) | |
| 461 | + | egui::RichText::new(text.clone()).color(theme::text_primary()) | |
| 446 | 462 | }; | |
| 447 | - | ui.selectable_label(active, rich) | |
| 463 | + | selectable_truncating(ui, active, rich, &text) | |
| 448 | 464 | } | |
| 449 | 465 | ||
| 450 | 466 | /// Secondary selectable list row. | |
| @@ -459,11 +475,11 @@ pub fn selectable_row_secondary( | |||
| 459 | 475 | ) -> egui::Response { | |
| 460 | 476 | let text = label.into(); | |
| 461 | 477 | let rich = if active { | |
| 462 | - | egui::RichText::new(text).strong().color(theme::accent_blue()) | |
| 478 | + | egui::RichText::new(text.clone()).strong().color(theme::accent_blue()) | |
| 463 | 479 | } else { | |
| 464 | - | egui::RichText::new(text).color(theme::text_secondary()) | |
| 480 | + | egui::RichText::new(text.clone()).color(theme::text_secondary()) | |
| 465 | 481 | }; | |
| 466 | - | ui.selectable_label(active, rich) | |
| 482 | + | selectable_truncating(ui, active, rich, &text) | |
| 467 | 483 | } | |
| 468 | 484 | ||
| 469 | 485 | /// Tag-tree row. | |
| @@ -474,11 +490,11 @@ pub fn selectable_row_secondary( | |||
| 474 | 490 | pub fn selectable_tag(ui: &mut egui::Ui, active: bool, label: impl Into<String>) -> egui::Response { | |
| 475 | 491 | let text = label.into(); | |
| 476 | 492 | let rich = if active { | |
| 477 | - | egui::RichText::new(text).color(theme::accent_blue()) | |
| 493 | + | egui::RichText::new(text.clone()).color(theme::accent_blue()) | |
| 478 | 494 | } else { | |
| 479 | - | egui::RichText::new(text).color(theme::text_secondary()) | |
| 495 | + | egui::RichText::new(text.clone()).color(theme::text_secondary()) | |
| 480 | 496 | }; | |
| 481 | - | ui.selectable_label(active, rich) | |
| 497 | + | selectable_truncating(ui, active, rich, &text) | |
| 482 | 498 | } | |
| 483 | 499 | ||
| 484 | 500 | /// Render a wizard step indicator: a horizontal row of step labels with the |