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![EntryKind::Message(text, None)];
344        let clock_menu = Menu::new(
345            rect,
346            ViewId::ClockMenu,
347            MenuKind::DropDown,
348            entries,
349            context,
350        );
351        rq.add(RenderData::new(
352            clock_menu.id(),
353            *clock_menu.rect(),
354            UpdateMode::Gui,
355        ));
356        view.children_mut()
357            .push(Box::new(clock_menu) as Box<dyn View>);
358    }
359}
360
361pub fn toggle_input_history_menu(
362    view: &mut dyn View,
363    id: ViewId,
364    rect: Rectangle,
365    enable: Option<bool>,
366    rq: &mut RenderQueue,
367    context: &mut Context,
368) {
369    if let Some(index) = locate_by_id(view, ViewId::InputHistoryMenu) {
370        if let Some(true) = enable {
371            return;
372        }
373        rq.add(RenderData::expose(
374            *view.child(index).rect(),
375            UpdateMode::Gui,
376        ));
377        view.children_mut().remove(index);
378    } else {
379        if let Some(false) = enable {
380            return;
381        }
382        let entries = context.input_history.get(&id).map(|h| {
383            h.iter()
384                .map(|s| {
385                    EntryKind::Command(s.to_string(), EntryId::SetInputText(id, s.to_string()))
386                })
387                .collect::<Vec<EntryKind>>()
388        });
389        if let Some(entries) = entries {
390            let menu_kind = match id {
391                ViewId::HomeSearchInput
392                | ViewId::ReaderSearchInput
393                | ViewId::DictionarySearchInput
394                | ViewId::CalculatorInput => MenuKind::DropDown,
395                _ => MenuKind::Contextual,
396            };
397            let input_history_menu =
398                Menu::new(rect, ViewId::InputHistoryMenu, menu_kind, entries, context);
399            rq.add(RenderData::new(
400                input_history_menu.id(),
401                *input_history_menu.rect(),
402                UpdateMode::Gui,
403            ));
404            view.children_mut()
405                .push(Box::new(input_history_menu) as Box<dyn View>);
406        }
407    }
408}
409
410pub fn toggle_keyboard_layout_menu(
411    view: &mut dyn View,
412    rect: Rectangle,
413    enable: Option<bool>,
414    rq: &mut RenderQueue,
415    context: &mut Context,
416) {
417    if let Some(index) = locate_by_id(view, ViewId::KeyboardLayoutMenu) {
418        if let Some(true) = enable {
419            return;
420        }
421        rq.add(RenderData::expose(
422            *view.child(index).rect(),
423            UpdateMode::Gui,
424        ));
425        view.children_mut().remove(index);
426    } else {
427        if let Some(false) = enable {
428            return;
429        }
430        let entries = context
431            .keyboard_layouts
432            .keys()
433            .map(|s| EntryKind::Command(s.to_string(), EntryId::SetKeyboardLayout(s.to_string())))
434            .collect::<Vec<EntryKind>>();
435        let keyboard_layout_menu = Menu::new(
436            rect,
437            ViewId::KeyboardLayoutMenu,
438            MenuKind::Contextual,
439            entries,
440            context,
441        );
442        rq.add(RenderData::new(
443            keyboard_layout_menu.id(),
444            *keyboard_layout_menu.rect(),
445            UpdateMode::Gui,
446        ));
447        view.children_mut()
448            .push(Box::new(keyboard_layout_menu) as Box<dyn View>);
449    }
450}