diff --git a/contributors/people.toml b/contributors/people.toml index e153870f..79f20c72 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -27,8 +27,3 @@ sync_github_name = true [people.sanyamsingh001] display_name = "sanyamsingh001" - -[people.switch87] -real_name = "Gert Pellin" -display_name = "Gert Pellin" -sync_github_name = false diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1a80acf0..8248ee5a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -118,8 +118,6 @@ mod tests_temp_slider; #[cfg(test)] mod tests_titlebar; #[cfg(test)] -mod tests_toast; -#[cfg(test)] mod tests_unsaved; #[cfg(test)] mod tests_wb_selector; diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 7a0dfb31..a8d99aed 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -69,9 +69,6 @@ pub fn resizable_side( } /// The HUD toast at the bottom centre of the canvas. -/// -/// Drawn as widgets rather than a painted galley so the message can be selected and copied: an -/// error here often carries a path or a command the user wants to paste somewhere else. pub fn toast(app: &mut LightcraftApp, ctx: &egui::Context) { let now = ctx.input(|i| i.time); let Some((text, until, label)) = app.ui.toast.clone() else { return }; @@ -82,27 +79,16 @@ pub fn toast(app: &mut LightcraftApp, ctx: &egui::Context) { let Some(canvas) = app.canvas_rect else { return }; let t = Tokens::get(ctx); let fade = ((until - now) / 0.25).clamp(0.0, 1.0) as f32; - let (background, foreground) = label.map(crate::theme::label_toast_colors).unwrap_or((egui::Color32::from_black_alpha(200), t.text)); - let background = background.gamma_multiply(fade); - let foreground = foreground.gamma_multiply(fade); + let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::new("toast"))); // long messages (a save warning, the web build's notices) wrap within the canvas let wrap = (canvas.width() - 80.0).clamp(200.0, 720.0); - egui::Area::new(egui::Id::new("toast")) - .order(egui::Order::Foreground) - .movable(false) - // (the toast fades by its own clock) - .fade_in(false) - .pivot(Align2::CENTER_CENTER) - .fixed_pos(pos2(canvas.center().x, canvas.bottom() - 60.0)) - .show(ctx, |ui| { - ui.set_max_width(wrap); - egui::Frame::NONE.fill(background).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::symmetric(20, 11)).show( - ui, - |ui| { - ui.add(egui::Label::new(egui::RichText::new(text).font(t.font(14.0)).color(foreground)).wrap()); - }, - ); - }); + let (background, foreground) = label.map(crate::theme::label_toast_colors).unwrap_or((egui::Color32::from_black_alpha(200), t.text)); + let foreground = foreground.gamma_multiply(fade); + let galley = painter.layout(text, t.font(14.0), foreground, wrap); + let size = vec2(galley.size().x + 40.0, (galley.size().y + 22.0).max(40.0)); + let r = Rect::from_center_size(pos2(canvas.center().x, canvas.bottom() - 60.0), size); + painter.rect_filled(r, 6.0, background.gamma_multiply(fade)); + painter.galley(r.center() - galley.size() / 2.0, galley, foreground); ctx.request_repaint_after(std::time::Duration::from_millis(30)); } diff --git a/crates/ui-egui/src/tests_labels.rs b/crates/ui-egui/src/tests_labels.rs index 9869223b..9325ab77 100644 --- a/crates/ui-egui/src/tests_labels.rs +++ b/crates/ui-egui/src/tests_labels.rs @@ -75,8 +75,6 @@ fn label_tints_grid_and_filmstrip_chrome_without_changing_photo_pixels() { fn label_toast_has_pale_colour_and_rating_restores_neutral_style() { let mut h = app(ViewMode::SquareGrid); h.app.run("photo.label", json!({"label": "red"})).unwrap(); - // (two frames: the toast is an egui Area, sized in its first frame and painted from the second, #1262) - h.step(); h.step(); let at = egui::pos2(h.app.canvas_rect.unwrap().center().x, h.app.canvas_rect.unwrap().bottom() - 45.0); let red = pixel(&h.paint(), at); diff --git a/crates/ui-egui/src/tests_toast.rs b/crates/ui-egui/src/tests_toast.rs deleted file mode 100644 index cf671d24..00000000 --- a/crates/ui-egui/src/tests_toast.rs +++ /dev/null @@ -1,58 +0,0 @@ -//! The toast is drawn as widgets, not as paint. That is what makes its message selectable: a toast -//! often carries a path or a command the user wants somewhere else, and painted text cannot be -//! picked up by the pointer at all. - -use std::time::Duration; - -use crate::headless::Headless; -use crate::{LightcraftApp, Services}; - -const T: Duration = Duration::from_secs(20); - -fn demo() -> Headless { - let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); - let mut h = Headless::new(app, [1200.0, 800.0], 1.0); - // let the first renders finish: a render still running when the test binary exits can crash in the - // GPU driver's teardown (the preview pool doesn't join its workers) - h.settle(T); - h -} - -/// One event per frame: [`LightcraftApp::raw_input_hook`] takes pointer events singly. -fn send(h: &mut Headless, e: egui::Event) { - h.app.synthetic.push(e); - h.step(); -} - -fn show_toast(h: &mut Headless, msg: &str) -> egui::Rect { - h.app.ui.toast = Some((msg.to_owned(), f64::MAX, None)); - h.step(); - h.step(); - h.view.ctx.memory(|m| m.area_rect(egui::Id::new("toast"))).expect("the toast is laid out as a widget, not paint") -} - -#[test] -fn the_toast_is_a_widget_the_pointer_reaches() { - let mut h = demo(); - let msg = "Exported frame to /tmp/scene-0001.png"; - let area = show_toast(&mut h, msg); - - assert!(area.is_positive(), "the toast has a rect of its own: {area:?}"); - assert!(h.painted_text().iter().any(|t| t.contains("Exported frame")), "the message is still painted"); - - // The pointer reaches the message: egui offers the text cursor over it. Painted text is never - // hit-tested, so this is the difference that makes selection possible at all. - send(&mut h, egui::Event::PointerMoved(area.center())); - assert_eq!(h.last_cursor, egui::CursorIcon::Text, "the toast text is a text target"); -} - -#[test] -fn the_toast_still_wraps_a_long_message_inside_the_canvas() { - let mut h = demo(); - let msg = "The SAM 3 download failed: the file is 4.1 GB and the connection dropped at 98 %, so nothing was installed and the masks are still unavailable"; - let area = show_toast(&mut h, msg); - - let canvas = h.app.canvas_rect.expect("the demo has a canvas"); - assert!(area.width() <= canvas.width() - 80.0, "the toast wraps within the canvas: {area:?}"); - assert!(h.painted_text().iter().any(|t| t.contains("SAM 3 download failed")), "the whole message is painted"); -}