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    ///
554    /// When `force` is `true`, every file is re-fingerprinted regardless of its stored
555    /// `mtime` and `file_size`. When `false`, files whose metadata has not changed are
556    /// skipped (incremental mode).
557    ImportLibrary {
558        library_index: Option<usize>,
559        force: bool,
560    },
561    /// Signals that a background import has finished.
562    ImportFinished {
563        library_index: Option<usize>,
564    },
565    /// Signals that a background thumbnail extraction has finished.
566    ///
567    /// Emitted by [`ThumbnailExtractionTask`](crate::task::thumbnail::ThumbnailExtractionTask)
568    /// when it completes (regardless of whether any thumbnails were extracted).
569    /// The [`TaskManager`](crate::task::TaskManager) intercepts this event to
570    /// drain any queued extractions that accumulated while the task was running.
571    ThumbnailExtractionFinished {
572        library_index: Option<usize>,
573    },
574    /// Requests a background dictionary index scan.
575    ///
576    /// Emitted when the settings editor closes after dictionaries were installed
577    /// or deleted. The [`TaskManager`](crate::task::TaskManager) intercepts this
578    /// event and starts a [`DictionaryIndexTask`](crate::task::dictionary_index::DictionaryIndexTask)
579    /// if one is not already running.
580    ReindexDictionaries,
581    /// Requests that `context.load_dictionaries()` be called to rebuild the
582    /// in-memory dictionary map.
583    ///
584    /// Emitted by [`DictionaryIndexTask`](crate::task::dictionary_index::DictionaryIndexTask)
585    /// after inserting a new `dictionary_index_meta` row (so the dictionary
586    /// becomes resolvable) and after deleting stale entries (so removed
587    /// dictionaries are no longer visible). The app and emulator main loops
588    /// handle this event directly on the context.
589    ReloadDictionaries,
590}
591
592#[derive(Debug, Clone, Eq, PartialEq)]
593pub enum AppCmd {
594    Sketch,
595    Calculator,
596    Dictionary { query: String, language: String },
597    SettingsEditor,
598    TouchEvents,
599    RotationValues,
600}
601
602#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
603pub enum ViewId {
604    Home,
605    Reader,
606    SortMenu,
607    MainMenu,
608    TitleMenu,
609    SelectionMenu,
610    AnnotationMenu,
611    BatteryMenu,
612    ClockMenu,
613    SearchTargetMenu,
614    InputHistoryMenu,
615    KeyboardLayoutMenu,
616    Frontlight,
617    Dictionary,
618    FontSizeMenu,
619    TextAlignMenu,
620    FontFamilyMenu,
621    MarginWidthMenu,
622    ContrastExponentMenu,
623    ContrastGrayMenu,
624    LineHeightMenu,
625    DirectoryMenu,
626    BookMenu,
627    LibraryMenu,
628    PageMenu,
629    PresetMenu,
630    MarginCropperMenu,
631    SearchMenu,
632    // TODO(ogkevin): merge all these settings editor view IDs into one
633    SettingsMenu,
634    SettingsValueMenu,
635    SettingsCategoryEditor,
636    LibraryEditor,
637    LibraryRename,
638    LibraryRenameInput,
639    AutoSuspendInput,
640    AutoPowerOffInput,
641    SettingsRetentionInput,
642    IntermissionSuspendInput,
643    IntermissionPowerOffInput,
644    IntermissionShareInput,
645    OtlpEndpointInput,
646    PyroscopeEndpointInput,
647    RefreshRateByKindEditor,
648    RefreshRateKindPairEditor,
649    RefreshRateRegularInput,
650    RefreshRateInvertedInput,
651    RefreshRateByKindRegularInput,
652    RefreshRateByKindInvertedInput,
653    SketchMenu,
654    RenameDocument,
655    RenameDocumentInput,
656    GoToPage,
657    GoToPageInput,
658    GoToResultsPage,
659    GoToResultsPageInput,
660    NamePage,
661    NamePageInput,
662    EditNote,
663    EditNoteInput,
664    EditLanguages,
665    EditLanguagesInput,
666    HomeSearchInput,
667    ReaderSearchInput,
668    DictionarySearchInput,
669    CalculatorInput,
670    SearchBar,
671    AddressBar,
672    AddressBarInput,
673    Keyboard,
674    AboutDialog,
675    ShareDialog,
676    DictionaryDownloadConfirm,
677    ForceImportConfirm,
678    MarginCropper,
679    TopBottomBars,
680    TableOfContents,
681    MessageNotif(Id),
682    SubMenu(u8),
683    Ota(ota::OtaViewId),
684    FileChooser,
685}
686
687#[derive(Debug, Copy, Clone, Eq, PartialEq)]
688pub enum SliderId {
689    FontSize,
690    LightIntensity,
691    LightWarmth,
692    ContrastExponent,
693    ContrastGray,
694}
695
696impl SliderId {
697    pub fn label(self) -> String {
698        match self {
699            SliderId::LightIntensity => "Intensity".to_string(),
700            SliderId::LightWarmth => "Warmth".to_string(),
701            SliderId::FontSize => "Font Size".to_string(),
702            SliderId::ContrastExponent => "Contrast Exponent".to_string(),
703            SliderId::ContrastGray => "Contrast Gray".to_string(),
704        }
705    }
706}
707
708#[derive(Debug, Clone)]
709pub enum Align {
710    Left(i32),
711    Right(i32),
712    Center,
713}
714
715impl Align {
716    #[inline]
717    pub fn offset(&self, width: i32, container_width: i32) -> i32 {
718        match *self {
719            Align::Left(dx) => dx,
720            Align::Right(dx) => container_width - width - dx,
721            Align::Center => (container_width - width) / 2,
722        }
723    }
724}
725
726#[derive(Debug, Copy, Clone)]
727pub enum KeyboardEvent {
728    Append(char),
729    Partial(char),
730    Move { target: TextKind, dir: LinearDir },
731    Delete { target: TextKind, dir: LinearDir },
732    Submit,
733}
734
735#[derive(Debug, Copy, Clone)]
736pub enum TextKind {
737    Char,
738    Word,
739    Extremum,
740}
741
742#[derive(Debug, Clone)]
743pub enum EntryKind {
744    Message(String, Option<String>),
745    Command(String, EntryId),
746    CheckBox(String, EntryId, bool),
747    RadioButton(String, EntryId, bool),
748    SubMenu(String, Vec<EntryKind>),
749    More(Vec<EntryKind>),
750    Separator,
751}
752
753#[derive(Debug, Clone, Eq, PartialEq)]
754pub enum EntryId {
755    About,
756    SystemInfo,
757    OpenDocumentation,
758    LoadLibrary(usize),
759    Load(PathBuf),
760    Save,
761    CleanUp,
762    Sort(SortMethod),
763    ReverseOrder,
764    EmptyTrash,
765    Rename(PathBuf),
766    Remove(PathBuf),
767    CopyTo(PathBuf, usize),
768    MoveTo(PathBuf, usize),
769    AddDirectory(PathBuf),
770    SelectDirectory(PathBuf),
771    ToggleSelectDirectory(PathBuf),
772    SetStatus(PathBuf, SimpleStatus),
773    SearchAuthor(String),
774    RemovePreset(usize),
775    FirstColumn(FirstColumn),
776    SecondColumn(SecondColumn),
777    ThumbnailPreviews,
778    ApplyCroppings(usize, PageScheme),
779    RemoveCroppings,
780    SetZoomMode(ZoomMode),
781    SetScrollMode(ScrollMode),
782    SetPageName,
783    RemovePageName,
784    HighlightSelection,
785    AnnotateSelection,
786    DefineSelection,
787    SearchForSelection,
788    AdjustSelection,
789    Annotations,
790    Bookmarks,
791    RemoveAnnotation([TextLocation; 2]),
792    EditAnnotationNote([TextLocation; 2]),
793    RemoveAnnotationNote([TextLocation; 2]),
794    GoTo(usize),
795    GoToSelectedPageName,
796    SearchDirection(LinearDir),
797    SetButtonScheme(ButtonScheme),
798    SetFontFamily(String),
799    SetFontSize(i32),
800    SetTextAlign(TextAlign),
801    SetMarginWidth(i32),
802    SetLineHeight(i32),
803    SetContrastExponent(i32),
804    SetContrastGray(i32),
805    SetRotationLock(Option<RotationLock>),
806    SetSearchTarget(Option<String>),
807    SetInputText(ViewId, String),
808    SetKeyboardLayout(String),
809    SetLocale(Option<LanguageIdentifier>),
810    // TODO(ogkevin): Make one entryId for settings editor
811    EditLibraryName,
812    EditLibraryPath,
813    DeleteLibrary(usize),
814    SetFinishedAction(FinishedAction),
815    SetLibraryFinishedAction(usize, FinishedAction),
816    ClearLibraryFinishedAction(usize),
817    SetIntermission(settings::IntermKind, settings::IntermissionDisplay),
818    EditIntermissionImage(settings::IntermKind),
819    ToggleShowHidden,
820    #[deprecated(note = "Use ToggleEvent::Settings instead")]
821    ToggleSleepCover,
822    #[deprecated(note = "Use ToggleEvent::Settings instead")]
823    ToggleAutoShare,
824    EditAutoSuspend,
825    EditAutoPowerOff,
826    EditSettingsRetention,
827    SetLogLevel(tracing::Level),
828    EditOtlpEndpoint,
829    EditPyroscopeEndpoint,
830    ToggleFuzzy,
831    ToggleInverted,
832    ToggleDithered,
833    ToggleWifi,
834    Rotate(i8),
835    Launch(AppCmd),
836    SetPenSize(i32),
837    SetPenColor(Color),
838    TogglePenDynamism,
839    ReloadDictionaries,
840    RequestDictionaryDownload(String),
841    DownloadDictionary(String),
842    DeleteDictionary(String),
843    RequestForceImport,
844    New,
845    Refresh,
846    TakeScreenshot,
847    Restart,
848    Reboot,
849    Quit,
850    Suspend,
851    PowerOff,
852    CheckForUpdates,
853    FileEntry(PathBuf),
854    Ota(OtaEntryId),
855    /// Open the per-kind refresh rate editor for the given file extension.
856    EditRefreshRateByKind(settings::FileExtension),
857    /// Delete the per-kind refresh rate override for the given file extension.
858    DeleteRefreshRateByKind(settings::FileExtension),
859    /// Add a new per-kind refresh rate override (opens extension picker submenu).
860    AddRefreshRateByKind,
861    /// Toggle whether a file extension is indexed during import.
862    ToggleAllowedKind(settings::FileExtension),
863    /// Toggle whether a file extension is rendered with dithering.
864    ToggleDitheredKind(settings::FileExtension),
865}
866
867impl EntryKind {
868    pub fn is_separator(&self) -> bool {
869        matches!(*self, EntryKind::Separator)
870    }
871
872    pub fn text(&self) -> &str {
873        match *self {
874            EntryKind::Message(ref s, ..)
875            | EntryKind::Command(ref s, ..)
876            | EntryKind::CheckBox(ref s, ..)
877            | EntryKind::RadioButton(ref s, ..)
878            | EntryKind::SubMenu(ref s, ..) => s,
879            EntryKind::More(..) => "More",
880            _ => "",
881        }
882    }
883
884    pub fn get(&self) -> Option<bool> {
885        match *self {
886            EntryKind::CheckBox(_, _, v) | EntryKind::RadioButton(_, _, v) => Some(v),
887            _ => None,
888        }
889    }
890
891    pub fn set(&mut self, value: bool) {
892        match *self {
893            EntryKind::CheckBox(_, _, ref mut v) | EntryKind::RadioButton(_, _, ref mut v) => {
894                *v = value
895            }
896            _ => (),
897        }
898    }
899}
900
901pub struct RenderData {
902    pub id: Option<Id>,
903    pub rect: Rectangle,
904    pub mode: UpdateMode,
905    pub wait: bool,
906}
907
908impl RenderData {
909    pub fn new(id: Id, rect: Rectangle, mode: UpdateMode) -> RenderData {
910        RenderData {
911            id: Some(id),
912            rect,
913            mode,
914            wait: true,
915        }
916    }
917
918    pub fn no_wait(id: Id, rect: Rectangle, mode: UpdateMode) -> RenderData {
919        RenderData {
920            id: Some(id),
921            rect,
922            mode,
923            wait: false,
924        }
925    }
926
927    pub fn expose(rect: Rectangle, mode: UpdateMode) -> RenderData {
928        RenderData {
929            id: None,
930            rect,
931            mode,
932            wait: true,
933        }
934    }
935}
936
937pub struct UpdateData {
938    pub token: u32,
939    pub time: Instant,
940    pub rect: Rectangle,
941}
942
943pub const MAX_UPDATE_DELAY: Duration = Duration::from_millis(600);
944
945impl UpdateData {
946    pub fn has_completed(&self) -> bool {
947        self.time.elapsed() >= MAX_UPDATE_DELAY
948    }
949}
950
951type RQ = FxHashMap<(UpdateMode, bool), Vec<(Option<Id>, Rectangle)>>;
952pub struct RenderQueue(RQ);
953
954impl RenderQueue {
955    pub fn new() -> RenderQueue {
956        RenderQueue(FxHashMap::default())
957    }
958
959    pub fn add(&mut self, data: RenderData) {
960        self.entry((data.mode, data.wait))
961            .or_insert_with(|| Vec::new())
962            .push((data.id, data.rect));
963    }
964
965    #[cfg(test)]
966    pub fn is_empty(&self) -> bool {
967        self.0.is_empty()
968    }
969
970    #[cfg(test)]
971    pub fn len(&self) -> usize {
972        self.0.values().map(|v| v.len()).sum()
973    }
974}
975
976impl Default for RenderQueue {
977    fn default() -> Self {
978        Self::new()
979    }
980}
981
982impl Deref for RenderQueue {
983    type Target = RQ;
984
985    fn deref(&self) -> &Self::Target {
986        &self.0
987    }
988}
989
990impl DerefMut for RenderQueue {
991    fn deref_mut(&mut self) -> &mut Self::Target {
992        &mut self.0
993    }
994}
995
996pub static ID_FEEDER: IdFeeder = IdFeeder::new(1);
997pub struct IdFeeder(AtomicU64);
998pub type Id = u64;
999
1000impl IdFeeder {
1001    pub const fn new(id: Id) -> Self {
1002        IdFeeder(AtomicU64::new(id))
1003    }
1004
1005    pub fn next(&self) -> Id {
1006        self.0.fetch_add(1, Ordering::Relaxed)
1007    }
1008}