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