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 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 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 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 callback.emit(());
137 }
138 });
139
140 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_siblings_callback_clone.borrow().emit(());
165
166 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_siblings_callback_clone.borrow().emit(());
192
193 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 is_menu_active_prop_clone.set(false);
205 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 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 }
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 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 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 let is_open_prop_clone = is_open_prop.clone();
349 close_popup_callback.set_sync(move |_| {
350 is_open_prop_clone.set(false);
352 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}