cadmus_core/view/
mod.rs

1//! Views are organized as a tree. A view might receive / send events and render itself.
2//!
3//! The z-level of the n-th child of a view is less or equal to the z-level of its n+1-th child.
4//!
5//! Events travel from the root to the leaves, only the leaf views will handle the root events, but
6//! any view can send events to its parent. From the events it receives from its children, a view
7//! resends the ones it doesn't handle to its own parent. Hence an event sent from a child might
8//! bubble up to the root. If it reaches the root without being captured by any view, then it will
9//! be written to the main event channel and will be sent to every leaf in one of the next loop
10//! iterations.
11
12pub mod action_label;
13pub mod battery;
14pub mod button;
15pub mod calculator;
16pub mod clock;
17pub mod common;
18pub mod device_auth;
19pub mod dialog;
20pub mod dictionary;
21pub mod file_chooser;
22pub mod filler;
23pub mod frontlight;
24pub mod github;
25pub mod home;
26pub mod icon;
27pub mod image;
28pub mod input_field;
29pub mod intermission;
30pub mod key;
31pub mod keyboard;
32pub mod label;
33pub mod labeled_icon;
34pub mod menu;
35pub mod menu_entry;
36pub mod named_input;
37pub mod navigation;
38pub mod notification;
39pub mod ota;
40
41pub use self::notification::NotificationEvent;
42pub mod page_label;
43pub mod preset;
44pub mod presets_list;
45pub mod progress_bar;
46pub mod reader;
47pub mod rotation_values;
48pub mod rounded_button;
49pub mod search_bar;
50pub mod settings_editor;
51pub mod sketch;
52pub mod slider;
53pub mod startup;
54pub mod toggle;
55pub mod toggleable_keyboard;
56pub mod top_bar;
57pub mod touch_events;
58
59use self::calculator::LineOrigin;
60use self::github::GithubEvent;
61use self::key::KeyKind;
62use crate::color::Color;
63use crate::context::Context;
64use crate::document::{Location, TextLocation};
65use crate::font::Fonts;
66use crate::framebuffer::{Framebuffer, UpdateMode};
67use crate::geom::{Boundary, CycleDir, LinearDir, Rectangle};
68use crate::gesture::GestureEvent;
69use crate::input::{DeviceEvent, FingerStatus};
70use crate::metadata::{
71    Info, Margin, PageScheme, ScrollMode, SimpleStatus, SortMethod, TextAlign, ZoomMode,
72};
73use crate::settings::{
74    self, ButtonScheme, FinishedAction, FirstColumn, RotationLock, SecondColumn,
75};
76use crate::view::ota::OtaEntryId;
77use downcast_rs::{impl_downcast, Downcast};
78use fxhash::FxHashMap;
79use std::collections::VecDeque;
80use std::fmt::{self, Debug};
81use std::ops::{Deref, DerefMut};
82use std::path::PathBuf;
83use std::sync::atomic::{AtomicU64, Ordering};
84use std::sync::mpsc::Sender;
85use std::time::{Duration, Instant};
86use tracing::error;
87use unic_langid::LanguageIdentifier;
88
89// Border thicknesses in pixels, at 300 DPI.
90pub const THICKNESS_SMALL: f32 = 1.0;
91pub const THICKNESS_MEDIUM: f32 = 2.0;
92pub const THICKNESS_LARGE: f32 = 3.0;
93
94// Border radii in pixels, at 300 DPI.
95pub const BORDER_RADIUS_SMALL: f32 = 6.0;
96pub const BORDER_RADIUS_MEDIUM: f32 = 9.0;
97pub const BORDER_RADIUS_LARGE: f32 = 12.0;
98
99// Big and small bar heights in pixels, at 300 DPI.
100// On the *Aura ONE*, the height is exactly `2 * sb + 10 * bb`.
101pub const SMALL_BAR_HEIGHT: f32 = 121.0;
102pub const BIG_BAR_HEIGHT: f32 = 163.0;
103
104pub const CLOSE_IGNITION_DELAY: Duration = Duration::from_millis(150);
105
106pub type Bus = VecDeque<Event>;
107pub type Hub = Sender<Event>;
108
109pub trait View: Downcast {
110    fn handle_event(
111        &mut self,
112        evt: &Event,
113        hub: &Hub,
114        bus: &mut Bus,
115        rq: &mut RenderQueue,
116        context: &mut Context,
117    ) -> bool;
118    fn render(&self, fb: &mut dyn Framebuffer, rect: Rectangle, fonts: &mut Fonts);
119    fn rect(&self) -> &Rectangle;
120    fn rect_mut(&mut self) -> &mut Rectangle;
121    fn children(&self) -> &Vec<Box<dyn View>>;
122    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>>;
123    fn id(&self) -> Id;
124
125    fn render_rect(&self, _rect: &Rectangle) -> Rectangle {
126        *self.rect()
127    }
128
129    fn resize(
130        &mut self,
131        rect: Rectangle,
132        _hub: &Hub,
133        _rq: &mut RenderQueue,
134        _context: &mut Context,
135    ) {
136        *self.rect_mut() = rect;
137    }
138
139    fn child(&self, index: usize) -> &dyn View {
140        self.children()[index].as_ref()
141    }
142
143    fn child_mut(&mut self, index: usize) -> &mut dyn View {
144        self.children_mut()[index].as_mut()
145    }
146
147    fn len(&self) -> usize {
148        self.children().len()
149    }
150
151    fn might_skip(&self, _evt: &Event) -> bool {
152        false
153    }
154
155    fn might_rotate(&self) -> bool {
156        true
157    }
158
159    fn is_background(&self) -> bool {
160        false
161    }
162
163    fn view_id(&self) -> Option<ViewId> {
164        None
165    }
166}
167
168impl_downcast!(View);
169
170impl Debug for Box<dyn View> {
171    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
172        write!(f, "Box<dyn View>")
173    }
174}
175
176// We start delivering events from the highest z-level to prevent views from capturing
177// gestures that occurred in higher views.
178// The consistency must also be ensured by the views: popups, for example, need to
179// capture any tap gesture with a touch point inside their rectangle.
180// A child can send events to the main channel through the *hub* or communicate with its parent through the *bus*.
181// A view that wants to render can write to the rendering queue.
182#[cfg_attr(feature = "otel", tracing::instrument(skip(view, hub, parent_bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
183pub fn handle_event(
184    view: &mut dyn View,
185    evt: &Event,
186    hub: &Hub,
187    parent_bus: &mut Bus,
188    rq: &mut RenderQueue,
189    context: &mut Context,
190) -> bool {
191    if view.len() > 0 {
192        let mut captured = false;
193
194        if view.might_skip(evt) {
195            return captured;
196        }
197
198        let mut child_bus: Bus = VecDeque::with_capacity(1);
199
200        for i in (0..view.len()).rev() {
201            if handle_event(view.child_mut(i), evt, hub, &mut child_bus, rq, context) {
202                captured = true;
203                break;
204            }
205        }
206
207        let mut temp_bus: Bus = VecDeque::with_capacity(1);
208
209        child_bus
210            .retain(|child_evt| !view.handle_event(child_evt, hub, &mut temp_bus, rq, context));
211
212        parent_bus.append(&mut child_bus);
213        parent_bus.append(&mut temp_bus);
214
215        captured || view.handle_event(evt, hub, parent_bus, rq, context)
216    } else {
217        view.handle_event(evt, hub, parent_bus, rq, context)
218    }
219}
220
221// We render from bottom to top. For a view to render it has to either appear in `ids` or intersect
222// one of the rectangles in `bgs`. When we're about to render a view, if `wait` is true, we'll wait
223// for all the updates in `updating` that intersect with the view.
224#[cfg_attr(feature = "otel", tracing::instrument(skip(view, ids, rects, bgs, fb, fonts, updating), fields(wait = wait)))]
225pub fn render(
226    view: &dyn View,
227    wait: bool,
228    ids: &FxHashMap<Id, Vec<Rectangle>>,
229    rects: &mut Vec<Rectangle>,
230    bgs: &mut Vec<Rectangle>,
231    fb: &mut dyn Framebuffer,
232    fonts: &mut Fonts,
233    updating: &mut Vec<UpdateData>,
234) {
235    let mut render_rects = Vec::new();
236
237    if view.len() == 0 || view.is_background() {
238        for rect in ids
239            .get(&view.id())
240            .cloned()
241            .into_iter()
242            .flatten()
243            .chain(rects.iter().filter_map(|r| r.intersection(view.rect())))
244            .chain(bgs.iter().filter_map(|r| r.intersection(view.rect())))
245        {
246            let render_rect = view.render_rect(&rect);
247
248            if wait {
249                updating.retain(|update| {
250                    let overlaps = render_rect.overlaps(&update.rect);
251                    if overlaps && !update.has_completed() {
252                        fb.wait(update.token)
253                            .map_err(|e| {
254                                error!("Can't wait for {}, {}: {:#}", update.token, update.rect, e)
255                            })
256                            .ok();
257                    }
258                    !overlaps
259                });
260            }
261
262            view.render(fb, rect, fonts);
263            render_rects.push(render_rect);
264
265            // Most views can't render a subrectangle of themselves.
266            if *view.rect() == render_rect {
267                break;
268            }
269        }
270    } else {
271        bgs.extend(ids.get(&view.id()).cloned().into_iter().flatten());
272    }
273
274    // Merge the contiguous zones to avoid having to schedule lots of small frambuffer updates.
275    for rect in render_rects.into_iter() {
276        if rects.is_empty() {
277            rects.push(rect);
278        } else {
279            if let Some(last) = rects.last_mut() {
280                if rect.extends(last) {
281                    last.absorb(&rect);
282                    let mut i = rects.len();
283                    while i > 1 && rects[i - 1].extends(&rects[i - 2]) {
284                        if let Some(rect) = rects.pop() {
285                            if let Some(last) = rects.last_mut() {
286                                last.absorb(&rect);
287                            }
288                        }
289                        i -= 1;
290                    }
291                } else {
292                    let mut i = rects.len();
293                    while i > 0 && !rects[i - 1].contains(&rect) {
294                        i -= 1;
295                    }
296                    if i == 0 {
297                        rects.push(rect);
298                    }
299                }
300            }
301        }
302    }
303
304    for i in 0..view.len() {
305        render(view.child(i), wait, ids, rects, bgs, fb, fonts, updating);
306    }
307}
308
309#[inline]
310pub fn process_render_queue(
311    view: &dyn View,
312    rq: &mut RenderQueue,
313    context: &mut Context,
314    updating: &mut Vec<UpdateData>,
315) {
316    for ((mode, wait), pairs) in rq.drain() {
317        let mut ids = FxHashMap::default();
318        let mut rects = Vec::new();
319        let mut bgs = Vec::new();
320
321        for (id, rect) in pairs.into_iter().rev() {
322            if let Some(id) = id {
323                ids.entry(id).or_insert_with(Vec::new).push(rect);
324            } else {
325                bgs.push(rect);
326            }
327        }
328
329        render(
330            view,
331            wait,
332            &ids,
333            &mut rects,
334            &mut bgs,
335            context.fb.as_mut(),
336            &mut context.fonts,
337            updating,
338        );
339
340        for rect in rects {
341            match context.fb.update(&rect, mode) {
342                Ok(token) => {
343                    updating.push(UpdateData {
344                        token,
345                        rect,
346                        time: Instant::now(),
347                    });
348                }
349                Err(err) => {
350                    error!("Can't update {}: {:#}.", rect, err);
351                }
352            }
353        }
354    }
355}
356
357#[inline]
358pub fn wait_for_all(updating: &mut Vec<UpdateData>, context: &mut Context) {
359    for update in updating.drain(..) {
360        if update.has_completed() {
361            continue;
362        }
363        context
364            .fb
365            .wait(update.token)
366            .map_err(|e| error!("Can't wait for {}, {}: {:#}", update.token, update.rect, e))
367            .ok();
368    }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub enum ToggleEvent {
373    View(ViewId),
374    Setting(settings_editor::ToggleSettings),
375}
376
377#[derive(Debug, Clone)]
378pub enum Event {
379    Device(DeviceEvent),
380    Gesture(GestureEvent),
381    Keyboard(KeyboardEvent),
382    Key(KeyKind),
383    Open(Box<Info>),
384    OpenHtml(String, Option<String>),
385    LoadPixmap(usize),
386    Update(UpdateMode),
387    RefreshBookPreview(PathBuf),
388    Invalid(PathBuf),
389    Notification(NotificationEvent),
390    Page(CycleDir),
391    ResultsPage(CycleDir),
392    GoTo(usize),
393    GoToLocation(Location),
394    ResultsGoTo(usize),
395    CropMargins(Box<Margin>),
396    Chapter(CycleDir),
397    SelectDirectory(PathBuf),
398    ToggleSelectDirectory(PathBuf),
399    NavigationBarResized(i32),
400    /// Manages input focus state for focusable views like [`InputField`](input_field::InputField).
401    ///
402    /// This event controls which view currently receives keyboard input.
403    /// It is **not** a navigation event — use [`Event::Show`] to transition
404    /// between screens or display new UI components.
405    ///
406    /// # Variants
407    ///
408    /// - `Focus(Some(view_id))` — Grants focus to the view matching `view_id`.
409    ///   The focused view will receive [`Event::Keyboard`] events. Parent views
410    ///   typically use this to show the on-screen keyboard.
411    /// - `Focus(None)` — Clears focus from all views. Parent views typically
412    ///   use this to hide the on-screen keyboard.
413    ///
414    /// # Dispatch behavior
415    ///
416    /// Focus events are **broadcast**: [`InputField`](input_field::InputField)
417    /// returns `false` after handling this event so that all input fields in
418    /// the hierarchy can update their focused/unfocused state.
419    ///
420    /// # Sending
421    ///
422    /// Typically sent through the hub (`hub.send(...)`) by:
423    /// - An [`InputField`](input_field::InputField) when tapped while unfocused
424    /// - A parent view after building a screen that contains an input field
425    /// - A keyboard's hide method to clear focus
426    ///
427    /// # Example
428    ///
429    /// ```no_run
430    /// use cadmus_core::view::{Event, ViewId};
431    /// use cadmus_core::view::ota::OtaViewId;
432    ///
433    /// // Focus the PR input field (e.g. after building the PR input screen).
434    /// // Note: `hub` is provided by the application's event loop.
435    /// # let (hub, _) = std::sync::mpsc::channel();
436    /// hub.send(Event::Focus(Some(ViewId::Ota(OtaViewId::PrInput)))).ok();
437    ///
438    /// // Clear focus from all views.
439    /// hub.send(Event::Focus(None)).ok();
440    /// ```
441    Focus(Option<ViewId>),
442    Select(EntryId),
443    PropagateSelect(EntryId),
444    EditLanguages,
445    Define(String),
446    Submit(ViewId, String),
447    Slider(SliderId, f32, FingerStatus),
448    ToggleNear(ViewId, Rectangle),
449    ToggleInputHistoryMenu(ViewId, Rectangle),
450    ToggleBookMenu(Rectangle, usize),
451    TogglePresetMenu(Rectangle, usize),
452    SubMenu(Rectangle, Vec<EntryKind>),
453    OpenSettingsCategory(settings_editor::Category),
454    SelectSettingsCategory(settings_editor::Category),
455    UpdateSettings(Box<settings::Settings>),
456    EditLibrary(usize),
457    UpdateLibrary(usize, Box<settings::LibrarySettings>),
458    AddLibrary,
459    DeleteLibrary(usize),
460    ProcessLine(LineOrigin, String),
461    History(CycleDir, bool),
462    Toggle(ToggleEvent),
463    Show(ViewId),
464    Close(ViewId),
465    CloseSub(ViewId),
466    Search(String),
467    SearchResult(usize, Vec<Boundary>),
468    FetcherAddDocument(u32, Box<Info>),
469    FetcherRemoveDocument(u32, PathBuf),
470    FetcherSearch {
471        id: u32,
472        path: Option<PathBuf>,
473        query: Option<String>,
474        sort_by: Option<(SortMethod, bool)>,
475    },
476    CheckFetcher(u32),
477    EndOfSearch,
478    Finished,
479    ClockTick,
480    BatteryTick,
481    ToggleFrontlight,
482    Load(PathBuf),
483    LoadPreset(usize),
484    Scroll(i32),
485    Save,
486    Guess,
487    CheckBattery,
488    SetWifi(bool),
489    MightSuspend,
490    PrepareSuspend,
491    Suspend,
492    Share,
493    PrepareShare,
494    Validate,
495    Cancel,
496    Reseed,
497    Back,
498    Quit,
499    WakeUp,
500    Hold(EntryId),
501    /// The file chooser was closed.
502    ///  The `Option<PathBuf>` contains the selected path, if any.
503    FileChooserClosed(Option<PathBuf>),
504    /// GitHub authentication and API interaction events.
505    Github(GithubEvent),
506    /// Settings-specific events
507    Settings(settings_editor::SettingsEvent),
508    /// Request to open a [`NamedInput`](named_input::NamedInput) text overlay.
509    ///
510    /// The handler is responsible for creating the overlay and placing it at the
511    /// correct position in the view hierarchy so it sits at the top of the z-order.
512    OpenNamedInput {
513        /// The `ViewId` used for both the overlay and the resulting `Submit` event.
514        view_id: ViewId,
515        /// Label text displayed inside the input dialog.
516        label: String,
517        /// Maximum number of characters the input field accepts.
518        max_chars: usize,
519        /// Current value pre-populated into the input field.
520        initial_text: String,
521    },
522    /// Progress update from a background OTA download thread.
523    ///
524    /// `OtaView` handles this by updating the status label text and the
525    /// progress bar fill to reflect the current download state.
526    OtaDownloadProgress {
527        label: String,
528        percent: u8,
529    },
530    /// Signal to start downloading the stable release after version check.
531    ///
532    /// This event is sent from the version check thread when the remote version
533    /// is newer than the current version, triggering the actual download to begin.
534    StartStableReleaseDownload,
535}
536
537#[derive(Debug, Clone, Eq, PartialEq)]
538pub enum AppCmd {
539    Sketch,
540    Calculator,
541    Dictionary { query: String, language: String },
542    SettingsEditor,
543    TouchEvents,
544    RotationValues,
545}
546
547#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
548pub enum ViewId {
549    Home,
550    Reader,
551    SortMenu,
552    MainMenu,
553    TitleMenu,
554    SelectionMenu,
555    AnnotationMenu,
556    BatteryMenu,
557    ClockMenu,
558    SearchTargetMenu,
559    InputHistoryMenu,
560    KeyboardLayoutMenu,
561    Frontlight,
562    Dictionary,
563    FontSizeMenu,
564    TextAlignMenu,
565    FontFamilyMenu,
566    MarginWidthMenu,
567    ContrastExponentMenu,
568    ContrastGrayMenu,
569    LineHeightMenu,
570    DirectoryMenu,
571    BookMenu,
572    LibraryMenu,
573    PageMenu,
574    PresetMenu,
575    MarginCropperMenu,
576    SearchMenu,
577    // TODO(ogkevin): merge all these settings editor view IDs into one
578    SettingsMenu,
579    SettingsValueMenu,
580    SettingsCategoryEditor,
581    LibraryEditor,
582    LibraryRename,
583    LibraryRenameInput,
584    AutoSuspendInput,
585    AutoPowerOffInput,
586    SettingsRetentionInput,
587    IntermissionSuspendInput,
588    IntermissionPowerOffInput,
589    IntermissionShareInput,
590    OtlpEndpointInput,
591    SketchMenu,
592    RenameDocument,
593    RenameDocumentInput,
594    GoToPage,
595    GoToPageInput,
596    GoToResultsPage,
597    GoToResultsPageInput,
598    NamePage,
599    NamePageInput,
600    EditNote,
601    EditNoteInput,
602    EditLanguages,
603    EditLanguagesInput,
604    HomeSearchInput,
605    ReaderSearchInput,
606    DictionarySearchInput,
607    CalculatorInput,
608    SearchBar,
609    AddressBar,
610    AddressBarInput,
611    Keyboard,
612    AboutDialog,
613    ShareDialog,
614    MarginCropper,
615    TopBottomBars,
616    TableOfContents,
617    MessageNotif(Id),
618    SubMenu(u8),
619    Ota(ota::OtaViewId),
620    FileChooser,
621}
622
623#[derive(Debug, Copy, Clone, Eq, PartialEq)]
624pub enum SliderId {
625    FontSize,
626    LightIntensity,
627    LightWarmth,
628    ContrastExponent,
629    ContrastGray,
630}
631
632impl SliderId {
633    pub fn label(self) -> String {
634        match self {
635            SliderId::LightIntensity => "Intensity".to_string(),
636            SliderId::LightWarmth => "Warmth".to_string(),
637            SliderId::FontSize => "Font Size".to_string(),
638            SliderId::ContrastExponent => "Contrast Exponent".to_string(),
639            SliderId::ContrastGray => "Contrast Gray".to_string(),
640        }
641    }
642}
643
644#[derive(Debug, Clone)]
645pub enum Align {
646    Left(i32),
647    Right(i32),
648    Center,
649}
650
651impl Align {
652    #[inline]
653    pub fn offset(&self, width: i32, container_width: i32) -> i32 {
654        match *self {
655            Align::Left(dx) => dx,
656            Align::Right(dx) => container_width - width - dx,
657            Align::Center => (container_width - width) / 2,
658        }
659    }
660}
661
662#[derive(Debug, Copy, Clone)]
663pub enum KeyboardEvent {
664    Append(char),
665    Partial(char),
666    Move { target: TextKind, dir: LinearDir },
667    Delete { target: TextKind, dir: LinearDir },
668    Submit,
669}
670
671#[derive(Debug, Copy, Clone)]
672pub enum TextKind {
673    Char,
674    Word,
675    Extremum,
676}
677
678#[derive(Debug, Clone)]
679pub enum EntryKind {
680    Message(String, Option<String>),
681    Command(String, EntryId),
682    CheckBox(String, EntryId, bool),
683    RadioButton(String, EntryId, bool),
684    SubMenu(String, Vec<EntryKind>),
685    More(Vec<EntryKind>),
686    Separator,
687}
688
689#[derive(Debug, Clone, Eq, PartialEq)]
690pub enum EntryId {
691    About,
692    SystemInfo,
693    OpenDocumentation,
694    LoadLibrary(usize),
695    Load(PathBuf),
696    Flush,
697    Save,
698    Import,
699    CleanUp,
700    Sort(SortMethod),
701    ReverseOrder,
702    EmptyTrash,
703    Rename(PathBuf),
704    Remove(PathBuf),
705    CopyTo(PathBuf, usize),
706    MoveTo(PathBuf, usize),
707    AddDirectory(PathBuf),
708    SelectDirectory(PathBuf),
709    ToggleSelectDirectory(PathBuf),
710    SetStatus(PathBuf, SimpleStatus),
711    SearchAuthor(String),
712    RemovePreset(usize),
713    FirstColumn(FirstColumn),
714    SecondColumn(SecondColumn),
715    ThumbnailPreviews,
716    ApplyCroppings(usize, PageScheme),
717    RemoveCroppings,
718    SetZoomMode(ZoomMode),
719    SetScrollMode(ScrollMode),
720    SetPageName,
721    RemovePageName,
722    HighlightSelection,
723    AnnotateSelection,
724    DefineSelection,
725    SearchForSelection,
726    AdjustSelection,
727    Annotations,
728    Bookmarks,
729    RemoveAnnotation([TextLocation; 2]),
730    EditAnnotationNote([TextLocation; 2]),
731    RemoveAnnotationNote([TextLocation; 2]),
732    GoTo(usize),
733    GoToSelectedPageName,
734    SearchDirection(LinearDir),
735    SetButtonScheme(ButtonScheme),
736    SetFontFamily(String),
737    SetFontSize(i32),
738    SetTextAlign(TextAlign),
739    SetMarginWidth(i32),
740    SetLineHeight(i32),
741    SetContrastExponent(i32),
742    SetContrastGray(i32),
743    SetRotationLock(Option<RotationLock>),
744    SetSearchTarget(Option<String>),
745    SetInputText(ViewId, String),
746    SetKeyboardLayout(String),
747    SetLocale(Option<LanguageIdentifier>),
748    // TODO(ogkevin): Make one entryId for settings editor
749    EditLibraryName,
750    EditLibraryPath,
751    DeleteLibrary(usize),
752    SetFinishedAction(FinishedAction),
753    SetLibraryFinishedAction(usize, FinishedAction),
754    ClearLibraryFinishedAction(usize),
755    SetIntermission(settings::IntermKind, settings::IntermissionDisplay),
756    EditIntermissionImage(settings::IntermKind),
757    ToggleShowHidden,
758    #[deprecated(note = "Use ToggleEvent::Settings instead")]
759    ToggleSleepCover,
760    #[deprecated(note = "Use ToggleEvent::Settings instead")]
761    ToggleAutoShare,
762    EditAutoSuspend,
763    EditAutoPowerOff,
764    EditSettingsRetention,
765    SetLogLevel(tracing::Level),
766    EditOtlpEndpoint,
767    ToggleFuzzy,
768    ToggleInverted,
769    ToggleDithered,
770    ToggleWifi,
771    Rotate(i8),
772    Launch(AppCmd),
773    SetPenSize(i32),
774    SetPenColor(Color),
775    TogglePenDynamism,
776    ReloadDictionaries,
777    New,
778    Refresh,
779    TakeScreenshot,
780    Restart,
781    Reboot,
782    Quit,
783    Suspend,
784    PowerOff,
785    CheckForUpdates,
786    FileEntry(PathBuf),
787    Ota(OtaEntryId),
788}
789
790impl EntryKind {
791    pub fn is_separator(&self) -> bool {
792        matches!(*self, EntryKind::Separator)
793    }
794
795    pub fn text(&self) -> &str {
796        match *self {
797            EntryKind::Message(ref s, ..)
798            | EntryKind::Command(ref s, ..)
799            | EntryKind::CheckBox(ref s, ..)
800            | EntryKind::RadioButton(ref s, ..)
801            | EntryKind::SubMenu(ref s, ..) => s,
802            EntryKind::More(..) => "More",
803            _ => "",
804        }
805    }
806
807    pub fn get(&self) -> Option<bool> {
808        match *self {
809            EntryKind::CheckBox(_, _, v) | EntryKind::RadioButton(_, _, v) => Some(v),
810            _ => None,
811        }
812    }
813
814    pub fn set(&mut self, value: bool) {
815        match *self {
816            EntryKind::CheckBox(_, _, ref mut v) | EntryKind::RadioButton(_, _, ref mut v) => {
817                *v = value
818            }
819            _ => (),
820        }
821    }
822}
823
824pub struct RenderData {
825    pub id: Option<Id>,
826    pub rect: Rectangle,
827    pub mode: UpdateMode,
828    pub wait: bool,
829}
830
831impl RenderData {
832    pub fn new(id: Id, rect: Rectangle, mode: UpdateMode) -> RenderData {
833        RenderData {
834            id: Some(id),
835            rect,
836            mode,
837            wait: true,
838        }
839    }
840
841    pub fn no_wait(id: Id, rect: Rectangle, mode: UpdateMode) -> RenderData {
842        RenderData {
843            id: Some(id),
844            rect,
845            mode,
846            wait: false,
847        }
848    }
849
850    pub fn expose(rect: Rectangle, mode: UpdateMode) -> RenderData {
851        RenderData {
852            id: None,
853            rect,
854            mode,
855            wait: true,
856        }
857    }
858}
859
860pub struct UpdateData {
861    pub token: u32,
862    pub time: Instant,
863    pub rect: Rectangle,
864}
865
866pub const MAX_UPDATE_DELAY: Duration = Duration::from_millis(600);
867
868impl UpdateData {
869    pub fn has_completed(&self) -> bool {
870        self.time.elapsed() >= MAX_UPDATE_DELAY
871    }
872}
873
874type RQ = FxHashMap<(UpdateMode, bool), Vec<(Option<Id>, Rectangle)>>;
875pub struct RenderQueue(RQ);
876
877impl RenderQueue {
878    pub fn new() -> RenderQueue {
879        RenderQueue(FxHashMap::default())
880    }
881
882    pub fn add(&mut self, data: RenderData) {
883        self.entry((data.mode, data.wait))
884            .or_insert_with(|| Vec::new())
885            .push((data.id, data.rect));
886    }
887
888    #[cfg(test)]
889    pub fn is_empty(&self) -> bool {
890        self.0.is_empty()
891    }
892
893    #[cfg(test)]
894    pub fn len(&self) -> usize {
895        self.0.values().map(|v| v.len()).sum()
896    }
897}
898
899impl Default for RenderQueue {
900    fn default() -> Self {
901        Self::new()
902    }
903}
904
905impl Deref for RenderQueue {
906    type Target = RQ;
907
908    fn deref(&self) -> &Self::Target {
909        &self.0
910    }
911}
912
913impl DerefMut for RenderQueue {
914    fn deref_mut(&mut self) -> &mut Self::Target {
915        &mut self.0
916    }
917}
918
919pub static ID_FEEDER: IdFeeder = IdFeeder::new(1);
920pub struct IdFeeder(AtomicU64);
921pub type Id = u64;
922
923impl IdFeeder {
924    pub const fn new(id: Id) -> Self {
925        IdFeeder(AtomicU64::new(id))
926    }
927
928    pub fn next(&self) -> Id {
929        self.0.fetch_add(1, Ordering::Relaxed)
930    }
931}