ferroui_controls/
native_menu_item.rs1use 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#[repr(C)]
19pub struct NativeMenuItem {
20 base: NativeMenuItemBase,
21 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 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 this.can_execute_changed();
66 }
67 }
68}
69
70struct 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 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 pub fn icon_property() -> StyledProperty<Option<Rc<dyn IBitmap>>> {
91 FerroProperty::register::<NativeMenuItem, _>("Icon", None)
92 }
93
94 pub fn header_property() -> StyledProperty<Option<String>> {
96 FerroProperty::register::<NativeMenuItem, _>("Header", None)
97 }
98
99 pub fn tool_tip_property() -> StyledProperty<Option<String>> {
101 FerroProperty::register::<NativeMenuItem, _>("ToolTip", None)
102 }
103
104 pub fn gesture_property() -> StyledProperty<Option<KeyGesture>> {
106 FerroProperty::register::<NativeMenuItem, _>("Gesture", None)
107 }
108
109 pub fn is_checked_property() -> StyledProperty<bool> {
111 MenuItem::is_checked_property().add_owner::<NativeMenuItem>()
112 }
113
114 pub fn toggle_type_property() -> StyledProperty<MenuItemToggleType> {
116 FerroProperty::register::<NativeMenuItem, _>("ToggleType", MenuItemToggleType::None)
117 }
118
119 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 pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
127 MenuItem::command_parameter_property().add_owner::<NativeMenuItem>()
128 }
129
130 pub fn is_enabled_property() -> StyledProperty<bool> {
132 FerroProperty::register::<NativeMenuItem, _>("IsEnabled", true)
133 }
134
135 pub fn is_visible_property() -> StyledProperty<bool> {
137 Visual::is_visible_property().add_owner::<NativeMenuItem>()
138 }
139 }
140}
141
142impl NativeMenuItem {
143 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 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 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 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 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 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 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 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 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 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 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 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 pub fn has_click_handlers(&self) -> bool {
267 !self.click.is_empty()
268 }
269
270 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 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 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 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 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 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}