biji_ui/components/menubar/
context.rs1use 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}