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