biji_ui/components/menubar/
context.rs

1use 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}