Skip to main content

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::{Downcast, impl_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 = "tracing", 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 = "tracing", 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    /// Open the refresh rate editor (global + per-kind overrides).
461    OpenRefreshRateEditor,
462    /// Open the per-kind refresh rate editor for the given file extension.
463    EditRefreshRateByKind(settings::FileExtension),
464    /// Commit a new or updated per-kind refresh rate pair to settings.
465    UpdateRefreshRateByKind(settings::FileExtension, Box<settings::RefreshRatePair>),
466    /// Delete the per-kind override for the given extension.
467    DeleteRefreshRateByKind(settings::FileExtension),
468    ProcessLine(LineOrigin, String),
469    History(CycleDir, bool),
470    Toggle(ToggleEvent),
471    Show(ViewId),
472    Close(ViewId),
473    CloseSub(ViewId),
474    Search(String),
475    SearchResult(usize, Vec<Boundary>),
476    FetcherAddDocument(u32, Box<Info>),
477    FetcherRemoveDocument(u32, PathBuf),
478    FetcherSearch {
479        id: u32,
480        path: Option<PathBuf>,
481        query: Option<String>,
482        sort_by: Option<(SortMethod, bool)>,
483    },
484    CheckFetcher(u32),
485    EndOfSearch,
486    Finished,
487    ClockTick,
488    BatteryTick,
489    ToggleFrontlight,
490    Load(PathBuf),
491    LoadPreset(usize),
492    Scroll(i32),
493    Save,
494    Guess,
495    CheckBattery,
496    SetWifi(bool),
497    MightSuspend,
498    PrepareSuspend,
499    Suspend,
500    Share,
501    PrepareShare,
502    Validate,
503    Cancel,
504    Reseed,
505    Back,
506    Quit,
507    WakeUp,
508    Hold(EntryId),
509    /// The file chooser was closed.
510    ///  The `Option<PathBuf>` contains the selected path, if any.
511    FileChooserClosed(Option<PathBuf>),
512    /// GitHub authentication and API interaction events.
513    Github(GithubEvent),
514    /// Settings-specific events
515    Settings(settings_editor::SettingsEvent),
516    /// Request to open a [`NamedInput`](named_input::NamedInput) text overlay.
517    ///
518    /// The handler is responsible for creating the overlay and placing it at the
519    /// correct position in the view hierarchy so it sits at the top of the z-order.
520    OpenNamedInput {
521        /// The `ViewId` used for both the overlay and the resulting `Submit` event.
522        view_id: ViewId,
523        /// Label text displayed inside the input dialog.
524        label: String,
525        /// Maximum number of characters the input field accepts.
526        max_chars: usize,
527        /// Current value pre-populated into the input field.
528        initial_text: String,
529    },
530    /// Progress update from a background OTA download thread.
531    ///
532    /// `OtaView` handles this by updating the status label text and the
533    /// progress bar fill to reflect the current download state.
534    OtaDownloadProgress {
535        label: String,
536        percent: u8,
537    },
538    /// Signal to start downloading the stable release after version check.
539    ///
540    /// This event is sent from the version check thread when the remote version
541    /// is newer than the current version, triggering the actual download to begin.
542    StartStableReleaseDownload,
543    /// Result of a background dictionary install spawned by `CategoryEditor`.
544    ///
545    /// Sent from the download thread when the install completes (success or
546    /// failure). `CategoryEditor` handles this by rebuilding the rows list so
547    /// the newly-installed dictionary appears immediately.
548    DictionaryInstallComplete {
549        lang: String,
550        result: Result<(), String>,
551    },
552    /// Requests a background import for the given library index (or the current library if `None`).
553    ImportLibrary {
554        library_index: Option<usize>,
555    },
556    /// Signals that a background import has finished.
557    ImportFinished {
558        library_index: Option<usize>,
559    },
560    /// Signals that a background thumbnail extraction has finished.
561    ///
562    /// Emitted by [`ThumbnailExtractionTask`](crate::task::thumbnail::ThumbnailExtractionTask)
563    /// when it completes (regardless of whether any thumbnails were extracted).
564    /// The [`TaskManager`](crate::task::TaskManager) intercepts this event to
565    /// drain any queued extractions that accumulated while the task was running.
566    ThumbnailExtractionFinished {
567        library_index: Option<usize>,
568    },
569    /// Requests a background dictionary index scan.
570    ///
571    /// Emitted when the settings editor closes after dictionaries were installed
572    /// or deleted. The [`TaskManager`](crate::task::TaskManager) intercepts this
573    /// event and starts a [`DictionaryIndexTask`](crate::task::dictionary_index::DictionaryIndexTask)
574    /// if one is not already running.
575    ReindexDictionaries,
576    /// Requests that `context.load_dictionaries()` be called to rebuild the
577    /// in-memory dictionary map.
578    ///
579    /// Emitted by [`DictionaryIndexTask`](crate::task::dictionary_index::DictionaryIndexTask)
580    /// after inserting a new `dictionary_index_meta` row (so the dictionary
581    /// becomes resolvable) and after deleting stale entries (so removed
582    /// dictionaries are no longer visible). The app and emulator main loops
583    /// handle this event directly on the context.
584    ReloadDictionaries,
585}
586
587#[derive(Debug, Clone, Eq, PartialEq)]
588pub enum AppCmd {
589    Sketch,
590    Calculator,
591    Dictionary { query: String, language: String },
592    SettingsEditor,
593    TouchEvents,
594    RotationValues,
595}
596
597#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
598pub enum ViewId {
599    Home,
600    Reader,
601    SortMenu,
602    MainMenu,
603    TitleMenu,
604    SelectionMenu,
605    AnnotationMenu,
606    BatteryMenu,
607    ClockMenu,
608    SearchTargetMenu,
609    InputHistoryMenu,
610    KeyboardLayoutMenu,
611    Frontlight,
612    Dictionary,
613    FontSizeMenu,
614    TextAlignMenu,
615    FontFamilyMenu,
616    MarginWidthMenu,
617    ContrastExponentMenu,
618    ContrastGrayMenu,
619    LineHeightMenu,
620    DirectoryMenu,
621    BookMenu,
622    LibraryMenu,
623    PageMenu,
624    PresetMenu,
625    MarginCropperMenu,
626    SearchMenu,
627    // TODO(ogkevin): merge all these settings editor view IDs into one
628    SettingsMenu,
629    SettingsValueMenu,
630    SettingsCategoryEditor,
631    LibraryEditor,
632    LibraryRename,
633    LibraryRenameInput,
634    AutoSuspendInput,
635    AutoPowerOffInput,
636    SettingsRetentionInput,
637    IntermissionSuspendInput,
638    IntermissionPowerOffInput,
639    IntermissionShareInput,
640    OtlpEndpointInput,
641    PyroscopeEndpointInput,
642    RefreshRateByKindEditor,
643    RefreshRateKindPairEditor,
644    RefreshRateRegularInput,
645    RefreshRateInvertedInput,
646    RefreshRateByKindRegularInput,
647    RefreshRateByKindInvertedInput,
648    SketchMenu,
649    RenameDocument,
650    RenameDocumentInput,
651    GoToPage,
652    GoToPageInput,
653    GoToResultsPage,
654    GoToResultsPageInput,
655    NamePage,
656    NamePageInput,
657    EditNote,
658    EditNoteInput,
659    EditLanguages,
660    EditLanguagesInput,
661    HomeSearchInput,
662    ReaderSearchInput,
663    DictionarySearchInput,
664    CalculatorInput,
665    SearchBar,
666    AddressBar,
667    AddressBarInput,
668    Keyboard,
669    AboutDialog,
670    ShareDialog,
671    DictionaryDownloadConfirm,
672    MarginCropper,
673    TopBottomBars,
674    TableOfContents,
675    MessageNotif(Id),
676    SubMenu(u8),
677    Ota(ota::OtaViewId),
678    FileChooser,
679}
680
681#[derive(Debug, Copy, Clone, Eq, PartialEq)]
682pub enum SliderId {
683    FontSize,
684    LightIntensity,
685    LightWarmth,
686    ContrastExponent,
687    ContrastGray,
688}
689
690impl SliderId {
691    pub fn label(self) -> String {
692        match self {
693            SliderId::LightIntensity => "Intensity".to_string(),
694            SliderId::LightWarmth => "Warmth".to_string(),
695            SliderId::FontSize => "Font Size".to_string(),
696            SliderId::ContrastExponent => "Contrast Exponent".to_string(),
697            SliderId::ContrastGray => "Contrast Gray".to_string(),
698        }
699    }
700}
701
702#[derive(Debug, Clone)]
703pub enum Align {
704    Left(i32),
705    Right(i32),
706    Center,
707}
708
709impl Align {
710    #[inline]
711    pub fn offset(&self, width: i32, container_width: i32) -> i32 {
712        match *self {
713            Align::Left(dx) => dx,
714            Align::Right(dx) => container_width - width - dx,
715            Align::Center => (container_width - width) / 2,
716        }
717    }
718}
719
720#[derive(Debug, Copy, Clone)]
721pub enum KeyboardEvent {
722    Append(char),
723    Partial(char),
724    Move { target: TextKind, dir: LinearDir },
725    Delete { target: TextKind, dir: LinearDir },
726    Submit,
727}
728
729#[derive(Debug, Copy, Clone)]
730pub enum TextKind {
731    Char,
732    Word,
733    Extremum,
734}
735
736#[derive(Debug, Clone)]
737pub enum EntryKind {
738    Message(String, Option<String>),
739    Command(String, EntryId),
740    CheckBox(String, EntryId, bool),
741    RadioButton(String, EntryId, bool),
742    SubMenu(String, Vec<EntryKind>),
743    More(Vec<EntryKind>),
744    Separator,
745}
746
747#[derive(Debug, Clone, Eq, PartialEq)]
748pub enum EntryId {
749    About,
750    SystemInfo,
751    OpenDocumentation,
752    LoadLibrary(usize),
753    Load(PathBuf),
754    Flush,
755    Save,
756    Import,
757    CleanUp,
758    Sort(SortMethod),
759    ReverseOrder,
760    EmptyTrash,
761    Rename(PathBuf),
762    Remove(PathBuf),
763    CopyTo(PathBuf, usize),
764    MoveTo(PathBuf, usize),
765    AddDirectory(PathBuf),
766    SelectDirectory(PathBuf),
767    ToggleSelectDirectory(PathBuf),
768    SetStatus(PathBuf, SimpleStatus),
769    SearchAuthor(String),
770    RemovePreset(usize),
771    FirstColumn(FirstColumn),
772    SecondColumn(SecondColumn),
773    ThumbnailPreviews,
774    ApplyCroppings(usize, PageScheme),
775    RemoveCroppings,
776    SetZoomMode(ZoomMode),
777    SetScrollMode(ScrollMode),
778    SetPageName,
779    RemovePageName,
780    HighlightSelection,
781    AnnotateSelection,
782    DefineSelection,
783    SearchForSelection,
784    AdjustSelection,
785    Annotations,
786    Bookmarks,
787    RemoveAnnotation([TextLocation; 2]),
788    EditAnnotationNote([TextLocation; 2]),
789    RemoveAnnotationNote([TextLocation; 2]),
790    GoTo(usize),
791    GoToSelectedPageName,
792    SearchDirection(LinearDir),
793    SetButtonScheme(ButtonScheme),
794    SetFontFamily(String),
795    SetFontSize(i32),
796    SetTextAlign(TextAlign),
797    SetMarginWidth(i32),
798    SetLineHeight(i32),
799    SetContrastExponent(i32),
800    SetContrastGray(i32),
801    SetRotationLock(Option<RotationLock>),
802    SetSearchTarget(Option<String>),
803    SetInputText(ViewId, String),
804    SetKeyboardLayout(String),
805    SetLocale(Option<LanguageIdentifier>),
806    // TODO(ogkevin): Make one entryId for settings editor
807    EditLibraryName,
808    EditLibraryPath,
809    DeleteLibrary(usize),
810    SetFinishedAction(FinishedAction),
811    SetLibraryFinishedAction(usize, FinishedAction),
812    ClearLibraryFinishedAction(usize),
813    SetIntermission(settings::IntermKind, settings::IntermissionDisplay),
814    EditIntermissionImage(settings::IntermKind),
815    ToggleShowHidden,
816    #[deprecated(note = "Use ToggleEvent::Settings instead")]
817    ToggleSleepCover,
818    #[deprecated(note = "Use ToggleEvent::Settings instead")]
819    ToggleAutoShare,
820    EditAutoSuspend,
821    EditAutoPowerOff,
822    EditSettingsRetention,
823    SetLogLevel(tracing::Level),
824    EditOtlpEndpoint,
825    EditPyroscopeEndpoint,
826    ToggleFuzzy,
827    ToggleInverted,
828    ToggleDithered,
829    ToggleWifi,
830    Rotate(i8),
831    Launch(AppCmd),
832    SetPenSize(i32),
833    SetPenColor(Color),
834    TogglePenDynamism,
835    ReloadDictionaries,
836    RequestDictionaryDownload(String),
837    DownloadDictionary(String),
838    DeleteDictionary(String),
839    New,
840    Refresh,
841    TakeScreenshot,
842    Restart,
843    Reboot,
844    Quit,
845    Suspend,
846    PowerOff,
847    CheckForUpdates,
848    FileEntry(PathBuf),
849    Ota(OtaEntryId),
850    /// Open the per-kind refresh rate editor for the given file extension.
851    EditRefreshRateByKind(settings::FileExtension),
852    /// Delete the per-kind refresh rate override for the given file extension.
853    DeleteRefreshRateByKind(settings::FileExtension),
854    /// Add a new per-kind refresh rate override (opens extension picker submenu).
855    AddRefreshRateByKind,
856    /// Toggle whether a file extension is indexed during import.
857    ToggleAllowedKind(settings::FileExtension),
858    /// Toggle whether a file extension is rendered with dithering.
859    ToggleDitheredKind(settings::FileExtension),
860}
861
862impl EntryKind {
863    pub fn is_separator(&self) -> bool {
864        matches!(*self, EntryKind::Separator)
865    }
866
867    pub fn text(&self) -> &str {
868        match *self {
869            EntryKind::Message(ref s, ..)
870            | EntryKind::Command(ref s, ..)
871            | EntryKind::CheckBox(ref s, ..)
872            | EntryKind::RadioButton(ref s, ..)
873            | EntryKind::SubMenu(ref s, ..) => s,
874            EntryKind::More(..) => "More",
875            _ => "",
876        }
877    }
878
879    pub fn get(&self) -> Option<bool> {
880        match *self {
881            EntryKind::CheckBox(_, _, v) | EntryKind::RadioButton(_, _, v) => Some(v),
882            _ => None,
883        }
884    }
885
886    pub fn set(&mut self, value: bool) {
887        match *self {
888            EntryKind::CheckBox(_, _, ref mut v) | EntryKind::RadioButton(_, _, ref mut v) => {
889                *v = value
890            }
891            _ => (),
892        }
893    }
894}
895
896pub struct RenderData {
897    pub id: Option<Id>,
898    pub rect: Rectangle,
899    pub mode: UpdateMode,
900    pub wait: bool,
901}
902
903impl RenderData {
904    pub fn new(id: Id, rect: Rectangle, mode: UpdateMode) -> RenderData {
905        RenderData {
906            id: Some(id),
907            rect,
908            mode,
909            wait: true,
910        }
911    }
912
913    pub fn no_wait(id: Id, rect: Rectangle, mode: UpdateMode) -> RenderData {
914        RenderData {
915            id: Some(id),
916            rect,
917            mode,
918            wait: false,
919        }
920    }
921
922    pub fn expose(rect: Rectangle, mode: UpdateMode) -> RenderData {
923        RenderData {
924            id: None,
925            rect,
926            mode,
927            wait: true,
928        }
929    }
930}
931
932pub struct UpdateData {
933    pub token: u32,
934    pub time: Instant,
935    pub rect: Rectangle,
936}
937
938pub const MAX_UPDATE_DELAY: Duration = Duration::from_millis(600);
939
940impl UpdateData {
941    pub fn has_completed(&self) -> bool {
942        self.time.elapsed() >= MAX_UPDATE_DELAY
943    }
944}
945
946type RQ = FxHashMap<(UpdateMode, bool), Vec<(Option<Id>, Rectangle)>>;
947pub struct RenderQueue(RQ);
948
949impl RenderQueue {
950    pub fn new() -> RenderQueue {
951        RenderQueue(FxHashMap::default())
952    }
953
954    pub fn add(&mut self, data: RenderData) {
955        self.entry((data.mode, data.wait))
956            .or_insert_with(|| Vec::new())
957            .push((data.id, data.rect));
958    }
959
960    #[cfg(test)]
961    pub fn is_empty(&self) -> bool {
962        self.0.is_empty()
963    }
964
965    #[cfg(test)]
966    pub fn len(&self) -> usize {
967        self.0.values().map(|v| v.len()).sum()
968    }
969}
970
971impl Default for RenderQueue {
972    fn default() -> Self {
973        Self::new()
974    }
975}
976
977impl Deref for RenderQueue {
978    type Target = RQ;
979
980    fn deref(&self) -> &Self::Target {
981        &self.0
982    }
983}
984
985impl DerefMut for RenderQueue {
986    fn deref_mut(&mut self) -> &mut Self::Target {
987        &mut self.0
988    }
989}
990
991pub static ID_FEEDER: IdFeeder = IdFeeder::new(1);
992pub struct IdFeeder(AtomicU64);
993pub type Id = u64;
994
995impl IdFeeder {
996    pub const fn new(id: Id) -> Self {
997        IdFeeder(AtomicU64::new(id))
998    }
999
1000    pub fn next(&self) -> Id {
1001        self.0.fetch_add(1, Ordering::Relaxed)
1002    }
1003}