Skip to main content

cadmus_core/view/
common.rs

1use super::menu::{Menu, MenuKind};
2use super::notification::Notification;
3use super::{AppCmd, EntryId, EntryKind, RenderData, RenderQueue, View, ViewId};
4use crate::context::Context;
5use crate::device::CURRENT_DEVICE;
6use crate::fl;
7use crate::framebuffer::UpdateMode;
8use crate::geom::{Point, Rectangle};
9use crate::settings::{ButtonScheme, RotationLock};
10use chrono::Local;
11use std::sync::mpsc;
12
13pub fn shift(view: &mut dyn View, delta: Point) {
14    *view.rect_mut() += delta;
15    for child in view.children_mut().iter_mut() {
16        shift(child.as_mut(), delta);
17    }
18}
19
20pub fn locate<T: View>(view: &dyn View) -> Option<usize> {
21    for (index, child) in view.children().iter().enumerate() {
22        if child.as_ref().is::<T>() {
23            return Some(index);
24        }
25    }
26    None
27}
28
29pub fn rlocate<T: View>(view: &dyn View) -> Option<usize> {
30    for (index, child) in view.children().iter().enumerate().rev() {
31        if child.as_ref().is::<T>() {
32            return Some(index);
33        }
34    }
35    None
36}
37
38pub fn locate_by_id(view: &dyn View, id: ViewId) -> Option<usize> {
39    view.children()
40        .iter()
41        .position(|c| c.view_id().map_or(false, |i| i == id))
42}
43
44pub fn overlapping_rectangle(view: &dyn View) -> Rectangle {
45    let mut rect = *view.rect();
46    for child in view.children() {
47        rect.absorb(&overlapping_rectangle(child.as_ref()));
48    }
49    rect
50}
51
52// Transfer the notifications from the view1 to the view2.
53pub fn transfer_notifications(
54    view1: &mut dyn View,
55    view2: &mut dyn View,
56    rq: &mut RenderQueue,
57    context: &mut Context,
58) {
59    for index in (0..view1.len()).rev() {
60        if view1.child(index).is::<Notification>() {
61            let mut child = view1.children_mut().remove(index);
62            if view2.rect() != view1.rect() {
63                let (tx, _rx) = mpsc::channel();
64                child.resize(*view2.rect(), &tx, rq, context);
65            }
66            view2.children_mut().push(child);
67        }
68    }
69}
70
71/// Recursively searches the view tree for a notification with the given ViewId.
72///
73/// # Arguments
74///
75/// * `view` - The root view to start searching from
76/// * `id` - The ViewId to search for
77///
78/// # Returns
79///
80/// A mutable reference to the Notification if found, or `None` if not found.
81///
82/// # Note
83///
84/// This function performs a depth-first search through the entire view hierarchy.
85/// It will find the first notification that matches the given id.
86pub fn find_notification_mut(view: &mut dyn View, id: ViewId) -> Option<&mut Notification> {
87    if view.is::<Notification>() && view.view_id() == Some(id) {
88        return view.downcast_mut::<Notification>();
89    }
90
91    for child in view.children_mut() {
92        if let Some(notif) = find_notification_mut(child.as_mut(), id) {
93            return Some(notif);
94        }
95    }
96
97    None
98}
99
100pub fn toggle_main_menu(
101    view: &mut dyn View,
102    rect: Rectangle,
103    enable: Option<bool>,
104    rq: &mut RenderQueue,
105    context: &mut Context,
106) {
107    if let Some(index) = locate_by_id(view, ViewId::MainMenu) {
108        if let Some(true) = enable {
109            return;
110        }
111        rq.add(RenderData::expose(
112            *view.child(index).rect(),
113            UpdateMode::Gui,
114        ));
115        view.children_mut().remove(index);
116    } else {
117        if let Some(false) = enable {
118            return;
119        }
120
121        let rotation = CURRENT_DEVICE.to_canonical(context.display.rotation);
122        let rotate = (0..4)
123            .map(|n| {
124                EntryKind::RadioButton(
125                    (n as i16 * 90).to_string(),
126                    EntryId::Rotate(CURRENT_DEVICE.from_canonical(n)),
127                    n == rotation,
128                )
129            })
130            .collect::<Vec<EntryKind>>();
131
132        let apps = vec![
133            EntryKind::Command(
134                "Dictionary".to_string(),
135                EntryId::Launch(AppCmd::Dictionary {
136                    query: "".to_string(),
137                    language: "".to_string(),
138                }),
139            ),
140            EntryKind::Command(
141                "Calculator".to_string(),
142                EntryId::Launch(AppCmd::Calculator),
143            ),
144            EntryKind::Command("Sketch".to_string(), EntryId::Launch(AppCmd::Sketch)),
145            EntryKind::Separator,
146            EntryKind::Command(
147                "Touch Events".to_string(),
148                EntryId::Launch(AppCmd::TouchEvents),
149            ),
150            EntryKind::Command(
151                "Rotation Values".to_string(),
152                EntryId::Launch(AppCmd::RotationValues),
153            ),
154        ];
155        let mut entries = vec![
156            EntryKind::Command("About".to_string(), EntryId::About),
157            EntryKind::Command("System Info".to_string(), EntryId::SystemInfo),
158            EntryKind::Command(
159                "Settings".to_string(),
160                EntryId::Launch(AppCmd::SettingsEditor),
161            ),
162            EntryKind::Command("Check for Updates".to_string(), EntryId::CheckForUpdates),
163            EntryKind::Separator,
164        ];
165
166        if CURRENT_DEVICE.has_gyroscope() {
167            let rotation_lock = context.settings.rotation_lock;
168            let gyro = vec![
169                EntryKind::RadioButton(
170                    "Auto".to_string(),
171                    EntryId::SetRotationLock(None),
172                    rotation_lock.is_none(),
173                ),
174                EntryKind::Separator,
175                EntryKind::RadioButton(
176                    "Portrait".to_string(),
177                    EntryId::SetRotationLock(Some(RotationLock::Portrait)),
178                    rotation_lock == Some(RotationLock::Portrait),
179                ),
180                EntryKind::RadioButton(
181                    "Landscape".to_string(),
182                    EntryId::SetRotationLock(Some(RotationLock::Landscape)),
183                    rotation_lock == Some(RotationLock::Landscape),
184                ),
185                EntryKind::RadioButton(
186                    "Ignore".to_string(),
187                    EntryId::SetRotationLock(Some(RotationLock::Current)),
188                    rotation_lock == Some(RotationLock::Current),
189                ),
190            ];
191            entries.push(EntryKind::SubMenu("Gyroscope".to_string(), gyro));
192        }
193
194        if CURRENT_DEVICE.has_page_turn_buttons() {
195            let button_scheme = context.settings.button_scheme;
196            let button_schemes = vec![
197                EntryKind::RadioButton(
198                    ButtonScheme::Natural.to_string(),
199                    EntryId::SetButtonScheme(ButtonScheme::Natural),
200                    button_scheme == ButtonScheme::Natural,
201                ),
202                EntryKind::RadioButton(
203                    ButtonScheme::Inverted.to_string(),
204                    EntryId::SetButtonScheme(ButtonScheme::Inverted),
205                    button_scheme == ButtonScheme::Inverted,
206                ),
207            ];
208            entries.push(EntryKind::SubMenu(
209                "Button Scheme".to_string(),
210                button_schemes,
211            ));
212        }
213
214        entries.extend(vec![
215            EntryKind::CheckBox(
216                "Invert Colors".to_string(),
217                EntryId::ToggleInverted,
218                context.fb.inverted(),
219            ),
220            EntryKind::CheckBox(
221                "Enable WiFi".to_string(),
222                EntryId::ToggleWifi,
223                context.settings.wifi,
224            ),
225            EntryKind::Separator,
226            EntryKind::SubMenu("Rotate".to_string(), rotate),
227            EntryKind::Command("Take Screenshot".to_string(), EntryId::TakeScreenshot),
228            EntryKind::Separator,
229            EntryKind::SubMenu("Applications".to_string(), apps),
230            EntryKind::Separator,
231            EntryKind::SubMenu(
232                fl!("top-menu-exit").to_string(),
233                vec![
234                    EntryKind::Command(fl!("top-menu-suspend").to_string(), EntryId::Suspend),
235                    EntryKind::Command(fl!("top-menu-restart-app").to_string(), EntryId::Restart),
236                    EntryKind::Command(fl!("top-menu-reboot-device").to_string(), EntryId::Reboot),
237                    EntryKind::Command(fl!("top-menu-power-off").to_string(), EntryId::PowerOff),
238                    EntryKind::Command(fl!("top-menu-quit").to_string(), EntryId::Quit),
239                ],
240            ),
241        ]);
242
243        let main_menu = Menu::new(rect, ViewId::MainMenu, MenuKind::DropDown, entries, context);
244        rq.add(RenderData::new(
245            main_menu.id(),
246            *main_menu.rect(),
247            UpdateMode::Gui,
248        ));
249        view.children_mut()
250            .push(Box::new(main_menu) as Box<dyn View>);
251    }
252}
253
254pub fn toggle_battery_menu(
255    view: &mut dyn View,
256    rect: Rectangle,
257    enable: Option<bool>,
258    rq: &mut RenderQueue,
259    context: &mut Context,
260) {
261    if let Some(index) = locate_by_id(view, ViewId::BatteryMenu) {
262        if let Some(true) = enable {
263            return;
264        }
265        rq.add(RenderData::expose(
266            *view.child(index).rect(),
267            UpdateMode::Gui,
268        ));
269        view.children_mut().remove(index);
270    } else {
271        if let Some(false) = enable {
272            return;
273        }
274
275        let mut entries = Vec::new();
276
277        match context
278            .battery
279            .status()
280            .ok()
281            .zip(context.battery.capacity().ok())
282        {
283            Some((status, capacity)) => {
284                for (i, (s, c)) in status.iter().zip(capacity.iter()).enumerate() {
285                    entries.push(EntryKind::Message(
286                        format!("{:?} {}%", s, c),
287                        if i > 0 {
288                            Some("cover".to_string())
289                        } else {
290                            None
291                        },
292                    ));
293                }
294            }
295            _ => {
296                entries.push(EntryKind::Message(
297                    "Information Unavailable".to_string(),
298                    None,
299                ));
300            }
301        }
302
303        let battery_menu = Menu::new(
304            rect,
305            ViewId::BatteryMenu,
306            MenuKind::DropDown,
307            entries,
308            context,
309        );
310        rq.add(RenderData::new(
311            battery_menu.id(),
312            *battery_menu.rect(),
313            UpdateMode::Gui,
314        ));
315        view.children_mut()
316            .push(Box::new(battery_menu) as Box<dyn View>);
317    }
318}
319
320pub fn toggle_clock_menu(
321    view: &mut dyn View,
322    rect: Rectangle,
323    enable: Option<bool>,
324    rq: &mut RenderQueue,
325    context: &mut Context,
326) {
327    if let Some(index) = locate_by_id(view, ViewId::ClockMenu) {
328        if let Some(true) = enable {
329            return;
330        }
331        rq.add(RenderData::expose(
332            *view.child(index).rect(),
333            UpdateMode::Gui,
334        ));
335        view.children_mut().remove(index);
336    } else {
337        if let Some(false) = enable {
338            return;
339        }
340        let text = Local::now()
341            .format(&context.settings.date_format)
342            .to_string();
343        let entries = vec![
344            EntryKind::Message(text, None),
345            EntryKind::Separator,
346            EntryKind::Command(fl!("top-menu-sync-time"), EntryId::SyncTime),
347        ];
348
349        let clock_menu = Menu::new(
350            rect,
351            ViewId::ClockMenu,
352            MenuKind::DropDown,
353            entries,
354            context,
355        );
356        rq.add(RenderData::new(
357            clock_menu.id(),
358            *clock_menu.rect(),
359            UpdateMode::Gui,
360        ));
361        view.children_mut()
362            .push(Box::new(clock_menu) as Box<dyn View>);
363    }
364}
365
366pub fn toggle_input_history_menu(
367    view: &mut dyn View,
368    id: ViewId,
369    rect: Rectangle,
370    enable: Option<bool>,
371    rq: &mut RenderQueue,
372    context: &mut Context,
373) {
374    if let Some(index) = locate_by_id(view, ViewId::InputHistoryMenu) {
375        if let Some(true) = enable {
376            return;
377        }
378        rq.add(RenderData::expose(
379            *view.child(index).rect(),
380            UpdateMode::Gui,
381        ));
382        view.children_mut().remove(index);
383    } else {
384        if let Some(false) = enable {
385            return;
386        }
387        let entries = context.input_history.get(&id).map(|h| {
388            h.iter()
389                .map(|s| {
390                    EntryKind::Command(s.to_string(), EntryId::SetInputText(id, s.to_string()))
391                })
392                .collect::<Vec<EntryKind>>()
393        });
394        if let Some(entries) = entries {
395            let menu_kind = match id {
396                ViewId::HomeSearchInput
397                | ViewId::ReaderSearchInput
398                | ViewId::DictionarySearchInput
399                | ViewId::CalculatorInput => MenuKind::DropDown,
400                _ => MenuKind::Contextual,
401            };
402            let input_history_menu =
403                Menu::new(rect, ViewId::InputHistoryMenu, menu_kind, entries, context);
404            rq.add(RenderData::new(
405                input_history_menu.id(),
406                *input_history_menu.rect(),
407                UpdateMode::Gui,
408            ));
409            view.children_mut()
410                .push(Box::new(input_history_menu) as Box<dyn View>);
411        }
412    }
413}
414
415pub fn toggle_keyboard_layout_menu(
416    view: &mut dyn View,
417    rect: Rectangle,
418    enable: Option<bool>,
419    rq: &mut RenderQueue,
420    context: &mut Context,
421) {
422    if let Some(index) = locate_by_id(view, ViewId::KeyboardLayoutMenu) {
423        if let Some(true) = enable {
424            return;
425        }
426        rq.add(RenderData::expose(
427            *view.child(index).rect(),
428            UpdateMode::Gui,
429        ));
430        view.children_mut().remove(index);
431    } else {
432        if let Some(false) = enable {
433            return;
434        }
435        let entries = context
436            .keyboard_layouts
437            .keys()
438            .map(|s| EntryKind::Command(s.to_string(), EntryId::SetKeyboardLayout(s.to_string())))
439            .collect::<Vec<EntryKind>>();
440        let keyboard_layout_menu = Menu::new(
441            rect,
442            ViewId::KeyboardLayoutMenu,
443            MenuKind::Contextual,
444            entries,
445            context,
446        );
447        rq.add(RenderData::new(
448            keyboard_layout_menu.id(),
449            *keyboard_layout_menu.rect(),
450            UpdateMode::Gui,
451        ));
452        view.children_mut()
453            .push(Box::new(keyboard_layout_menu) as Box<dyn View>);
454    }
455}