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