diff --git a/contributors/people.toml b/contributors/people.toml index a715fbcc..bc8cf71b 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -41,3 +41,6 @@ real_name = "Justin John" [people.Nael-Nathanael] sync_github_name = true + +[people.volkovskey] +real_name = "Andrii Volkov" diff --git a/crates/ui-egui/src/idle_tests.rs b/crates/ui-egui/src/idle_tests.rs index c61699b5..57a5cf3c 100644 --- a/crates/ui-egui/src/idle_tests.rs +++ b/crates/ui-egui/src/idle_tests.rs @@ -190,6 +190,25 @@ fn a_toast_wakes_the_ui_only_to_go_away() { assert!(h.app.ui.toast.is_none(), "gone after 3 s"); } +#[test] +fn a_toast_raised_by_a_slow_frame_still_shows_for_3_s() { + let (mut h, _) = document(); + h.settle(); + // The Open dialog and a failed open block the frame that raises the error: the next frame + // comes 5 s after it started. + h.app.toast("can't open file.fig"); + h.frame(vec![]); + h.time += 5.0; + h.frame(vec![]); + assert!(h.app.ui.toast.is_some(), "shown, not expired by the wait"); + h.time += 2.0; + h.frame(vec![]); + assert!(h.app.ui.toast.is_some()); + h.time += 1.5; + h.frame(vec![]); + assert!(h.app.ui.toast.is_none(), "gone after 3 s"); +} + #[test] fn a_finished_background_render_wakes_the_ui() { let ctx = egui::Context::default(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index b727caa1..e9accbc0 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -337,6 +337,17 @@ pub type AutoSaveEncoding = std::thread::JoinHandle<(figcraft_engine::cmd::file: /// How long a toast shows (seconds). const TOAST_SECS: f64 = 3.0; +/// How long a toast has been showing ([`UiState::toast`]). +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ToastClock { + /// Raised, not drawn yet. + Raised, + /// Drawn once; the next frame starts the clock. + Drawn, + /// Showing since this time (UI clock, seconds). + Since(f64), +} + /// Draw another frame `secs` from now (nothing else needs drawing until then: an idle window draws /// no frames). #[track_caller] @@ -402,8 +413,8 @@ pub struct UiState { pub title_bar: TitleBar, /// Every icon on one screen, for reviewing the set (control channel `ui.set {iconGallery}`). pub icon_gallery: bool, - /// A message at the bottom of the window and when it first showed (UI clock, seconds). - pub toast: Option<(String, Option)>, + /// A message at the bottom of the window and how long it has been showing. + pub toast: Option<(String, ToastClock)>, pub present: Option, /// The full-window presenter's options (how it sizes the prototype), kept between /// presentations. @@ -740,7 +751,7 @@ impl FigApp { } pub fn toast(&mut self, msg: impl Into) { - self.ui.toast = Some((msg.into(), None)); + self.ui.toast = Some((msg.into(), ToastClock::Raised)); } /// Say that `n` documents left unsaved by a crash were opened again from recovery copies. @@ -1379,16 +1390,32 @@ impl FigApp { } fn toast_ui(&mut self, ctx: &egui::Context) { - let Some((msg, shown)) = &mut self.ui.toast else { return }; + let Some((msg, clock)) = &mut self.ui.toast else { return }; let now = ctx.input(|i| i.time); - let left = TOAST_SECS - (now - *shown.get_or_insert(now)); + // The clock starts on the frame after the one that first draws the toast: that frame's + // time was taken before whatever raised it ran (a file dialog, a long open), and would + // count that wait against it, so a failed open's message expired before it was seen. + let left = match *clock { + ToastClock::Raised => { + *clock = ToastClock::Drawn; + ctx.request_repaint(); + TOAST_SECS + } + ToastClock::Drawn => { + *clock = ToastClock::Since(now); + TOAST_SECS + } + ToastClock::Since(t) => TOAST_SECS - (now - t), + }; if left <= 0.0 { self.ui.toast = None; return; } let msg = msg.clone(); // Nothing moves while it shows: the next frame it needs is the one that hides it. - repaint_in(ctx, left); + if matches!(clock, ToastClock::Since(_)) { + repaint_in(ctx, left); + } let screen = ctx.content_rect(); egui::Area::new(egui::Id::new("toast")) .fixed_pos(egui::pos2(screen.center().x, screen.max.y - 96.0))