use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use teksilo_core::AppEventPoster;
use teksilo_core::MenuItemId;
use teksilo_core::widget::EventContext;
use teksilo_core::window::TeksiloWindowId;
#[cfg(target_os = "macos")]
mod macos;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NativeCheck {
#[default]
None,
Off,
On,
Mixed,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NativeKeyEquivalent {
pub key: String,
pub command: bool,
pub shift: bool,
pub alt: bool,
pub control: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StandardMenuRole {
App,
Window,
Help,
}
#[derive(Debug, Clone, Default)]
pub struct StandardLabels {
pub title: String,
pub about: String,
pub settings: String,
pub services: String,
pub hide: String,
pub hide_others: String,
pub show_all: String,
pub quit: String,
pub minimize: String,
pub zoom: String,
}
#[derive(Debug, Clone)]
pub struct StandardRoutedItem {
pub id: MenuItemId,
pub key_equiv: Option<NativeKeyEquivalent>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum NativeMenuNode {
Item {
id: MenuItemId,
title: String,
key_equiv: Option<NativeKeyEquivalent>,
enabled: bool,
visible: bool,
check: NativeCheck,
},
Submenu {
title: String,
children: Vec<NativeMenuNode>,
},
Separator,
Standard {
role: StandardMenuRole,
labels: StandardLabels,
quit_item: Option<StandardRoutedItem>,
settings_item: Option<StandardRoutedItem>,
},
}
#[derive(Debug, Clone, Default)]
pub struct NativeMenuSnapshot {
pub roots: Vec<NativeMenuNode>,
}
#[derive(Debug, Clone, Default)]
pub struct MenuItemDelta {
pub enabled: Option<bool>,
pub visible: Option<bool>,
pub check: Option<NativeCheck>,
pub title: Option<String>,
pub key_equiv: Option<Option<NativeKeyEquivalent>>,
}
pub type MenuActionFn = Rc<dyn Fn(&mut EventContext)>;
#[derive(Clone, Default)]
pub struct NativeMenuActivation {
pub intent: Option<&'static str>,
pub action: Option<MenuActionFn>,
pub quits: bool,
}
impl std::fmt::Debug for NativeMenuActivation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NativeMenuActivation")
.field("intent", &self.intent)
.field("action", &self.action.as_ref().map(|_| "<closure>"))
.field("quits", &self.quits)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct NativeMenuEventPayload {
pub window_id_owner: TeksiloWindowId,
pub item_id: MenuItemId,
}
#[derive(Debug, Clone)]
pub struct NativeAppMenuEventPayload {
pub item_id: MenuItemId,
}
pub trait NativeMenuBackend {
fn set_window_menu(
&mut self,
window_id: TeksiloWindowId,
menu: NativeMenuSnapshot,
poster: Arc<dyn AppEventPoster>,
);
fn activate_window(&mut self, window_id: TeksiloWindowId);
#[doc(hidden)]
fn set_window_owner(&mut self, window_id: TeksiloWindowId, owner: Option<TeksiloWindowId>) {
let _ = (window_id, owner);
}
fn clear_window(&mut self, window_id: TeksiloWindowId);
fn update_item(&mut self, id: MenuItemId, delta: MenuItemDelta);
#[doc(hidden)]
fn shows_bar(&self) -> bool {
true
}
fn set_app_menu(
&mut self,
menu: NativeMenuSnapshot,
poster: Arc<dyn AppEventPoster>,
declared: bool,
) {
let _ = (menu, poster, declared);
}
}
impl NativeMenuBackend for Box<dyn NativeMenuBackend> {
fn set_window_menu(
&mut self,
window_id: TeksiloWindowId,
menu: NativeMenuSnapshot,
poster: Arc<dyn AppEventPoster>,
) {
(**self).set_window_menu(window_id, menu, poster)
}
fn activate_window(&mut self, window_id: TeksiloWindowId) {
(**self).activate_window(window_id)
}
fn set_window_owner(&mut self, window_id: TeksiloWindowId, owner: Option<TeksiloWindowId>) {
(**self).set_window_owner(window_id, owner)
}
fn clear_window(&mut self, window_id: TeksiloWindowId) {
(**self).clear_window(window_id)
}
fn update_item(&mut self, id: MenuItemId, delta: MenuItemDelta) {
(**self).update_item(id, delta)
}
fn shows_bar(&self) -> bool {
(**self).shows_bar()
}
fn set_app_menu(
&mut self,
menu: NativeMenuSnapshot,
poster: Arc<dyn AppEventPoster>,
declared: bool,
) {
(**self).set_app_menu(menu, poster, declared)
}
}
type WindowActivations = HashMap<MenuItemId, NativeMenuActivation>;
struct Gate {
intent: &'static str,
own: bool,
handled: bool,
shown: bool,
}
struct NativeMenuState {
backend: RefCell<Box<dyn NativeMenuBackend>>,
activations: RefCell<HashMap<TeksiloWindowId, WindowActivations>>,
app_activations: RefCell<WindowActivations>,
gates: RefCell<HashMap<MenuItemId, Gate>>,
refreshed: RefCell<Option<Vec<&'static str>>>,
#[cfg(test)]
evaluations: std::cell::Cell<usize>,
}
#[derive(Clone)]
pub struct NativeMenuHandle {
inner: Rc<NativeMenuState>,
}
impl NativeMenuHandle {
pub fn new<B: NativeMenuBackend + 'static>(backend: B) -> Self {
Self {
inner: Rc::new(NativeMenuState {
backend: RefCell::new(Box::new(backend)),
activations: RefCell::new(HashMap::new()),
app_activations: RefCell::new(HashMap::new()),
gates: RefCell::new(HashMap::new()),
refreshed: RefCell::new(None),
#[cfg(test)]
evaluations: std::cell::Cell::new(0),
}),
}
}
pub fn set_window_menu(
&self,
window_id: TeksiloWindowId,
menu: NativeMenuSnapshot,
activations: HashMap<MenuItemId, NativeMenuActivation>,
poster: Arc<dyn AppEventPoster>,
) {
self.gate_rows(&menu, &activations);
self.inner
.activations
.borrow_mut()
.insert(window_id, activations);
self.inner
.backend
.borrow_mut()
.set_window_menu(window_id, menu, poster);
}
pub fn activate_window(&self, window_id: TeksiloWindowId) {
self.inner.backend.borrow_mut().activate_window(window_id);
}
#[doc(hidden)]
pub fn set_window_owner(&self, window_id: TeksiloWindowId, owner: Option<TeksiloWindowId>) {
self.inner
.backend
.borrow_mut()
.set_window_owner(window_id, owner);
}
pub fn clear_window(&self, window_id: TeksiloWindowId) {
self.inner.activations.borrow_mut().remove(&window_id);
self.forget_dead_gates();
self.inner.backend.borrow_mut().clear_window(window_id);
}
pub fn update_item(&self, id: MenuItemId, mut delta: MenuItemDelta) {
if let Some(own) = delta.enabled
&& let Some(gate) = self.inner.gates.borrow_mut().get_mut(&id)
{
gate.own = own;
gate.shown = own && gate.handled;
delta.enabled = Some(gate.shown);
}
self.inner.backend.borrow_mut().update_item(id, delta);
}
#[doc(hidden)]
pub fn refresh_availability(&self, handled: &[&'static str]) {
if self
.inner
.refreshed
.borrow()
.as_deref()
.is_some_and(|last| last == handled)
{
return;
}
*self.inner.refreshed.borrow_mut() = Some(handled.to_vec());
let mut changed = Vec::new();
for (id, gate) in self.inner.gates.borrow_mut().iter_mut() {
#[cfg(test)]
self.inner.evaluations.set(self.inner.evaluations.get() + 1);
gate.handled = handled.binary_search(&gate.intent).is_ok();
let shown = gate.own && gate.handled;
if shown != gate.shown {
gate.shown = shown;
changed.push((*id, shown));
}
}
let mut backend = self.inner.backend.borrow_mut();
for (id, enabled) in changed {
backend.update_item(
id,
MenuItemDelta {
enabled: Some(enabled),
..Default::default()
},
);
}
}
#[doc(hidden)]
pub fn shows_bar(&self) -> bool {
self.inner.backend.borrow().shows_bar()
}
#[cfg(test)]
fn evaluations(&self) -> usize {
self.inner.evaluations.get()
}
#[doc(hidden)]
pub fn gated_enabled(&self, id: MenuItemId) -> Option<bool> {
self.inner.gates.borrow().get(&id).map(|gate| gate.shown)
}
fn gate_rows(&self, menu: &NativeMenuSnapshot, activations: &WindowActivations) {
fn own_enabled(nodes: &[NativeMenuNode], id: MenuItemId) -> Option<bool> {
nodes.iter().find_map(|node| match node {
NativeMenuNode::Item {
id: item, enabled, ..
} if *item == id => Some(*enabled),
NativeMenuNode::Submenu { children, .. } => own_enabled(children, id),
_ => None,
})
}
let mut gates = self.inner.gates.borrow_mut();
*self.inner.refreshed.borrow_mut() = None;
for (id, activation) in activations {
let (Some(intent), None, false) =
(activation.intent, &activation.action, activation.quits)
else {
continue;
};
let own = own_enabled(&menu.roots, *id).unwrap_or(true);
gates.insert(
*id,
Gate {
intent,
own,
handled: true,
shown: own,
},
);
}
}
fn forget_dead_gates(&self) {
let windows = self.inner.activations.borrow();
let app = self.inner.app_activations.borrow();
self.inner.gates.borrow_mut().retain(|id, _| {
app.contains_key(id) || windows.values().any(|menu| menu.contains_key(id))
});
}
pub fn set_app_menu(
&self,
menu: NativeMenuSnapshot,
activations: HashMap<MenuItemId, NativeMenuActivation>,
poster: Arc<dyn AppEventPoster>,
declared: bool,
) {
self.gate_rows(&menu, &activations);
*self.inner.app_activations.borrow_mut() = activations;
self.forget_dead_gates();
self.inner
.backend
.borrow_mut()
.set_app_menu(menu, poster, declared);
}
pub fn app_activation(&self, id: MenuItemId) -> Option<NativeMenuActivation> {
self.inner.app_activations.borrow().get(&id).cloned()
}
pub fn activation(
&self,
window_id: TeksiloWindowId,
id: MenuItemId,
) -> Option<NativeMenuActivation> {
self.inner
.activations
.borrow()
.get(&window_id)
.and_then(|m| m.get(&id).cloned())
}
}
impl std::fmt::Debug for NativeMenuHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NativeMenuHandle")
.field("windows", &self.inner.activations.borrow().len())
.finish_non_exhaustive()
}
}
#[doc(hidden)]
pub fn key_window_bar(
key: TeksiloWindowId,
has_menu: impl Fn(TeksiloWindowId) -> bool,
owner: impl Fn(TeksiloWindowId) -> Option<TeksiloWindowId>,
app_declared: bool,
shown: Option<TeksiloWindowId>,
) -> Option<TeksiloWindowId> {
let mut window = key;
let mut visited = vec![window];
loop {
if has_menu(window) {
return Some(window);
}
match owner(window) {
Some(next) if !visited.contains(&next) => {
visited.push(next);
window = next;
}
_ => break,
}
}
if app_declared { None } else { shown }
}
pub(crate) fn bar_after_app_menu(
key_bar: Option<Option<TeksiloWindowId>>,
shown: Option<TeksiloWindowId>,
) -> Option<Option<TeksiloWindowId>> {
match key_bar {
Some(bar) => (bar.is_none() || bar != shown).then_some(bar),
None => shown.is_none().then_some(None),
}
}
pub(crate) fn bar_after_clear(
cleared: TeksiloWindowId,
key: Option<TeksiloWindowId>,
shown: Option<TeksiloWindowId>,
has_menu: impl Fn(TeksiloWindowId) -> bool,
owner: impl Fn(TeksiloWindowId) -> Option<TeksiloWindowId>,
app_declared: bool,
) -> Option<Option<TeksiloWindowId>> {
if shown != Some(cleared) {
return None;
}
Some(key.filter(|key| *key != cleared).and_then(|key| {
key_window_bar(
key,
|window| window != cleared && has_menu(window),
|window| owner(window).filter(|owner| *owner != cleared),
app_declared,
None,
)
}))
}
#[derive(Default)]
pub struct NoopNativeMenuBackend;
impl NoopNativeMenuBackend {
pub fn new() -> Self {
Self
}
}
impl NativeMenuBackend for NoopNativeMenuBackend {
fn set_window_menu(
&mut self,
_window_id: TeksiloWindowId,
_menu: NativeMenuSnapshot,
_poster: Arc<dyn AppEventPoster>,
) {
}
fn activate_window(&mut self, _window_id: TeksiloWindowId) {}
fn clear_window(&mut self, _window_id: TeksiloWindowId) {}
fn update_item(&mut self, _id: MenuItemId, _delta: MenuItemDelta) {}
fn shows_bar(&self) -> bool {
false
}
}
#[cfg(any(target_os = "macos", test))]
#[derive(Debug)]
pub(crate) struct MacBarPlan<'a> {
pub(crate) menus: Vec<&'a NativeMenuNode>,
pub(crate) windows_menu: Option<usize>,
pub(crate) help_menu: Option<usize>,
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn mac_bar_plan(snapshot: &NativeMenuSnapshot) -> MacBarPlan<'_> {
let is_app = |node: &NativeMenuNode| {
matches!(
node,
NativeMenuNode::Standard {
role: StandardMenuRole::App,
..
}
)
};
let app = snapshot.roots.iter().find(|node| is_app(node));
let rest = snapshot.roots.iter().filter(|node| {
!is_app(node)
&& matches!(
node,
NativeMenuNode::Submenu { .. } | NativeMenuNode::Standard { .. }
)
});
let menus: Vec<&NativeMenuNode> = app.into_iter().chain(rest).collect();
let first = |wanted: StandardMenuRole| {
menus.iter().position(
|node| matches!(node, NativeMenuNode::Standard { role, .. } if *role == wanted),
)
};
MacBarPlan {
windows_menu: first(StandardMenuRole::Window),
help_menu: first(StandardMenuRole::Help),
menus,
}
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn mac_relists_window(excluded: bool, visible: bool, miniaturized: bool) -> bool {
!excluded && (visible || miniaturized)
}
#[cfg(any(target_os = "macos", test))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MacAppMenuRow {
About,
Settings,
Services,
Hide,
HideOthers,
ShowAll,
Quit,
Separator,
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn mac_app_menu_rows(settings: bool) -> Vec<MacAppMenuRow> {
use MacAppMenuRow::*;
let mut rows = vec![About, Separator];
if settings {
rows.extend([Settings, Separator]);
}
rows.extend([
Services, Separator, Hide, HideOthers, ShowAll, Separator, Quit,
]);
rows
}
pub fn default_backend() -> Box<dyn NativeMenuBackend> {
#[cfg(target_os = "macos")]
{
Box::new(macos::MacOsNativeMenuBackend::new())
}
#[cfg(not(target_os = "macos"))]
{
Box::new(NoopNativeMenuBackend::new())
}
}
#[derive(Clone, Default)]
pub struct MemoryNativeMenuBackend {
inner: Rc<RefCell<MemoryRecording>>,
}
#[derive(Default)]
struct MemoryRecording {
menus: HashMap<TeksiloWindowId, NativeMenuSnapshot>,
app_menu: Option<NativeMenuSnapshot>,
key: Option<TeksiloWindowId>,
active: Option<TeksiloWindowId>,
owners: HashMap<TeksiloWindowId, TeksiloWindowId>,
app_declared: bool,
deltas: Vec<(MenuItemId, MenuItemDelta)>,
cleared: Vec<TeksiloWindowId>,
}
impl MemoryRecording {
fn bar_for(&self, key: TeksiloWindowId) -> Option<TeksiloWindowId> {
key_window_bar(
key,
|window| self.menus.contains_key(&window),
|window| self.owners.get(&window).copied(),
self.app_declared,
self.active,
)
}
}
impl MemoryNativeMenuBackend {
pub fn new() -> Self {
Self::default()
}
pub fn menu_for(&self, window_id: TeksiloWindowId) -> Option<NativeMenuSnapshot> {
self.inner.borrow().menus.get(&window_id).cloned()
}
pub fn active_window(&self) -> Option<TeksiloWindowId> {
self.inner.borrow().active
}
pub fn shows_app_menu(&self) -> bool {
self.inner.borrow().active.is_none()
}
pub fn deltas(&self) -> Vec<(MenuItemId, MenuItemDelta)> {
self.inner.borrow().deltas.clone()
}
pub fn cleared(&self) -> Vec<TeksiloWindowId> {
self.inner.borrow().cleared.clone()
}
pub fn app_menu(&self) -> Option<NativeMenuSnapshot> {
self.inner.borrow().app_menu.clone()
}
}
impl NativeMenuBackend for MemoryNativeMenuBackend {
fn set_window_menu(
&mut self,
window_id: TeksiloWindowId,
menu: NativeMenuSnapshot,
_poster: Arc<dyn AppEventPoster>,
) {
let mut rec = self.inner.borrow_mut();
rec.menus.insert(window_id, menu);
rec.active = match rec.key {
Some(key) => rec.bar_for(key),
None => Some(window_id),
};
}
fn activate_window(&mut self, window_id: TeksiloWindowId) {
let mut rec = self.inner.borrow_mut();
rec.key = Some(window_id);
rec.active = rec.bar_for(window_id);
}
fn set_window_owner(&mut self, window_id: TeksiloWindowId, owner: Option<TeksiloWindowId>) {
let mut rec = self.inner.borrow_mut();
match owner {
Some(owner) => rec.owners.insert(window_id, owner),
None => rec.owners.remove(&window_id),
};
}
fn clear_window(&mut self, window_id: TeksiloWindowId) {
let mut rec = self.inner.borrow_mut();
rec.menus.remove(&window_id);
rec.owners.remove(&window_id);
rec.cleared.push(window_id);
let bar = bar_after_clear(
window_id,
rec.key,
rec.active,
|window| rec.menus.contains_key(&window),
|window| rec.owners.get(&window).copied(),
rec.app_declared,
);
if let Some(bar) = bar {
rec.active = bar;
}
}
fn update_item(&mut self, id: MenuItemId, delta: MenuItemDelta) {
self.inner.borrow_mut().deltas.push((id, delta));
}
fn set_app_menu(
&mut self,
menu: NativeMenuSnapshot,
_poster: Arc<dyn AppEventPoster>,
declared: bool,
) {
let mut rec = self.inner.borrow_mut();
rec.app_menu = Some(menu);
rec.app_declared = declared;
let key_bar = rec.key.map(|key| rec.bar_for(key));
if let Some(bar) = bar_after_app_menu(key_bar, rec.active) {
rec.active = bar;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use teksilo_core::SubscriptionId;
struct NullPoster;
impl AppEventPoster for NullPoster {
fn post_subscription_event(
&self,
_sub_id: SubscriptionId,
_event: Box<dyn std::any::Any + Send>,
) {
}
fn post_external(&self, _payload: Box<dyn std::any::Any + Send>) {}
}
fn poster() -> Arc<dyn AppEventPoster> {
Arc::new(NullPoster)
}
fn win(n: u64) -> TeksiloWindowId {
TeksiloWindowId::new(n)
}
fn sample_snapshot(id: MenuItemId) -> NativeMenuSnapshot {
NativeMenuSnapshot {
roots: vec![NativeMenuNode::Submenu {
title: "File".into(),
children: vec![NativeMenuNode::Item {
id,
title: "New".into(),
key_equiv: None,
enabled: true,
visible: true,
check: NativeCheck::None,
}],
}],
}
}
#[test]
fn set_menu_records_snapshot_and_activations() {
let backend = MemoryNativeMenuBackend::new();
let handle = NativeMenuHandle::new(backend.clone());
let id = MenuItemId::next();
let fired = Arc::new(Mutex::new(false));
let fired2 = fired.clone();
let mut acts = HashMap::new();
acts.insert(
id,
NativeMenuActivation {
intent: Some("app.new"),
action: Some(Rc::new(move |_ctx: &mut EventContext| {
*fired2.lock().unwrap() = true;
})),
quits: false,
},
);
handle.set_window_menu(win(1), sample_snapshot(id), acts, poster());
assert!(backend.menu_for(win(1)).is_some());
assert_eq!(backend.active_window(), Some(win(1)));
let act = handle.activation(win(1), id).expect("activation recorded");
assert_eq!(act.intent, Some("app.new"));
assert!(act.action.is_some());
}
#[test]
fn activate_and_clear_window() {
let backend = MemoryNativeMenuBackend::new();
let handle = NativeMenuHandle::new(backend.clone());
let id = MenuItemId::next();
handle.set_window_menu(win(1), sample_snapshot(id), HashMap::new(), poster());
handle.set_window_menu(
win(2),
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
);
handle.activate_window(win(2));
assert_eq!(backend.active_window(), Some(win(2)));
handle.clear_window(win(2));
assert_eq!(backend.cleared(), vec![win(2)]);
assert!(handle.activation(win(2), id).is_none());
assert!(backend.menu_for(win(2)).is_none());
}
#[test]
fn activating_a_window_without_a_menu_shows_the_application_menu() {
let backend = MemoryNativeMenuBackend::new();
let handle = NativeMenuHandle::new(backend.clone());
handle.set_app_menu(
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
true,
);
handle.set_window_menu(
win(1),
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
);
handle.activate_window(win(1));
assert_eq!(backend.active_window(), Some(win(1)));
handle.activate_window(win(2));
assert_eq!(backend.active_window(), None);
assert!(backend.shows_app_menu());
handle.activate_window(win(1));
assert_eq!(backend.active_window(), Some(win(1)), "and back");
}
#[test]
fn the_key_window_decides_and_its_close_leaves_the_application_menu() {
let backend = MemoryNativeMenuBackend::new();
let handle = NativeMenuHandle::new(backend.clone());
handle.activate_window(win(1));
handle.set_window_menu(
win(2),
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
);
assert!(backend.shows_app_menu(), "window 1 is key and has none");
handle.set_window_menu(
win(1),
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
);
assert_eq!(backend.active_window(), Some(win(1)));
handle.clear_window(win(1));
assert!(backend.shows_app_menu());
}
#[test]
fn an_intent_only_row_greys_out_while_nothing_takes_its_intent() {
let backend = MemoryNativeMenuBackend::new();
let handle = NativeMenuHandle::new(backend.clone());
let (plain, with_action, quit) =
(MenuItemId::next(), MenuItemId::next(), MenuItemId::next());
let activation = |action: bool, quits: bool| NativeMenuActivation {
intent: Some("app.print"),
action: action.then(|| -> MenuActionFn { Rc::new(|_| {}) }),
quits,
};
handle.set_window_menu(
win(1),
sample_snapshot(plain),
[
(plain, activation(false, false)),
(with_action, activation(true, false)),
(quit, activation(false, true)),
]
.into_iter()
.collect(),
poster(),
);
let enabled_deltas = |backend: &MemoryNativeMenuBackend| {
backend
.deltas()
.into_iter()
.filter_map(|(id, delta)| delta.enabled.map(|enabled| (id, enabled)))
.collect::<Vec<_>>()
};
handle.refresh_availability(&["app.print"]);
assert!(enabled_deltas(&backend).is_empty(), "nothing changed");
handle.refresh_availability(&[]);
assert_eq!(enabled_deltas(&backend), [(plain, false)]);
assert_eq!(handle.gated_enabled(plain), Some(false));
assert_eq!(handle.gated_enabled(with_action), None);
assert_eq!(handle.gated_enabled(quit), None);
handle.update_item(
plain,
MenuItemDelta {
enabled: Some(true),
..Default::default()
},
);
assert_eq!(enabled_deltas(&backend).last(), Some(&(plain, false)));
handle.refresh_availability(&["app.print"]);
assert_eq!(enabled_deltas(&backend).last(), Some(&(plain, true)));
handle.update_item(
plain,
MenuItemDelta {
enabled: Some(false),
..Default::default()
},
);
handle.refresh_availability(&["app.print"]);
assert_eq!(enabled_deltas(&backend).last(), Some(&(plain, false)));
handle.clear_window(win(1));
assert_eq!(handle.gated_enabled(plain), None, "forgotten with its menu");
}
fn standard(role: StandardMenuRole, title: &str) -> NativeMenuNode {
NativeMenuNode::Standard {
role,
labels: StandardLabels {
title: title.into(),
..Default::default()
},
quit_item: None,
settings_item: None,
}
}
fn bar_titles(plan: &MacBarPlan<'_>) -> Vec<String> {
plan.menus
.iter()
.map(|node| match node {
NativeMenuNode::Submenu { title, .. } => title.clone(),
NativeMenuNode::Standard { labels, .. } => labels.title.clone(),
other => panic!("the bar holds menus only, got {other:?}"),
})
.collect()
}
#[test]
fn the_app_menu_is_first_in_the_mac_bar_wherever_it_is_declared() {
let mut file = sample_snapshot(MenuItemId::next()).roots;
let snapshot = NativeMenuSnapshot {
roots: vec![
file.remove(0),
NativeMenuNode::Separator,
standard(StandardMenuRole::Window, "Window"),
standard(StandardMenuRole::App, "Probe"),
standard(StandardMenuRole::Help, "Help"),
standard(StandardMenuRole::App, "Second"),
],
};
assert_eq!(
bar_titles(&mac_bar_plan(&snapshot)),
["Probe", "File", "Window", "Help"],
"App first, the rest in declared order, one App menu, no top-level separator"
);
let declared_first = NativeMenuSnapshot {
roots: vec![
standard(StandardMenuRole::App, "Probe"),
sample_snapshot(MenuItemId::next()).roots.remove(0),
],
};
assert_eq!(
bar_titles(&mac_bar_plan(&declared_first)),
["Probe", "File"]
);
}
#[test]
fn the_mac_window_menu_relists_only_shown_or_miniaturized_windows() {
assert!(mac_relists_window(false, true, false), "on screen");
assert!(mac_relists_window(false, false, true), "in the Dock");
assert!(!mac_relists_window(false, false, false), "hidden");
assert!(
!mac_relists_window(true, true, false),
"excluded (a dialog)"
);
}
#[test]
fn the_mac_app_menu_has_appkits_rows_in_appkits_order() {
use MacAppMenuRow::*;
assert_eq!(
mac_app_menu_rows(true),
[
About, Separator, Settings, Separator, Services, Separator, Hide, HideOthers,
ShowAll, Separator, Quit,
]
);
assert_eq!(
mac_app_menu_rows(false),
[
About, Separator, Services, Separator, Hide, HideOthers, ShowAll, Separator, Quit,
],
"no Settings row, and no empty group for it"
);
}
#[test]
fn the_mac_bar_keeps_the_window_list_in_its_window_menu_or_a_hidden_one() {
let snapshot = NativeMenuSnapshot {
roots: vec![
sample_snapshot(MenuItemId::next()).roots.remove(0),
standard(StandardMenuRole::Window, "Window"),
standard(StandardMenuRole::App, "Probe"),
standard(StandardMenuRole::Window, "Second window menu"),
standard(StandardMenuRole::Help, "Help"),
],
};
let plan = mac_bar_plan(&snapshot);
assert_eq!(
plan.windows_menu,
Some(2),
"the first Window menu, in bar order"
);
assert_eq!(plan.help_menu, Some(4));
let no_window_menu = NativeMenuSnapshot {
roots: vec![
sample_snapshot(MenuItemId::next()).roots.remove(0),
standard(StandardMenuRole::App, "Probe"),
],
};
let plan = mac_bar_plan(&no_window_menu);
assert_eq!(plan.windows_menu, None, "a hidden one is added");
assert_eq!(plan.help_menu, None);
}
#[test]
fn a_key_window_without_a_menu_shows_its_owners_or_the_applications() {
let menus = [win(1)];
let has_menu = |w: TeksiloWindowId| menus.contains(&w);
let owner = |w: TeksiloWindowId| match w.raw() {
2 => Some(win(1)),
3 => Some(win(2)),
_ => None,
};
let bar = |key, declared, shown| key_window_bar(key, has_menu, owner, declared, shown);
assert_eq!(bar(win(1), true, None), Some(win(1)), "its own");
assert_eq!(bar(win(2), true, None), Some(win(1)), "its owner's");
assert_eq!(bar(win(3), true, None), Some(win(1)), "up the chain");
assert_eq!(bar(win(4), true, Some(win(1))), None, "the application's");
assert_eq!(
bar(win(4), false, Some(win(1))),
Some(win(1)),
"none declared: the bar shown before stays"
);
assert_eq!(bar(win(4), false, None), None);
let looping = |w: TeksiloWindowId| Some(if w == win(5) { win(6) } else { win(5) });
assert_eq!(key_window_bar(win(5), has_menu, looping, true, None), None);
}
#[test]
fn the_memory_backend_shows_a_dialogs_owners_menu() {
let backend = MemoryNativeMenuBackend::new();
let handle = NativeMenuHandle::new(backend.clone());
handle.set_app_menu(
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
false,
);
handle.set_window_menu(
win(1),
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
);
handle.set_window_owner(win(2), Some(win(1)));
handle.activate_window(win(1));
handle.activate_window(win(2));
assert_eq!(
backend.active_window(),
Some(win(1)),
"the dialog's owner's"
);
handle.activate_window(win(3));
assert_eq!(
backend.active_window(),
Some(win(1)),
"no application menu declared: a secondary window keeps the bar"
);
handle.set_app_menu(
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
true,
);
handle.activate_window(win(3));
assert!(backend.shows_app_menu(), "declared: the application's");
handle.activate_window(win(2));
assert_eq!(backend.active_window(), Some(win(1)));
handle.clear_window(win(1));
assert!(backend.shows_app_menu(), "the owner's menu went with it");
}
#[test]
fn declaring_the_application_menu_shows_it_for_a_menu_less_key_window() {
let backend = MemoryNativeMenuBackend::new();
let handle = NativeMenuHandle::new(backend.clone());
let app_menu = |declared| {
handle.set_app_menu(
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
declared,
)
};
app_menu(false);
handle.set_window_menu(
win(1),
sample_snapshot(MenuItemId::next()),
HashMap::new(),
poster(),
);
handle.activate_window(win(1));
handle.activate_window(win(2));
assert_eq!(backend.active_window(), Some(win(1)), "precondition");
app_menu(true);
assert!(backend.shows_app_menu(), "window 2 is key and has none");
handle.activate_window(win(1));
app_menu(true);
assert_eq!(
backend.active_window(),
Some(win(1)),
"a key window's own menu stays"
);
}
#[test]
fn a_cleared_windows_bar_never_comes_back() {
let (closed, dialog, other, plain) = (win(1), win(2), win(3), win(4));
let has_menu = |w: TeksiloWindowId| w == closed || w == other;
let owner = |w: TeksiloWindowId| (w == dialog).then_some(closed);
for declared in [true, false] {
for key in [Some(closed), Some(dialog), Some(other), Some(plain), None] {
let bar = bar_after_clear(closed, key, Some(closed), has_menu, owner, declared)
.expect("its bar was up: replaced");
assert_ne!(bar, Some(closed), "key {key:?}, declared {declared}");
if key == Some(other) {
assert_eq!(bar, Some(other), "the key window's own menu");
}
}
assert_eq!(
bar_after_clear(closed, Some(plain), Some(other), has_menu, owner, declared),
None,
"another window's bar stays"
);
}
}
#[test]
fn what_setting_the_application_menu_shows() {
let app = None;
assert_eq!(bar_after_app_menu(Some(app), Some(win(1))), Some(app));
assert_eq!(bar_after_app_menu(Some(app), app), Some(app), "rebuilt");
assert_eq!(bar_after_app_menu(Some(Some(win(1))), Some(win(1))), None);
assert_eq!(
bar_after_app_menu(Some(Some(win(1))), app),
Some(Some(win(1)))
);
assert_eq!(bar_after_app_menu(None, app), Some(app));
assert_eq!(bar_after_app_menu(None, Some(win(1))), None);
}
#[test]
fn an_unchanged_refresh_evaluates_nothing() {
let backend = MemoryNativeMenuBackend::new();
let handle = NativeMenuHandle::new(backend.clone());
let row = MenuItemId::next();
let intent_only = || NativeMenuActivation {
intent: Some("app.print"),
action: None,
quits: false,
};
handle.set_window_menu(
win(1),
sample_snapshot(row),
[(row, intent_only())].into_iter().collect(),
poster(),
);
handle.refresh_availability(&["app.print"]);
assert_eq!(handle.evaluations(), 1);
handle.refresh_availability(&["app.print"]);
assert_eq!(handle.evaluations(), 1, "nothing changed: no row evaluated");
let other = MenuItemId::next();
handle.set_window_menu(
win(2),
sample_snapshot(other),
[(other, intent_only())].into_iter().collect(),
poster(),
);
handle.refresh_availability(&["app.print"]);
assert_eq!(handle.evaluations(), 3, "a menu installed since: both rows");
handle.refresh_availability(&[]);
assert_eq!(handle.evaluations(), 5, "another answer");
assert_eq!(handle.gated_enabled(other), Some(false));
}
#[test]
fn only_a_backend_with_a_menu_bar_says_it_shows_one() {
assert!(!NativeMenuHandle::new(NoopNativeMenuBackend::new()).shows_bar());
assert!(NativeMenuHandle::new(MemoryNativeMenuBackend::new()).shows_bar());
}
#[test]
fn update_item_records_delta() {
let backend = MemoryNativeMenuBackend::new();
let handle = NativeMenuHandle::new(backend.clone());
let id = MenuItemId::next();
handle.update_item(
id,
MenuItemDelta {
enabled: Some(false),
check: Some(NativeCheck::On),
..Default::default()
},
);
let deltas = backend.deltas();
assert_eq!(deltas.len(), 1);
assert_eq!(deltas[0].0, id);
assert_eq!(deltas[0].1.enabled, Some(false));
assert_eq!(deltas[0].1.check, Some(NativeCheck::On));
}
#[test]
fn noop_backend_is_inert() {
let handle = NativeMenuHandle::new(NoopNativeMenuBackend::new());
let id = MenuItemId::next();
handle.set_window_menu(win(1), sample_snapshot(id), HashMap::new(), poster());
handle.activate_window(win(1));
handle.update_item(id, MenuItemDelta::default());
handle.clear_window(win(1));
assert!(handle.activation(win(1), id).is_none());
}
}