biji_ui/components/menubar/
context.rs1use std::{collections::HashMap, time::Duration};
2
3use leptos::{html::Div, prelude::*};
4
5use crate::{
6 items::{
7 filter_active, next_item, previous_item, FilterActiveItems, Focus, GetIndex, IsActive,
8 ManageFocus, NavigateItems, Toggle,
9 },
10 utils::positioning::Positioning,
11};
12
13#[derive(Copy, Clone)]
14pub struct MenubarContext {
15 pub menubar_ref: NodeRef<Div>,
16 pub root: RwSignal<RootContext>,
17}
18
19#[derive(Copy, Clone)]
20pub struct RootContext {
21 pub item_focus: RwSignal<Option<usize>>,
22 pub items: RwSignal<HashMap<usize, MenuContext>>,
23 pub allow_menu_loop: bool,
24 pub allow_item_loop: bool,
25 pub prevent_scroll: bool,
26}
27
28impl Default for RootContext {
29 fn default() -> Self {
30 Self {
31 item_focus: RwSignal::new(None),
32 items: RwSignal::new(HashMap::new()),
33 allow_menu_loop: false,
34 allow_item_loop: false,
35 prevent_scroll: false,
36 }
37 }
38}
39
40impl RootContext {
41 pub fn upsert_item(&self, index: usize, item: MenuContext) {
42 self.items.update(|items| {
43 *items.entry(index).or_insert(item) = item;
44 });
45 }
46
47 pub fn remove_item(&self, index: usize) {
48 self.items.update(|items| {
49 items.remove(&index);
50 });
51 }
52
53 pub fn close_all(&self) {
54 self.items.try_update(|items| {
55 for item in items.values() {
56 item.open.set(false);
57 }
58 });
59 }
60
61 pub fn next_index(&self) -> usize {
62 self.items.get_untracked().len()
63 }
64
65 pub fn focus_active_item(&self) -> bool {
66 if let Some(Some(item_focus)) = self.item_focus.try_get_untracked() {
67 if let Some(item) = self.items.get_untracked().get(&item_focus) {
68 return item.focus();
69 }
70 }
71 false
72 }
73}
74
75impl FilterActiveItems<MenuContext> for RootContext {
76 fn filter_active_items(&self) -> Vec<MenuContext> {
77 filter_active(self.items.get())
78 }
79}
80
81impl NavigateItems<MenuContext> for RootContext {
82 fn navigate_first_item(&self) -> Option<MenuContext> {
83 let active_items = self.filter_active_items();
84
85 if let Some(first) = active_items.get(0) {
86 return Some(first.clone());
87 }
88 None
89 }
90
91 fn navigate_last_item(&self) -> Option<MenuContext> {
92 let active_items = self.filter_active_items();
93
94 if let Some(last) = active_items.last() {
95 return Some(last.clone());
96 }
97 None
98 }
99
100 fn navigate_next_item(&self) -> Option<MenuContext> {
101 let active_items = self.filter_active_items();
102
103 next_item(active_items, self.item_focus.get(), self.allow_menu_loop)
104 }
105
106 fn navigate_previous_item(&self) -> Option<MenuContext> {
107 let active_items = self.filter_active_items();
108
109 previous_item(active_items, self.item_focus.get(), self.allow_menu_loop)
110 }
111}
112
113impl ManageFocus for RootContext {
114 fn set_focus(&self, index: Option<usize>) {
115 self.item_focus.set(index);
116 }
117
118 fn item_in_focus(&self, index: usize) -> bool {
119 self.item_focus.get() == Some(index)
120 }
121}
122
123#[derive(Copy, Clone)]
124pub struct MenuContext {
125 pub index: usize,
126 pub disabled: bool,
127 pub open: RwSignal<bool>,
128 pub menu_ref: NodeRef<Div>,
129 pub trigger_ref: NodeRef<Div>,
130 pub item_focus: RwSignal<Option<usize>>,
131 pub items: RwSignal<HashMap<usize, ItemData>>,
132 pub allow_loop: bool,
133 pub positioning: Positioning,
134 pub hide_delay: Duration,
135}
136
137impl Default for MenuContext {
138 fn default() -> Self {
139 Self {
140 index: 0,
141 disabled: false,
142 open: RwSignal::new(false),
143 menu_ref: NodeRef::default(),
144 trigger_ref: NodeRef::default(),
145 item_focus: RwSignal::new(None),
146 items: RwSignal::new(HashMap::new()),
147 allow_loop: false,
148 positioning: Positioning::BottomStart,
149 hide_delay: Duration::from_millis(200),
150 }
151 }
152}
153
154impl MenuContext {
155 pub fn upsert_item(&self, index: usize, item: ItemData) {
156 self.items.update(|items| {
157 *items.entry(index).or_insert(item) = item;
158 });
159 }
160
161 pub fn remove_item(&self, index: usize) {
162 self.items.update(|items| {
163 items.remove(&index);
164 });
165 }
166
167 pub fn next_index(&self) -> usize {
168 self.items.get_untracked().len()
169 }
170
171 pub fn close_all(&self) {
172 self.items.try_update(|items| {
173 for item in items.values() {
174 if let ItemData::SubMenuItem { child_context, .. } = item {
175 child_context.close();
176 }
177 }
178 });
179 }
180}
181
182impl IsActive for MenuContext {
183 fn is_active(&self) -> bool {
184 !self.disabled
185 }
186}
187
188impl GetIndex<usize> for MenuContext {
189 fn get_index(&self) -> usize {
190 self.index
191 }
192}
193
194impl Focus for MenuContext {
195 fn focus(&self) -> bool {
196 let Some(trigger_ref) = self.trigger_ref.get() else {
197 return false;
198 };
199
200 let _ = trigger_ref.focus();
201 true
202 }
203}
204
205impl ManageFocus for MenuContext {
206 fn set_focus(&self, index: Option<usize>) {
207 self.item_focus.set(index);
208 }
209
210 fn item_in_focus(&self, index: usize) -> bool {
211 self.item_focus.get() == Some(index)
212 }
213}
214
215impl FilterActiveItems<ItemData> for MenuContext {
216 fn filter_active_items(&self) -> Vec<ItemData> {
217 filter_active(self.items.get())
218 }
219}
220
221impl NavigateItems<ItemData> for MenuContext {
222 fn navigate_first_item(&self) -> Option<ItemData> {
223 let active_items = self.filter_active_items();
224
225 if let Some(first) = active_items.get(0) {
226 return Some(first.clone());
227 }
228 None
229 }
230
231 fn navigate_last_item(&self) -> Option<ItemData> {
232 let active_items = self.filter_active_items();
233
234 if let Some(last) = active_items.last() {
235 return Some(last.clone());
236 }
237 None
238 }
239
240 fn navigate_next_item(&self) -> Option<ItemData> {
241 let active_items = self.filter_active_items();
242
243 next_item(active_items, self.item_focus.get(), self.allow_loop)
244 }
245
246 fn navigate_previous_item(&self) -> Option<ItemData> {
247 let active_items = self.filter_active_items();
248
249 previous_item(active_items, self.item_focus.get(), self.allow_loop)
250 }
251}
252
253impl Toggle for MenuContext {
254 fn toggle(&self) {
255 self.open.set(!self.open.get());
256 }
257
258 fn open(&self) {
259 self.open.set(true);
260 }
261
262 fn close(&self) {
263 self.open.set(false);
264 }
265}
266
267#[derive(Copy, Clone)]
268pub enum ItemData {
269 Item {
270 index: usize,
271 disabled: bool,
272 trigger_ref: NodeRef<Div>,
273 is_submenu: bool,
274 },
275 SubMenuItem {
276 index: usize,
277 disabled: bool,
278 is_submenu: bool,
279 parent_context: MenuContext,
280 child_context: MenuContext,
281 },
282}
283
284impl ItemData {
285 pub fn get_trigger_ref(&self) -> NodeRef<Div> {
286 match self {
287 ItemData::Item { trigger_ref, .. } => trigger_ref.clone(),
288 ItemData::SubMenuItem {
289 child_context: context,
290 ..
291 } => context.trigger_ref.clone(),
292 }
293 }
294
295 pub fn is_submenu(&self) -> bool {
296 match self {
297 ItemData::Item { is_submenu, .. } => *is_submenu,
298 ItemData::SubMenuItem { is_submenu, .. } => *is_submenu,
299 }
300 }
301
302 pub fn get_disabled(&self) -> bool {
303 match self {
304 ItemData::Item { disabled, .. } => *disabled,
305 ItemData::SubMenuItem { disabled, .. } => *disabled,
306 }
307 }
308}
309
310impl IsActive for ItemData {
311 fn is_active(&self) -> bool {
312 match self {
313 ItemData::Item { disabled, .. } => !disabled,
314 ItemData::SubMenuItem { disabled, .. } => !disabled,
315 }
316 }
317}
318
319impl GetIndex<usize> for ItemData {
320 fn get_index(&self) -> usize {
321 match self {
322 ItemData::Item { index, .. } => *index,
323 ItemData::SubMenuItem { index, .. } => *index,
324 }
325 }
326}
327
328impl Focus for ItemData {
329 fn focus(&self) -> bool {
330 match self {
331 ItemData::Item { trigger_ref, .. } => {
332 let Some(trigger_ref) = trigger_ref.get() else {
333 return false;
334 };
335
336 let _ = trigger_ref.focus();
337 true
338 }
339 ItemData::SubMenuItem {
340 child_context: context,
341 ..
342 } => {
343 let Some(trigger_ref) = context.trigger_ref.get() else {
344 return false;
345 };
346
347 let _ = trigger_ref.focus();
348 true
349 }
350 }
351 }
352}