Skip to main content

fui_controls/controls/
menu.rs

1use fui_core::{ControlObject, Property, Style, ViewContext};
2use fui_drawing::Color;
3use fui_macros::ui;
4use std::cell::RefCell;
5use std::rc::{Rc, Weak};
6use typed_builder::TypedBuilder;
7use typemap::TypeMap;
8
9use crate::controls::*;
10use crate::{DataHolder, GestureArea};
11use fui_core::*;
12
13#[derive(TypedBuilder)]
14pub struct Menu {
15    #[builder(default = Orientation::Horizontal)]
16    pub orientation: Orientation,
17
18    pub items: Vec<MenuItem>,
19}
20
21impl Menu {
22    pub fn to_view(
23        self,
24        _style: Option<Box<dyn Style<Self>>>,
25        context: ViewContext,
26    ) -> Rc<RefCell<dyn ControlObject>> {
27        // menu is active when tapped
28        let is_menu_active_prop = Property::new(false);
29
30        let content_prop = ObservableVec::new();
31        let mut close_item_popup_callbacks = Vec::new();
32        let mut close_siblings_callbacks = Vec::new();
33
34        let menu: Rc<RefCell<dyn ControlObject>> = ui!(
35            Shadow {
36                Style: Default { size: 12.0f32 },
37
38                Border {
39                    border_type: BorderType::None,
40                    Style: Default { background_color: Color::rgba(1.0, 1.0, 1.0, 0.8), },
41
42                    StackPanel {
43                        orientation: self.orientation,
44
45                        &content_prop,
46                    }
47                }
48            }
49        );
50
51        menu.borrow_mut()
52            .get_context_mut()
53            .set_attached_values(context.attached_values);
54
55        let uncovered_controls: Vec<_> = vec![Rc::downgrade(&menu)];
56
57        for item in self.items.into_iter() {
58            let close_siblings_callback_rc = Rc::new(RefCell::new(Callback::empty()));
59            let (view, close_item_popup_callback) = Self::menu_item_to_view(
60                item,
61                true,
62                &is_menu_active_prop,
63                &uncovered_controls,
64                &close_siblings_callback_rc,
65            );
66            content_prop.push(view);
67            close_item_popup_callbacks.push(close_item_popup_callback);
68            close_siblings_callbacks.push(close_siblings_callback_rc);
69        }
70
71        // setup sibling closing logic
72        for i in 0..close_siblings_callbacks.len() {
73            let mut close_item_popup_callbacks_for_i = Vec::new();
74            for j in 0..close_item_popup_callbacks.len() {
75                if j != i {
76                    close_item_popup_callbacks_for_i.push(close_item_popup_callbacks[j].clone());
77                }
78            }
79
80            close_siblings_callbacks[i].borrow_mut().set_sync(move |_| {
81                for i in 0..close_item_popup_callbacks_for_i.len() {
82                    close_item_popup_callbacks_for_i[i].emit(());
83                }
84            });
85        }
86
87        menu
88    }
89
90    fn menu_item_to_view(
91        menu_item: MenuItem,
92        is_top: bool,
93        is_menu_active_prop: &Property<bool>,
94        uncovered_controls: &Vec<Weak<RefCell<dyn ControlObject>>>,
95        close_siblings_callback_rc: &Rc<RefCell<Callback<()>>>,
96    ) -> (Rc<RefCell<dyn ControlObject>>, Callback<()>) {
97        match menu_item {
98            MenuItem::Separator => {
99                let separator: Rc<RefCell<dyn ControlObject>> = ui! {
100                    Text {
101                        Style: Default { color: [0.0f32, 0.0f32, 0.0f32, 1.0f32] },
102                        text: "---------"
103                    }
104                };
105                (separator, Callback::empty())
106            }
107
108            MenuItem::Text {
109                text,
110                shortcut: _shortcut,
111                icon: _icon,
112                callback,
113                sub_items,
114            } => {
115                let has_sub_items = sub_items.len() > 0;
116
117                let is_open_prop = Property::new(false);
118                let background_property = Property::new(Color::rgba(0.0, 0.0, 0.0, 0.0));
119                let foreground_property = Property::new(Color::rgba(0.0, 0.0, 0.0, 1.0));
120
121                let mut on_hover_callback = Callback::empty();
122                let mut on_tap_up_callback = Callback::empty();
123
124                if is_top {
125                    // top bar menu case
126
127                    // open sub menu on tap down
128                    let is_menu_active_prop_clone = is_menu_active_prop.clone();
129                    let is_open_prop_clone = is_open_prop.clone();
130                    on_tap_up_callback.set_sync(move |_| {
131                        if has_sub_items {
132                            is_menu_active_prop_clone.set(true);
133                            is_open_prop_clone.set(true);
134                        } else {
135                            // execute menu command
136                            callback.emit(());
137                        }
138                    });
139
140                    // hover highlights items even when menu is not active
141                    let background_property_clone = background_property.clone();
142                    let foreground_property_clone = foreground_property.clone();
143                    let is_menu_active_prop_clone = is_menu_active_prop.clone();
144                    let is_open_prop_clone = is_open_prop.clone();
145                    let close_siblings_callback_clone = close_siblings_callback_rc.clone();
146                    on_hover_callback.set_sync(move |value| {
147                        background_property_clone.set(
148                            if value || is_menu_active_prop_clone.get() {
149                                Color::rgba(0.0, 0.0, 0.0, 0.8)
150                            } else {
151                                Color::rgba(0.0, 0.0, 0.0, 0.0)
152                            },
153                        );
154                        foreground_property_clone.set(
155                            if value || is_menu_active_prop_clone.get() {
156                                Color::rgba(1.0, 1.0, 0.0, 1.0)
157                            } else {
158                                Color::rgba(0.0, 0.0, 0.0, 1.0)
159                            },
160                        );
161
162                        if value && is_menu_active_prop_clone.get() {
163                            // close all the other popups on the same level (siblings)
164                            close_siblings_callback_clone.borrow().emit(());
165
166                            // open popup if there are sub items
167                            if has_sub_items {
168                                is_open_prop_clone.set(true);
169                            }
170                        }
171                    });
172                } else {
173                    let background_property_clone = background_property.clone();
174                    let foreground_property_clone = foreground_property.clone();
175                    let is_open_prop_clone = is_open_prop.clone();
176                    let close_siblings_callback_clone = close_siblings_callback_rc.clone();
177                    on_hover_callback.set_sync(move |value| {
178                        background_property_clone.set(if value || has_sub_items {
179                            Color::rgba(0.0, 0.0, 0.0, 0.8)
180                        } else {
181                            Color::rgba(0.0, 0.0, 0.0, 0.0)
182                        });
183                        foreground_property_clone.set(if value || has_sub_items {
184                            Color::rgba(1.0, 1.0, 0.0, 1.0)
185                        } else {
186                            Color::rgba(0.0, 0.0, 0.0, 1.0)
187                        });
188
189                        if value {
190                            // close all the other popups on the same level (siblings)
191                            close_siblings_callback_clone.borrow().emit(());
192
193                            // open popup if there are sub items
194                            if has_sub_items {
195                                is_open_prop_clone.set(true);
196                            }
197                        }
198                    });
199
200                    let is_menu_active_prop_clone = is_menu_active_prop.clone();
201                    on_tap_up_callback.set_sync(move |_| {
202                        if !has_sub_items {
203                            // close menu
204                            is_menu_active_prop_clone.set(false);
205                            // execute menu command
206                            callback.emit(());
207                        }
208                    });
209                }
210
211                let title_content: Rc<RefCell<dyn ControlObject>> = if is_top {
212                    ui!(Text {
213                        Row: 0,
214                        Column: 1,
215                        Margin: Thickness::new(5.0f32, 0.0f32, 5.0f32, 0.0f32),
216                        Style: Dynamic {
217                            color: foreground_property.clone()
218                        },
219                        text: text
220                    })
221                } else {
222                    ui!(
223                        Grid {
224                            columns: 4,
225                            widths: vec![
226                                (0, Length::Exact(25.0f32)),
227                                (1, Length::Fill(1.0f32)),
228                                (2, Length::Auto),
229                                (3, Length::Exact(25.0f32)),
230                            ],
231
232                            Text {
233                                Row: 0, Column: 1,
234                                HorizontalAlignment: Alignment::Start,
235                                Style: Dynamic { color: foreground_property.clone() },
236
237                                text: text
238                            },
239
240                            Text {
241                                Row: 0, Column: 3,
242                                Style: Dynamic { color: foreground_property.clone() },
243                                text: if sub_items.len() > 0 { ">" } else { "" },
244                            }
245                        }
246                    )
247                };
248
249                // return callback that closes the popup
250                let mut close_popup_callback = Callback::empty();
251
252                let popup = if sub_items.len() == 0 {
253                    Children::None
254                } else {
255                    let sub_content_prop = ObservableVec::new();
256
257                    let popup_placement = if is_top {
258                        PopupPlacement::BelowOrAboveParent
259                    } else {
260                        PopupPlacement::LeftOrRightParent
261                    };
262
263                    let background_property_clone = background_property.clone();
264                    let foreground_property_clone = foreground_property.clone();
265                    let popup_close_subscription = is_open_prop.on_changed(move |value| {
266                        if value == false {
267                            background_property_clone.set(Color::rgba(0.0, 0.0, 0.0, 0.0));
268                            foreground_property_clone.set(Color::rgba(0.0, 0.0, 0.0, 1.0));
269                        }
270
271                        if is_top {
272                            //is_menu_active_prop_clone.set(false);
273                        }
274                    });
275
276                    let popup_content: Rc<RefCell<dyn ControlObject>> = ui!(
277                        Shadow {
278                            Style: Default { size: 12.0f32 },
279
280                            Border {
281                                border_type: BorderType::Raisen,
282                                Style: Default { background_color: Color::rgba(1.0, 1.0, 1.0, 0.8), },
283
284                                Grid {
285                                    columns: 1,
286                                    default_width: Length::Fill(1.0f32),
287                                    default_height: Length::Auto,
288
289                                    &sub_content_prop,
290                                }
291                            }
292                        }
293                    );
294
295                    let mut close_item_popup_callbacks = Vec::new();
296                    let mut close_siblings_callbacks = Vec::new();
297
298                    let mut uncovered_controls = uncovered_controls.to_vec();
299                    uncovered_controls.push(Rc::downgrade(&popup_content));
300                    for item in sub_items.into_iter() {
301                        let close_siblings_callback_rc = Rc::new(RefCell::new(Callback::empty()));
302                        let (view, close_item_popup_callback) = Self::menu_item_to_view(
303                            item,
304                            false,
305                            &is_menu_active_prop,
306                            &uncovered_controls,
307                            &close_siblings_callback_rc,
308                        );
309                        sub_content_prop.push(view);
310                        close_item_popup_callbacks.push(close_item_popup_callback);
311                        close_siblings_callbacks.push(close_siblings_callback_rc);
312                    }
313
314                    // setup sibling closing logic
315                    for i in 0..close_siblings_callbacks.len() {
316                        let mut close_item_popup_callbacks_for_i = Vec::new();
317                        for j in 0..close_item_popup_callbacks.len() {
318                            if j != i {
319                                close_item_popup_callbacks_for_i
320                                    .push(close_item_popup_callbacks[j].clone());
321                            }
322                        }
323
324                        close_siblings_callbacks[i].borrow_mut().set_sync(move |_| {
325                            for i in 0..close_item_popup_callbacks_for_i.len() {
326                                close_item_popup_callbacks_for_i[i].emit(());
327                            }
328                        });
329                    }
330
331                    // when clicked outside last open submenu
332                    // make whole menu inactive (and close all submenu windows)
333                    let mut auto_hide_occured_callback = Callback::empty();
334                    let is_menu_active_prop_clone = is_menu_active_prop.clone();
335                    auto_hide_occured_callback.set_sync(move |_| {
336                        is_menu_active_prop_clone.set(false);
337                    });
338
339                    let is_open_prop_clone = is_open_prop.clone();
340                    let is_menu_active_prop_changed =
341                        is_menu_active_prop.on_changed(move |value| {
342                            if !value {
343                                is_open_prop_clone.set(false);
344                            }
345                        });
346
347                    // return callback that closes the popup
348                    let is_open_prop_clone = is_open_prop.clone();
349                    close_popup_callback.set_sync(move |_| {
350                        // close this popup
351                        is_open_prop_clone.set(false);
352                        // close all sub-popups
353                        for subsibling in &close_siblings_callbacks {
354                            subsibling.borrow().emit(());
355                        }
356                    });
357
358                    let data_holder = DataHolder {
359                        data: (popup_close_subscription, is_menu_active_prop_changed),
360                    }
361                    .to_view(
362                        None,
363                        ViewContext {
364                            attached_values: TypeMap::new(),
365                            children: Children::None,
366                        },
367                    );
368
369                    let popup = ui!(Popup {
370                        is_open: is_open_prop,
371                        placement: popup_placement,
372                        auto_hide: PopupAutoHide::ClickedOutside,
373                        auto_hide_occured: auto_hide_occured_callback,
374                        uncovered_controls: uncovered_controls,
375
376                        popup_content,
377
378                        data_holder,
379                    });
380
381                    Children::SingleStatic(popup)
382                };
383
384                let content = ui!(
385                    GestureArea {
386                        hover_change: on_hover_callback,
387                        tap_up: on_tap_up_callback,
388
389                        Border {
390                            border_type: BorderType::None,
391                            Style: Default { background_color: background_property },
392
393                            title_content,
394                        },
395
396                        popup,
397                    }
398                );
399
400                (content, close_popup_callback)
401            }
402
403            MenuItem::Custom {
404                content,
405                callback: _,
406                sub_items: _,
407            } => (content, Callback::empty()),
408        }
409    }
410}