Skip to main content

ferroui_controls/
menu_base.rs

1use crate::generators::RecycleKey;
2use crate::i_menu::{register_menu, IMenu};
3use crate::i_menu_element::IMenuElement;
4use crate::i_menu_item::{as_menu_item, IMenuItem};
5use crate::platform::{DefaultMenuInteractionHandler, IMenuInteractionHandler};
6use crate::primitives::{SelectingItemsControl, SelectingItemsControlImpl, TemplatedControlImpl};
7use crate::{Control, ControlImpl, ItemsControl, ItemsControlImpl, Menu, MenuItem, Separator, TopLevel};
8use ferroui_base::input::{IFocusScope, IMainMenu, InputElement, InputElementImpl, KeyEventArgs, NavigationDirection};
9use ferroui_base::interactivity::{
10    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
11};
12use ferroui_base::layout::LayoutableImpl;
13use ferroui_base::{
14    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, ferro_routed_event, BoxedValue,
15    DirectProperty, FerroObjectImpl, FerroProperty, Ref, StyledElement, StyledElementImpl, VisualImpl, VisualImplExt,
16    VisualTreeAttachmentEventArgs,
17};
18use std::cell::Cell;
19use std::rc::Rc;
20
21/// Base class for menu controls.
22#[repr(C)]
23pub struct MenuBase {
24    base: SelectingItemsControl,
25    is_open: Cell<bool>,
26    interaction_handler: Rc<dyn IMenuInteractionHandler>,
27}
28
29ferro_class! {
30    MenuBase: SelectingItemsControl, virtuals MenuBaseImpl: SelectingItemsControlImpl {
31        /// Closes the menu.
32        fn close(this);
33        /// Opens the menu.
34        fn open(this);
35        /// Called when a submenu opens somewhere in the menu.
36        fn on_submenu_opened(this, e: &RoutedEventArgs);
37    }
38}
39
40ferro_class_info!(MenuBase {});
41
42ferro_impl_classes!(
43    MenuBase: FerroObjectImpl,
44    StyledElementImpl,
45    LayoutableImpl,
46    InteractiveImpl,
47    ControlImpl,
48    TemplatedControlImpl,
49    SelectingItemsControlImpl
50);
51
52impl IFocusScope for MenuBase {}
53
54impl VisualImpl for MenuBase {
55    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
56        Self::parent_on_attached_to_visual_tree(this, e);
57        this.interaction_handler.attach(&this.to_ref());
58    }
59
60    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
61        Self::parent_on_detached_from_visual_tree(this, e);
62        this.interaction_handler.detach(&this.to_ref());
63    }
64}
65
66impl InputElementImpl for MenuBase {
67    fn is_focus_scope(_this: &Self) -> bool {
68        true
69    }
70
71    fn on_key_down(_this: &Self, _e: &KeyEventArgs) {
72        // Don't handle here: let the interaction handler handle it.
73    }
74}
75
76impl ItemsControlImpl for MenuBase {
77    fn create_container_for_item_override(
78        _this: &Self,
79        _item: &Option<BoxedValue>,
80        _index: i32,
81        _recycle_key: Option<RecycleKey>,
82    ) -> Ref<Control> {
83        MenuItem::new().upcast()
84    }
85
86    fn needs_container_override(_this: &Self, item: &Option<BoxedValue>, _index: i32) -> (bool, Option<RecycleKey>) {
87        Self::needs_menu_container(item)
88    }
89}
90
91impl MenuBaseImpl for MenuBase {
92    fn close(_this: &Self) {
93        panic!("MenuBase is abstract.")
94    }
95
96    fn open(_this: &Self) {
97        panic!("MenuBase is abstract.")
98    }
99
100    fn on_submenu_opened(this: &Self, e: &RoutedEventArgs) {
101        let menu_item = e.source().and_then(|source| source.cast::<MenuItem>());
102
103        if let Some(menu_item) = menu_item {
104            let this_element: &StyledElement = this;
105            if menu_item.parent().is_some_and(|parent| std::ptr::eq::<StyledElement>(&*parent, this_element)) {
106                let children: Vec<Ref<MenuItem>> = StyledElement::logical_children(this)
107                    .to_vec()
108                    .into_iter()
109                    .filter_map(|child| child.cast::<MenuItem>())
110                    .collect();
111
112                for child in children {
113                    if child != menu_item && child.is_sub_menu_open() {
114                        child.set_is_sub_menu_open(false);
115                    }
116                }
117            }
118        }
119
120        this.set_is_open(true);
121    }
122}
123
124/// The menu viewed as a menu element and as a menu.
125struct MenuBaseHandle(Ref<MenuBase>);
126
127impl IMenuElement for MenuBaseHandle {
128    fn element(&self) -> Ref<InputElement> {
129        self.0.clone().upcast()
130    }
131
132    fn selected_item(&self) -> Option<Rc<dyn IMenuItem>> {
133        let index = self.0.selected_index();
134        if index != -1 {
135            self.0.container_from_index(index).and_then(|container| as_menu_item(&container))
136        } else {
137            None
138        }
139    }
140
141    fn set_selected_item(&self, value: Option<Rc<dyn IMenuItem>>) {
142        let index = match value.and_then(|value| value.element().cast::<Control>()) {
143            Some(c) => self.0.index_from_container(&c),
144            None => -1,
145        };
146        self.0.set_selected_index(index);
147    }
148
149    fn sub_items(&self) -> Vec<Rc<dyn IMenuItem>> {
150        StyledElement::logical_children(&self.0).to_vec().iter().filter_map(|child| as_menu_item(child)).collect()
151    }
152
153    fn open(&self) {
154        self.0.open();
155    }
156
157    fn close(&self) {
158        self.0.close();
159    }
160
161    fn move_selection(&self, direction: NavigationDirection, wrap: bool) -> bool {
162        self.0.move_selection(direction, wrap, false)
163    }
164
165    fn as_menu(&self) -> Option<Rc<dyn IMenu>> {
166        Some(Rc::new(MenuBaseHandle(self.0.clone())))
167    }
168
169    fn as_main_menu(&self) -> Option<Rc<dyn IMainMenu>> {
170        self.0.clone().cast::<Menu>().map(|menu| menu.to_main_menu())
171    }
172}
173
174impl IMenu for MenuBaseHandle {
175    fn interaction_handler(&self) -> Rc<dyn IMenuInteractionHandler> {
176        self.0.interaction_handler()
177    }
178
179    fn is_open(&self) -> bool {
180        self.0.is_open()
181    }
182
183    fn top_level(&self) -> Option<Ref<TopLevel>> {
184        TopLevel::get_top_level(Some(&self.0))
185    }
186}
187
188ferro_properties! {
189    impl MenuBase {
190        /// Defines the `IsOpen` property.
191        pub fn is_open_property() -> DirectProperty<MenuBase, bool> {
192            FerroProperty::register_direct::<MenuBase, _>("IsOpen", |o| o.is_open(), None, false)
193        }
194    }
195}
196
197impl MenuBase {
198    ferro_routed_event!(
199        /// Defines the `Opened` event.
200        pub fn opened_event() -> RoutedEvent<RoutedEventArgs> {
201            RoutedEvent::register::<MenuBase, _>("Opened", RoutingStrategies::BUBBLE)
202        }
203    );
204
205    ferro_routed_event!(
206        /// Defines the `Closed` event.
207        pub fn closed_event() -> RoutedEvent<RoutedEventArgs> {
208            RoutedEvent::register::<MenuBase, _>("Closed", RoutingStrategies::BUBBLE)
209        }
210    );
211
212    /// Initializes static members of the [`MenuBase`] class.
213    fn static_constructor() {
214        register_menu::<MenuBase>(|menu| Rc::new(MenuBaseHandle(menu)));
215        MenuItem::submenu_opened_event().add_class_handler::<MenuBase>(|x, e| x.on_submenu_opened(e));
216    }
217
218    /// Creates the class data of a menu that uses the default menu
219    /// interaction handler; see [`ferroui_base::FerroObject::construct`].
220    pub fn construct() -> Self {
221        Self::construct_with(DefaultMenuInteractionHandler::new(false))
222    }
223
224    /// Creates the class data of a menu that uses `interaction_handler`.
225    pub fn construct_with(interaction_handler: Rc<dyn IMenuInteractionHandler>) -> Self {
226        Self { base: SelectingItemsControl::construct(), is_open: Cell::new(false), interaction_handler }
227    }
228
229    /// Gets a value indicating whether the menu is open.
230    pub fn is_open(&self) -> bool {
231        self.is_open.get()
232    }
233
234    /// Sets a value indicating whether the menu is open. For deriving
235    /// classes.
236    pub fn set_is_open(&self, value: bool) {
237        self.set_and_raise_cell(Self::is_open_property(), &self.is_open, value);
238    }
239
240    /// Gets the interaction handler for the menu.
241    pub fn interaction_handler(&self) -> Rc<dyn IMenuInteractionHandler> {
242        self.interaction_handler.clone()
243    }
244
245    /// Occurs when a menu is opened.
246    pub fn opened(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
247        self.add_handler(Self::opened_event(), handler)
248    }
249
250    /// Occurs when a menu is closed.
251    pub fn closed(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
252        self.add_handler(Self::closed_event(), handler)
253    }
254
255    /// The menu viewed as [`IMenu`].
256    pub(crate) fn to_menu(&self) -> Rc<dyn IMenu> {
257        Rc::new(MenuBaseHandle(self.to_ref()))
258    }
259
260    /// The container decision shared by menus and menu items: menu items
261    /// and separators are their own containers.
262    pub(crate) fn needs_menu_container(item: &Option<BoxedValue>) -> (bool, Option<RecycleKey>) {
263        let is_container = item
264            .as_ref()
265            .and_then(Control::from_boxed)
266            .is_some_and(|control| control.is::<MenuItem>() || control.is::<Separator>());
267
268        if is_container {
269            (false, None)
270        } else {
271            (true, Some(ItemsControl::DEFAULT_RECYCLE_KEY))
272        }
273    }
274}