Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 34 additions & 21 deletions crates/neomacs-display-runtime/src/menus/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<neomacs_display_protocol::menu::MenuToken>,
latest: Option<neomacs_display_protocol::menu::MenuToken>,
state: MenuLifetimeState,
results: std::collections::VecDeque<neomacs_display_protocol::menu::MenuResult>,
}

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Loading