diff --git a/crates/neomacs-display-runtime/src/menus/session.rs b/crates/neomacs-display-runtime/src/menus/session.rs index a5cd1fca0b..83500f8a24 100644 --- a/crates/neomacs-display-runtime/src/menus/session.rs +++ b/crates/neomacs-display-runtime/src/menus/session.rs @@ -5,11 +5,20 @@ use neomacs_display_protocol::{ menu::{MeasuredMenu, MenuPanel, MenuPanelRole}, }; +/// The latest accepted snapshot and whether it can still produce a result. +/// A closed snapshot is retained to reject stale show and hide requests. +#[derive(Default, Clone, Copy)] +enum MenuLifetimeState { + #[default] + Unseen, + Open(neomacs_display_protocol::menu::MenuToken), + Closed(neomacs_display_protocol::menu::MenuToken), +} + /// Revision ordering and immutable results, independent of native surfaces. #[derive(Default)] pub(super) struct MenuLifetime { - active: Option, - latest: Option, + state: MenuLifetimeState, results: std::collections::VecDeque, } @@ -21,37 +30,41 @@ impl MenuLifetime { )); } pub fn show(&mut self, token: neomacs_display_protocol::menu::MenuToken) -> bool { - if self.latest.is_some_and(|previous| { - (previous.session, previous.revision) >= (token.session, token.revision) - }) { - return false; + match self.state { + MenuLifetimeState::Unseen => {} + MenuLifetimeState::Open(previous) | MenuLifetimeState::Closed(previous) => { + if (previous.session, previous.revision) >= (token.session, token.revision) { + return false; + } + } } - self.latest = Some(token); - self.active = Some(token); + self.state = MenuLifetimeState::Open(token); true } pub fn close(&mut self) { - self.active = None; + if let MenuLifetimeState::Open(token) = self.state { + self.state = MenuLifetimeState::Closed(token); + } } pub fn hide(&mut self, token: neomacs_display_protocol::menu::MenuToken) -> bool { - if self.latest.is_none() - || self.latest == Some(token) - || (self.active.is_none() - && self.latest.is_some_and(|previous| { - (previous.session, previous.revision) < (token.session, token.revision) - })) - { - self.latest = Some(token); - self.close(); - return true; + let accepted = match self.state { + MenuLifetimeState::Unseen => true, + MenuLifetimeState::Open(previous) => previous == token, + MenuLifetimeState::Closed(previous) => { + (previous.session, previous.revision) <= (token.session, token.revision) + } + }; + if accepted { + self.state = MenuLifetimeState::Closed(token); } - false + accepted } pub fn finish(&mut self, index: i32) { - if let Some(token) = self.active.take() { + if let MenuLifetimeState::Open(token) = self.state { + self.state = MenuLifetimeState::Closed(token); self.results .push_back(neomacs_display_protocol::menu::MenuResult::from_index( token, index, diff --git a/crates/neomacs-display-runtime/src/menus/session/tests/session_test.rs b/crates/neomacs-display-runtime/src/menus/session/tests/session_test.rs index 2e69c4f262..da4560a428 100644 --- a/crates/neomacs-display-runtime/src/menus/session/tests/session_test.rs +++ b/crates/neomacs-display-runtime/src/menus/session/tests/session_test.rs @@ -987,3 +987,71 @@ fn menu_columns_and_paint_positions_use_measured_cjk_advances() { 168.0 ); } + +#[test] +fn close_keeps_the_revision_without_delivering_a_result() { + let token = neomacs_display_protocol::menu::MenuToken { + session: 100, + revision: 1, + }; + let mut lifetime = MenuLifetime::default(); + lifetime.close(); + assert!(lifetime.show(token)); + lifetime.close(); + lifetime.close(); + lifetime.finish(2); + assert!(lifetime.take_result().is_none()); + assert!(!lifetime.show(token)); + assert!(lifetime.show(neomacs_display_protocol::menu::MenuToken { + revision: 2, + ..token + })); +} + +#[test] +fn newer_hide_only_advances_the_revision_after_the_menu_closes() { + let current = neomacs_display_protocol::menu::MenuToken { + session: 100, + revision: 1, + }; + let newer = neomacs_display_protocol::menu::MenuToken { + revision: 2, + ..current + }; + let mut lifetime = MenuLifetime::default(); + assert!(lifetime.show(current)); + assert!(!lifetime.hide(newer)); + lifetime.finish(3); + let result = lifetime.take_result().unwrap(); + assert_eq!(result.token, current); + assert_eq!(result.index(), 3); + assert!(lifetime.hide(newer)); + assert!(!lifetime.hide(current)); + assert!(!lifetime.show(newer)); + lifetime.finish(4); + assert!(lifetime.take_result().is_none()); +} + +#[test] +fn rejection_preserves_the_open_menu_and_finish_delivers_once() { + let current = neomacs_display_protocol::menu::MenuToken { + session: 100, + revision: 1, + }; + let rejected = neomacs_display_protocol::menu::MenuToken { + session: 101, + revision: 1, + }; + let mut lifetime = MenuLifetime::default(); + assert!(lifetime.show(current)); + lifetime.reject(rejected); + lifetime.finish(2); + lifetime.finish(3); + let rejection = lifetime.take_result().unwrap(); + assert_eq!(rejection.token, rejected); + assert_eq!(rejection.index(), -1); + let selection = lifetime.take_result().unwrap(); + assert_eq!(selection.token, current); + assert_eq!(selection.index(), 2); + assert!(lifetime.take_result().is_none()); +}