Skip to main content

ferroui_controls/
native_menu.rs

1use crate::i_native_menu_exporter_events_impl_bridge::INativeMenuExporterEventsImplBridge;
2use crate::utils::debug_display::debug_type_name;
3use crate::{NativeMenuItem, NativeMenuItemBase};
4use ferroui_base::collections::{FerroList, NotifyCollectionChangedEventArgs, ResetBehavior};
5use ferroui_base::reactive::{Disposable, IDisposable};
6use ferroui_base::utilities::HandlerList;
7use ferroui_base::{
8    ferro_class, ferro_class_info, ferro_properties, instantiate, DirectProperty, FerroObject,
9    FerroObjectImpl, FerroObjectImplExt, FerroProperty, IntoRef, Ref, WeakRef,
10};
11use std::cell::RefCell;
12use std::rc::Rc;
13
14/// A menu that is shown by the platform: the menu of a window or of the
15/// application, the menu of a tray icon or of the dock.
16#[repr(C)]
17pub struct NativeMenu {
18    base: FerroObject,
19    items: FerroList<Ref<NativeMenuItemBase>>,
20    /// The item the menu is the submenu of. A back pointer: the item owns
21    /// its menu.
22    parent: RefCell<Option<WeakRef<NativeMenuItem>>>,
23    needs_update: HandlerList<dyn Fn(&NativeMenu)>,
24    opening: HandlerList<dyn Fn(&NativeMenu)>,
25    closed: HandlerList<dyn Fn(&NativeMenu)>,
26}
27
28ferro_class!(NativeMenu: FerroObject);
29ferro_class_info!(NativeMenu { new: NativeMenu::new });
30
31impl FerroObjectImpl for NativeMenu {
32    fn constructed(this: &Self) {
33        Self::parent_constructed(this);
34
35        let weak = this.to_ref().downgrade();
36        this.items.set_validate(Some(Rc::new({
37            let weak = weak.clone();
38            move |item: &Ref<NativeMenuItemBase>| Self::validate(&weak, item)
39        })));
40        this.items.add_collection_changed(Rc::new(
41            move |e: &NotifyCollectionChangedEventArgs<'_, Ref<NativeMenuItemBase>>| Self::items_changed(&weak, e),
42        ));
43    }
44}
45
46/// The native menu viewed through the interface it implements.
47struct NativeMenuHandle(Ref<NativeMenu>);
48
49impl INativeMenuExporterEventsImplBridge for NativeMenuHandle {
50    fn raise_needs_update(&self) {
51        for (_, handler) in self.0.needs_update.snapshot().iter() {
52            handler(&self.0);
53        }
54    }
55
56    fn raise_opening(&self) {
57        for (_, handler) in self.0.opening.snapshot().iter() {
58            handler(&self.0);
59        }
60    }
61
62    fn raise_closed(&self) {
63        for (_, handler) in self.0.closed.snapshot().iter() {
64            handler(&self.0);
65        }
66    }
67}
68
69ferro_properties! {
70    impl NativeMenu, also [NativeMenu::register_export_properties] {
71        /// Defines the `Parent` property.
72        pub fn parent_property() -> DirectProperty<NativeMenu, Option<Ref<NativeMenuItem>>> {
73            FerroProperty::register_direct::<NativeMenu, _>("Parent", |o| o.parent(), None, None)
74        }
75    }
76}
77
78impl NativeMenu {
79    fn static_constructor() {
80        Self::static_constructor_export();
81    }
82
83    /// Creates the class data; see [`FerroObject::construct`].
84    pub fn construct() -> Self {
85        let items = FerroList::new();
86        items.set_reset_behavior(ResetBehavior::Remove);
87        Self {
88            base: FerroObject::construct(),
89            items,
90            parent: RefCell::new(None),
91            needs_update: HandlerList::new(),
92            opening: HandlerList::new(),
93            closed: HandlerList::new(),
94        }
95    }
96
97    pub fn new() -> Ref<Self> {
98        instantiate(Self::construct())
99    }
100
101    /// The items of the menu.
102    pub fn items(&self) -> FerroList<Ref<NativeMenuItemBase>> {
103        self.items.clone()
104    }
105
106    /// Raised when the menu requests an update.
107    ///
108    /// Use this event to add, remove or modify menu items before a menu is
109    /// shown or a hotkey is pressed.
110    pub fn needs_update(&self, handler: impl Fn(&NativeMenu) + 'static) -> Rc<dyn IDisposable> {
111        self.subscribe(|menu| &menu.needs_update, handler)
112    }
113
114    /// Raised before the menu is opened.
115    ///
116    /// Do not update the menu in this event; use
117    /// [`needs_update`](Self::needs_update).
118    pub fn opening(&self, handler: impl Fn(&NativeMenu) + 'static) -> Rc<dyn IDisposable> {
119        self.subscribe(|menu| &menu.opening, handler)
120    }
121
122    /// Raised after the menu is closed.
123    ///
124    /// Do not update the menu in this event; use
125    /// [`needs_update`](Self::needs_update).
126    pub fn closed(&self, handler: impl Fn(&NativeMenu) + 'static) -> Rc<dyn IDisposable> {
127        self.subscribe(|menu| &menu.closed, handler)
128    }
129
130    fn subscribe(
131        &self,
132        event: fn(&NativeMenu) -> &HandlerList<dyn Fn(&NativeMenu)>,
133        handler: impl Fn(&NativeMenu) + 'static,
134    ) -> Rc<dyn IDisposable> {
135        let token = event(self).add(Rc::new(handler));
136        let weak = self.to_ref().downgrade();
137        Disposable::create(move || {
138            if let Some(this) = weak.upgrade() {
139                event(&this).remove(token);
140            }
141        })
142    }
143
144    fn validate(this: &WeakRef<NativeMenu>, item: &Ref<NativeMenuItemBase>) {
145        if let Some(parent) = item.parent() {
146            panic!(
147                "The menu item {} already has a parent {} while trying to add it as a child of {}.",
148                debug_display(item),
149                debug_display(&parent),
150                this.upgrade().map_or_else(|| NativeMenu::TYPE.name().to_string(), |this| debug_display(&this)),
151            );
152        }
153    }
154
155    fn items_changed(this: &WeakRef<NativeMenu>, e: &NotifyCollectionChangedEventArgs<'_, Ref<NativeMenuItemBase>>) {
156        for i in e.old_items {
157            i.set_parent(None);
158        }
159        for i in e.new_items {
160            i.set_parent(this.upgrade());
161        }
162    }
163
164    /// The item the menu is the submenu of.
165    pub fn parent(&self) -> Option<Ref<NativeMenuItem>> {
166        let parent = self.parent.borrow().clone();
167        parent.and_then(|parent| parent.upgrade())
168    }
169
170    pub(crate) fn set_parent(&self, value: Option<Ref<NativeMenuItem>>) {
171        let old = self.parent();
172        if old == value {
173            return;
174        }
175        *self.parent.borrow_mut() = value.as_ref().map(|value| value.downgrade());
176        self.raise_direct_property_changed(Self::parent_property(), &old, &value);
177    }
178
179    /// The menu viewed as the contract through which the platform exporter
180    /// of the menu raises its events.
181    pub fn to_exporter_events_bridge(&self) -> Rc<dyn INativeMenuExporterEventsImplBridge> {
182        Rc::new(NativeMenuHandle(self.to_ref()))
183    }
184
185    /// Adds an item to the menu.
186    pub fn add(&self, item: impl IntoRef<NativeMenuItemBase>) {
187        self.items.add(item.into_ref());
188    }
189
190    /// Enumerates the items of the menu.
191    pub fn iter(&self) -> std::vec::IntoIter<Ref<NativeMenuItemBase>> {
192        self.items.to_vec().into_iter()
193    }
194}
195
196impl IntoIterator for &NativeMenu {
197    type Item = Ref<NativeMenuItemBase>;
198    type IntoIter = std::vec::IntoIter<Ref<NativeMenuItemBase>>;
199
200    fn into_iter(self) -> Self::IntoIter {
201        self.iter()
202    }
203}
204
205/// The text that describes an object of the native menu classes in
206/// diagnostics: the name of its class and, for a menu item, its header.
207pub(crate) fn debug_display(object: &FerroObject) -> String {
208    let mut builder = String::new();
209    match object.downcast_ref::<NativeMenuItem>() {
210        Some(item) => item.build_debug_display(&mut builder, true),
211        None => builder.push_str(&debug_type_name(object)),
212    }
213    builder
214}