fret_ui_kit/primitives/menu/
sub_trigger.rs1use std::sync::Arc;
11
12use fret_core::{KeyCode, Rect, Size};
13use fret_ui::element::PressableState;
14use fret_ui::elements::GlobalElementId;
15use fret_ui::{ElementContext, UiHost};
16
17use crate::declarative::model_watch::ModelWatchExt as _;
18use crate::on_activate;
19use crate::primitives::direction::{self as direction_prim, LayoutDirection};
20use crate::primitives::menu::sub;
21
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct MenuSubTriggerGeometryHint {
24 pub outer: Rect,
25 pub desired: Size,
26}
27
28#[allow(clippy::too_many_arguments)]
32pub fn wire<H: UiHost>(
33 cx: &mut ElementContext<'_, H>,
34 st: PressableState,
35 item_id: GlobalElementId,
36 disabled: bool,
37 has_submenu: bool,
38 value: Arc<str>,
39 models: &sub::MenuSubmenuModels,
40 cfg: sub::MenuSubmenuConfig,
41 geometry_hint: Option<MenuSubTriggerGeometryHint>,
42) -> Option<bool> {
43 if disabled {
44 return has_submenu.then_some(false);
45 }
46
47 if has_submenu {
48 sub::install_timer_handler(cx, item_id, models.clone(), cfg);
52
53 let models_for_hover = models.clone();
54 let value_for_hover = value.clone();
55 let cfg_for_hover = cfg;
56 let trigger_id_for_hover = item_id;
57 cx.pressable_add_on_hover_change(Arc::new(move |host, acx, is_hovered| {
58 sub::handle_sub_trigger_hover_change(
59 host,
60 acx,
61 &models_for_hover,
62 cfg_for_hover,
63 trigger_id_for_hover,
64 is_hovered,
65 value_for_hover.clone(),
66 );
67 }));
68 }
69
70 if st.hovered {
71 sub::sync_while_trigger_hovered(cx, models, cfg, has_submenu, value.clone(), item_id);
72 }
73
74 if st.focused {
75 sub::close_if_focus_moved_without_pointer(cx, models, &value, item_id);
76 }
77
78 if has_submenu {
79 let models_for_activate = models.clone();
80 let value_for_activate = value.clone();
81 cx.pressable_add_on_activate(on_activate(move |host, acx, _reason| {
82 sub::open_on_activate(host, acx, &models_for_activate, value_for_activate.clone());
83 }));
84 }
85
86 let key_has_submenu = has_submenu;
87 let models_for_key = models.clone();
88 let value_for_key = value.clone();
89 let cfg_for_key = cfg;
90 let trigger_id_for_key = item_id;
91 let dir = direction_prim::use_direction_in_scope(cx, None);
92 cx.key_on_key_down_for(
93 item_id,
94 Arc::new(move |host, acx, down| {
95 if down.repeat {
96 return false;
97 }
98 let is_open_key = matches!(
99 (down.key, dir),
100 (KeyCode::ArrowRight, LayoutDirection::Ltr)
101 | (KeyCode::ArrowLeft, LayoutDirection::Rtl)
102 );
103 if is_open_key {
104 if !key_has_submenu {
105 return false;
106 }
107 sub::open_on_arrow_right(
108 host,
109 acx,
110 &models_for_key,
111 trigger_id_for_key,
112 value_for_key.clone(),
113 cfg_for_key.focus_delay,
114 );
115 return true;
116 }
117
118 let is_close_key = matches!(
119 (down.key, dir),
120 (KeyCode::ArrowLeft, LayoutDirection::Ltr)
121 | (KeyCode::ArrowRight, LayoutDirection::Rtl)
122 );
123 if is_close_key {
124 let is_open = host
125 .models_mut()
126 .read(&models_for_key.open_value, |v| v.is_some())
127 .ok()
128 .unwrap_or(false);
129 if !is_open {
130 return false;
131 }
132
133 sub::close_on_arrow_left(host, acx, &models_for_key);
134 return true;
135 }
136
137 false
138 }),
139 );
140
141 let expanded = cx
142 .watch_model(&models.open_value)
143 .cloned()
144 .unwrap_or(None)
145 .as_ref()
146 .is_some_and(|cur: &Arc<str>| cur.as_ref() == value.as_ref());
147
148 if has_submenu && expanded {
149 sub::set_trigger_if_none(cx, item_id, &models.trigger);
150
151 let open_trigger = cx
152 .app
153 .models_mut()
154 .read(&models.trigger, |v| *v)
155 .ok()
156 .flatten();
157 if open_trigger.is_none_or(|t| t == item_id)
158 && let Some(hint) = geometry_hint
159 {
160 sub::set_geometry_from_element_anchor_if_present(
161 cx,
162 item_id,
163 models,
164 hint.outer,
165 hint.desired,
166 );
167 }
168 }
169
170 has_submenu.then_some(expanded)
171}