Skip to main content

ferroui_controls/
native_menu_item.rs

1use crate::i_native_menu_item_exporter_events_impl_bridge::INativeMenuItemExporterEventsImplBridge;
2use crate::utils::debug_display::{append_optional_value, debug_type_name};
3use crate::{MenuItem, MenuItemToggleType, NativeMenu, NativeMenuItemBase};
4use ferroui_base::input::{ICommand, KeyGesture};
5use ferroui_base::media::imaging::IBitmap;
6use ferroui_base::utilities::WeakEvents;
7use ferroui_base::reactive::{Disposable, IDisposable};
8use ferroui_base::utilities::HandlerList;
9use ferroui_base::{
10    ferro_class, ferro_class_info, ferro_properties, instantiate, BoxedValue, FerroObject,
11    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledProperty, StyledPropertyMetadata,
12    StyledPropertyOptions, Visual,
13};
14use std::cell::RefCell;
15use std::rc::Rc;
16
17/// An item of a [`NativeMenu`].
18#[repr(C)]
19pub struct NativeMenuItem {
20    base: NativeMenuItemBase,
21    /// The subscription to the "can execute changed" event of the command.
22    /// The handler holds the item weakly, so that a command does not keep
23    /// the items that use it alive.
24    can_execute_changed_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
25    click: HandlerList<dyn Fn(&NativeMenuItem)>,
26}
27
28ferro_class!(NativeMenuItem: NativeMenuItemBase);
29ferro_class_info!(NativeMenuItem { new: NativeMenuItem::new });
30
31impl FerroObjectImpl for NativeMenuItem {
32    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
33        Self::parent_on_property_changed(this, change);
34
35        let property = change.property();
36
37        if property == Self::menu_property().as_property() {
38            if let Some(new_menu) = change.get_new_value::<Option<Ref<NativeMenu>>>() {
39                if new_menu.parent().is_some_and(|parent| parent != this.to_ref()) {
40                    panic!("NativeMenu already has a parent");
41                }
42                new_menu.set_parent(Some(this.to_ref()));
43            }
44        } else if property == Self::command_property().as_property() {
45            let (old_command, new_command) = change.get_old_and_new_value::<Option<Rc<dyn ICommand>>>();
46            if old_command.is_some() {
47                let subscription = this.can_execute_changed_subscription.take();
48                if let Some(subscription) = subscription {
49                    subscription.dispose();
50                }
51            }
52            if let Some(new_command) = new_command {
53                // A weak subscription, as in the reference: it ends with
54                // this item.
55                let subscription =
56                    WeakEvents::subscribe_command_can_execute_changed(&new_command, &this.to_ref(), |this| this.can_execute_changed());
57                let old = this.can_execute_changed_subscription.replace(Some(subscription));
58                if let Some(old) = old {
59                    old.dispose();
60                }
61            }
62            this.can_execute_changed();
63        } else if property == Self::command_parameter_property().as_property() && this.command().is_some() {
64            // The parameter can be assigned after the command, which makes the first query stale.
65            this.can_execute_changed();
66        }
67    }
68}
69
70/// The native menu item viewed through the interface it implements.
71struct NativeMenuItemHandle(Ref<NativeMenuItem>);
72
73impl INativeMenuItemExporterEventsImplBridge for NativeMenuItemHandle {
74    fn raise_clicked(&self) {
75        self.0.raise_clicked();
76    }
77}
78
79ferro_properties! {
80    impl NativeMenuItem {
81        /// Defines the `Menu` property.
82        pub fn menu_property() -> StyledProperty<Option<Ref<NativeMenu>>> {
83            FerroProperty::register_with::<NativeMenuItem, _>(
84                "Menu",
85                StyledPropertyOptions::new(None).coerce(Self::coerce_menu),
86            )
87        }
88
89        /// Defines the `Icon` property.
90        pub fn icon_property() -> StyledProperty<Option<Rc<dyn IBitmap>>> {
91            FerroProperty::register::<NativeMenuItem, _>("Icon", None)
92        }
93
94        /// Defines the `Header` property.
95        pub fn header_property() -> StyledProperty<Option<String>> {
96            FerroProperty::register::<NativeMenuItem, _>("Header", None)
97        }
98
99        /// Defines the `ToolTip` property.
100        pub fn tool_tip_property() -> StyledProperty<Option<String>> {
101            FerroProperty::register::<NativeMenuItem, _>("ToolTip", None)
102        }
103
104        /// Defines the `Gesture` property.
105        pub fn gesture_property() -> StyledProperty<Option<KeyGesture>> {
106            FerroProperty::register::<NativeMenuItem, _>("Gesture", None)
107        }
108
109        /// Defines the `IsChecked` property.
110        pub fn is_checked_property() -> StyledProperty<bool> {
111            MenuItem::is_checked_property().add_owner::<NativeMenuItem>()
112        }
113
114        /// Defines the `ToggleType` property.
115        pub fn toggle_type_property() -> StyledProperty<MenuItemToggleType> {
116            FerroProperty::register::<NativeMenuItem, _>("ToggleType", MenuItemToggleType::None)
117        }
118
119        /// Defines the `Command` property.
120        pub fn command_property() -> StyledProperty<Option<Rc<dyn ICommand>>> {
121            MenuItem::command_property()
122                .add_owner_with::<NativeMenuItem>(StyledPropertyMetadata::new(None).with_enable_data_validation(true))
123        }
124
125        /// Defines the `CommandParameter` property.
126        pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
127            MenuItem::command_parameter_property().add_owner::<NativeMenuItem>()
128        }
129
130        /// Defines the `IsEnabled` property.
131        pub fn is_enabled_property() -> StyledProperty<bool> {
132            FerroProperty::register::<NativeMenuItem, _>("IsEnabled", true)
133        }
134
135        /// Defines the `IsVisible` property.
136        pub fn is_visible_property() -> StyledProperty<bool> {
137            Visual::is_visible_property().add_owner::<NativeMenuItem>()
138        }
139    }
140}
141
142impl NativeMenuItem {
143    /// Creates the class data; see [`FerroObject::construct`].
144    pub fn construct() -> Self {
145        Self {
146            base: NativeMenuItemBase::construct(),
147            can_execute_changed_subscription: RefCell::new(None),
148            click: HandlerList::new(),
149        }
150    }
151
152    pub fn new() -> Ref<Self> {
153        instantiate(Self::construct())
154    }
155
156    /// Creates an item with the given header.
157    pub fn with_header(header: &str) -> Ref<Self> {
158        let this = Self::new();
159        this.set_header(Some(header.to_string()));
160        this
161    }
162
163    /// The submenu of the item.
164    pub fn menu(&self) -> Option<Ref<NativeMenu>> {
165        self.get_value(Self::menu_property())
166    }
167
168    pub fn set_menu(&self, value: Option<Ref<NativeMenu>>) {
169        self.set_value(Self::menu_property(), value)
170    }
171
172    fn coerce_menu(sender: &FerroObject, value: Option<Ref<NativeMenu>>) -> Option<Ref<NativeMenu>> {
173        if let Some(parent) = value.as_ref().and_then(|value| value.parent()) {
174            let parent: &FerroObject = &parent;
175            if !std::ptr::eq(parent, sender) {
176                panic!("NativeMenu already has a parent");
177            }
178        }
179        value
180    }
181
182    /// The icon that appears in the item.
183    pub fn icon(&self) -> Option<Rc<dyn IBitmap>> {
184        self.get_value(Self::icon_property())
185    }
186
187    pub fn set_icon(&self, value: Option<Rc<dyn IBitmap>>) {
188        self.set_value(Self::icon_property(), value)
189    }
190
191    /// The header of the item.
192    pub fn header(&self) -> Option<String> {
193        self.get_value(Self::header_property())
194    }
195
196    pub fn set_header(&self, value: Option<String>) {
197        self.set_value(Self::header_property(), value)
198    }
199
200    /// Gets the tooltip associated with the menu item. This may not be
201    /// supported by the native menu provider, but will be passed on to the
202    /// non-native fallback menu item if used.
203    pub fn tool_tip(&self) -> Option<String> {
204        self.get_value(Self::tool_tip_property())
205    }
206
207    pub fn set_tool_tip(&self, value: Option<String>) {
208        self.set_value(Self::tool_tip_property(), value)
209    }
210
211    /// The input gesture that will be displayed in the item.
212    pub fn gesture(&self) -> Option<KeyGesture> {
213        self.get_value(Self::gesture_property())
214    }
215
216    pub fn set_gesture(&self, value: Option<KeyGesture>) {
217        self.set_value(Self::gesture_property(), value)
218    }
219
220    /// Whether the item is checked.
221    pub fn is_checked(&self) -> bool {
222        self.get_value(Self::is_checked_property())
223    }
224
225    pub fn set_is_checked(&self, value: bool) {
226        self.set_value(Self::is_checked_property(), value)
227    }
228
229    /// How the item reacts to clicks.
230    pub fn toggle_type(&self) -> MenuItemToggleType {
231        self.get_value(Self::toggle_type_property())
232    }
233
234    pub fn set_toggle_type(&self, value: MenuItemToggleType) {
235        self.set_value(Self::toggle_type_property(), value)
236    }
237
238    /// Whether the item is enabled.
239    pub fn is_enabled(&self) -> bool {
240        self.get_value(Self::is_enabled_property())
241    }
242
243    pub fn set_is_enabled(&self, value: bool) {
244        self.set_value(Self::is_enabled_property(), value)
245    }
246
247    /// Gets a value indicating whether this menu item is visible.
248    pub fn is_visible(&self) -> bool {
249        self.get_value(Self::is_visible_property())
250    }
251
252    pub fn set_is_visible(&self, value: bool) {
253        self.set_value(Self::is_visible_property(), value)
254    }
255
256    // Objects of the framework belong to the thread that created them, so
257    // the notification of the command always arrives on that thread; the
258    // reference re-dispatches one that arrives on another thread.
259    fn can_execute_changed(&self) {
260        let (command, parameter) = (self.command(), self.command_parameter());
261        let can_execute = command.is_none_or(|command| command.can_execute(parameter.as_ref()));
262        self.set_current_value(Self::is_enabled_property(), can_execute);
263    }
264
265    /// Whether the `click` event has handlers.
266    pub fn has_click_handlers(&self) -> bool {
267        !self.click.is_empty()
268    }
269
270    /// The command to invoke when the item is clicked.
271    pub fn command(&self) -> Option<Rc<dyn ICommand>> {
272        self.get_value(Self::command_property())
273    }
274
275    pub fn set_command(&self, value: Option<Rc<dyn ICommand>>) {
276        self.set_value(Self::command_property(), value)
277    }
278
279    /// The parameter to pass to the command.
280    pub fn command_parameter(&self) -> Option<BoxedValue> {
281        self.get_value(Self::command_parameter_property())
282    }
283
284    pub fn set_command_parameter(&self, value: Option<BoxedValue>) {
285        self.set_value(Self::command_parameter_property(), value)
286    }
287
288    /// Occurs when a [`NativeMenuItem`] is clicked.
289    pub fn click(&self, handler: impl Fn(&NativeMenuItem) + 'static) -> Rc<dyn IDisposable> {
290        let token = self.click.add(Rc::new(handler));
291        let weak = self.to_ref().downgrade();
292        Disposable::create(move || {
293            if let Some(this) = weak.upgrade() {
294                this.click.remove(token);
295            }
296        })
297    }
298
299    /// The item viewed as the contract through which the platform
300    /// exporter of the menu raises the events of the item.
301    pub fn to_exporter_events_bridge(&self) -> Rc<dyn INativeMenuItemExporterEventsImplBridge> {
302        Rc::new(NativeMenuItemHandle(self.to_ref()))
303    }
304
305    fn raise_clicked(&self) {
306        for (_, handler) in self.click.snapshot().iter() {
307            handler(self);
308        }
309
310        // The command and its parameter are read again for the execution,
311        // as in the reference: "can execute" may have changed them.
312        if self.command().is_some_and(|command| command.can_execute(self.command_parameter().as_ref())) {
313            if let Some(command) = self.command() {
314                command.execute(self.command_parameter().as_ref());
315            }
316        }
317    }
318
319    /// The text that describes the item in diagnostics.
320    pub(crate) fn build_debug_display(&self, builder: &mut String, include_content: bool) {
321        builder.push_str(&debug_type_name(self));
322
323        if include_content {
324            append_optional_value(builder, "Header", self.header().as_deref());
325        }
326    }
327}