biji_ui/components/menubar/
context.rs

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