From ad2525ece291f299e5cbdf421f87cc6ff4bc963a Mon Sep 17 00:00:00 2001 From: benlukka <68825026+benlukka@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:18:03 +0200 Subject: [PATCH] fix(ui): eliminate preview flickering when hovering presets and profiles - Retain active preset preview when hovering within the scroll or panel area across headers and gaps. - Prevent premature dropping of hover preview textures during asynchronous rerendering in detail view. - Clear cached hover state when clicking an item or when the pointer leaves the panel. - Add headless regression tests covering preset hover persistence and transitions. Signed-off-by: benlukka <68825026+benlukka@users.noreply.github.com> --- crates/ui-egui/src/headless.rs | 9 +++++++++ crates/ui-egui/src/panels/detail.rs | 4 ++-- crates/ui-egui/src/panels/presets.rs | 24 ++++++++++++++++++++++-- crates/ui-egui/src/panels/profiles.rs | 16 ++++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index a8c6e062..52cb96f9 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -413,6 +413,15 @@ mod tests { assert_eq!(after.develop, before.develop); assert_eq!(after.history.len(), before.history.len(), "no history entry while hovering"); assert!(h.app.session.undo.is_empty()); + // hovering down across group headers within the presets list retains the preview (no flicker) + h.request("ui.hoverWidget", json!({"id": "presetGroup:Creative"}), t); + h.step(); + assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: High Contrast B&W")); + // hovering another preset switches smoothly to it + h.request("ui.hoverWidget", json!({"id": "preset:lc.warm-glow"}), t); + h.settle(SETTLE); + h.step(); + assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: Warm Glow")); // thumbnails: variant textures for the visible presets h.app.ui.preset_thumbs = true; h.request("ui.move", json!({"x": 5, "y": 500}), t); diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index a8fbc638..0697b75c 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -280,8 +280,8 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } else if show_before { shown = draw(Slot::Before, img_rect); p.text(pos2(img_rect.left() + 8.0, img_rect.top() + 14.0), Align2::LEFT_CENTER, crate::i18n::tr("Before"), t.font(12.0), t.text); - } else if let Some((key, label)) = &hover_key - && let Some(tex) = app.renderer.textures.get(&Slot::Hover).filter(|t| t.photo == id && t.key == *key) + } else if let Some((_key, label)) = &hover_key + && let Some(tex) = app.renderer.textures.get(&Slot::Hover).filter(|t| t.photo == id) { p.image(tex.tex.id(), img_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); let g = p.layout_no_wrap(label.clone(), t.font(12.0), Color32::WHITE); diff --git a/crates/ui-egui/src/panels/presets.rs b/crates/ui-egui/src/panels/presets.rs index 74b1e129..20a095e0 100644 --- a/crates/ui-egui/src/panels/presets.rs +++ b/crates/ui-egui/src/panels/presets.rs @@ -78,7 +78,8 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let current = active.and_then(|id| app.session.develop_of(id)).map(|d| (*d).clone()); let thumbs = app.ui.preset_thumbs && active.is_some(); let row_h = if thumbs { 54.0 } else { 26.0 }; - egui::ScrollArea::vertical().id_salt("presets-scroll").auto_shrink([false, false]).show(ui, |ui| { + let scroll = egui::ScrollArea::vertical().id_salt("presets-scroll").auto_shrink([false, false]).show(ui, |ui| { + ui.spacing_mut().item_spacing.y = 0.0; let mut groups: BTreeMap> = BTreeMap::new(); for p in &app.session.presets { groups.entry(p.group.clone()).or_default().push(p.clone()); @@ -141,11 +142,16 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { if resp.hovered() && let Some(s) = look { - app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Preset: {name}", name = name), settings: s }); + let label = crate::i18n::tr_format!("Preset: {name}", name = name); + app.hover_preview = Some(HoverPreview { label: label.clone(), settings: s }); + if let Some(id) = active { + ui.data_mut(|d| d.insert_temp(egui::Id::new("last-preset-hover"), (id, pr.clone(), label))); + } } if resp.clicked() { let _ = app.run("preset.apply", json!({"id": pid, "amount": 100})); ui.data_mut(|d| d.insert_temp(amt_id, (pid.clone(), 100.0))); + ui.data_mut(|d| d.remove::<(lightcraft_catalog::PhotoId, Preset, String)>(egui::Id::new("last-preset-hover"))); app.toast(ui.ctx(), crate::i18n::tr_format!("Preset: {name}", name = name)); } let (builtin, group) = (pr.builtin, pr.group.clone()); @@ -207,6 +213,20 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } ui.add_space(30.0); }); + let last_hover_id = egui::Id::new("last-preset-hover"); + let pointer_in_scroll = ui.ctx().input(|i| i.pointer.hover_pos()).is_some_and(|pos| scroll.inner_rect.contains(pos)); + if pointer_in_scroll { + if app.hover_preview.is_none() + && let (Some(id), Some(curr)) = (active, current.as_ref()) + && let Some((saved_id, saved_preset, saved_label)) = + ui.data(|d| d.get_temp::<(lightcraft_catalog::PhotoId, Preset, String)>(last_hover_id)) + && saved_id == id + { + app.hover_preview = Some(HoverPreview { label: saved_label, settings: saved_preset.apply(curr, 1.0) }); + } + } else { + ui.data_mut(|d| d.remove::<(lightcraft_catalog::PhotoId, Preset, String)>(last_hover_id)); + } }); } diff --git a/crates/ui-egui/src/panels/profiles.rs b/crates/ui-egui/src/panels/profiles.rs index ff54f446..7633cdef 100644 --- a/crates/ui-egui/src/panels/profiles.rs +++ b/crates/ui-egui/src/panels/profiles.rs @@ -81,6 +81,20 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { ui.add_space(6.0); } ui.add_space(30.0); + let last_hover_id = egui::Id::new("last-profile-hover"); + let pointer_in_profiles = ui.ctx().input(|i| i.pointer.hover_pos()).is_some_and(|pos| ui.max_rect().contains(pos)); + if pointer_in_profiles { + if app.hover_preview.is_none() + && let Some((saved_id, saved_profile_id)) = ui.data(|m| m.get_temp::<(PhotoId, String)>(last_hover_id)) + && saved_id == id + && let Some(p) = profile(&saved_profile_id) + { + let s = with_profile(&d, p); + app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", p.name), settings: s }); + } + } else { + ui.data_mut(|m| m.remove::<(PhotoId, String)>(last_hover_id)); + } } /// `d` with profile `p` (at 100 % unless it is the applied one). @@ -127,6 +141,7 @@ fn cell_ui(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id: } if resp.hovered() && !applied && !over_star { app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", p.name), settings: s.clone() }); + ui.data_mut(|m| m.insert_temp(egui::Id::new("last-profile-hover"), (id, p.id.to_string()))); } let resp = resp.on_hover_text(format!("{} ({})", p.name, p.group)); if resp.clicked() { @@ -134,6 +149,7 @@ fn cell_ui(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id: let _ = app.run("profile.favorite", json!({"id": p.id})); } else if !applied { let _ = app.run("develop.profile", json!({"id": p.id, "amount": 100})); + ui.data_mut(|m| m.remove::<(PhotoId, String)>(egui::Id::new("last-profile-hover"))); } } }