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,
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    IntermissionSuspendInput,
650    IntermissionPowerOffInput,
651    IntermissionShareInput,
652    OtlpEndpointInput,
653    PyroscopeEndpointInput,
654    RefreshRateByKindEditor,
655    RefreshRateKindPairEditor,
656    RefreshRateRegularInput,
657    RefreshRateInvertedInput,
658    RefreshRateByKindRegularInput,
659    RefreshRateByKindInvertedInput,
660    SketchMenu,
661    RenameDocument,
662    RenameDocumentInput,
663    GoToPage,
664    GoToPageInput,
665    GoToResultsPage,
666    GoToResultsPageInput,
667    NamePage,
668    NamePageInput,
669    EditNote,
670    EditNoteInput,
671    EditLanguages,
672    EditLanguagesInput,
673    HomeSearchInput,
674    ReaderSearchInput,
675    DictionarySearchInput,
676    CalculatorInput,
677    SearchBar,
678    AddressBar,
679    AddressBarInput,
680    Keyboard,
681    AboutDialog,
682    ShareDialog,
683    DictionaryDownloadConfirm,
684    ForceImportConfirm,
685    MarginCropper,
686    TopBottomBars,
687    TableOfContents,
688    MessageNotif(Id),
689    SubMenu(u8),
690    Ota(ota::OtaViewId),
691    FileChooser,
692}
693
694#[derive(Debug, Copy, Clone, Eq, PartialEq)]
695pub enum SliderId {
696    FontSize,
697    LightIntensity,
698    LightWarmth,
699    ContrastExponent,
700    ContrastGray,
701}
702
703impl SliderId {
704    pub fn label(self) -> String {
705        match self {
706            SliderId::LightIntensity => "Intensity".to_string(),
707            SliderId::LightWarmth => "Warmth".to_string(),
708            SliderId::FontSize => "Font Size".to_string(),
709            SliderId::ContrastExponent => "Contrast Exponent".to_string(),
710            SliderId::ContrastGray => "Contrast Gray".to_string(),
711        }
712    }
713}
714
715#[derive(Debug, Clone)]
716pub enum Align {
717    Left(i32),
718    Right(i32),
719    Center,
720}
721
722impl Align {
723    #[inline]
724    pub fn offset(&self, width: i32, container_width: i32) -> i32 {
725        match *self {
726            Align::Left(dx) => dx,
727            Align::Right(dx) => container_width - width - dx,
728            Align::Center => (container_width - width) / 2,
729        }
730    }
731}
732
733#[derive(Debug, Copy, Clone)]
734pub enum KeyboardEvent {
735    Append(char),
736    Partial(char),
737    Move { target: TextKind, dir: LinearDir },
738    Delete { target: TextKind, dir: LinearDir },
739    Submit,
740}
741
742#[derive(Debug, Copy, Clone)]
743pub enum TextKind {
744    Char,
745    Word,
746    Extremum,
747}
748
749#[derive(Debug, Clone)]
750pub enum EntryKind {
751    Message(String, Option<String>),
752    Command(String, EntryId),
753    CheckBox(String, EntryId, bool),
754    RadioButton(String, EntryId, bool),
755    SubMenu(String, Vec<EntryKind>),
756    More(Vec<EntryKind>),
757    Separator,
758}
759
760#[derive(Debug, Clone, Eq, PartialEq)]
761pub enum EntryId {
762    About,
763    SystemInfo,
764    OpenDocumentation,
765    LoadLibrary(usize),
766    Load(PathBuf),
767    Save,
768    CleanUp,
769    Sort(SortMethod),
770    ReverseOrder,
771    EmptyTrash,
772    Rename(PathBuf),
773    Remove(PathBuf),
774    CopyTo(PathBuf, usize),
775    MoveTo(PathBuf, usize),
776    AddDirectory(PathBuf),
777    SelectDirectory(PathBuf),
778    ToggleSelectDirectory(PathBuf),
779    SetStatus(PathBuf, SimpleStatus),
780    SearchAuthor(String),
781    RemovePreset(usize),
782    FirstColumn(FirstColumn),
783    SecondColumn(SecondColumn),
784    ThumbnailPreviews,
785    ApplyCroppings(usize, PageScheme),
786    RemoveCroppings,
787    SetZoomMode(ZoomMode),
788    SetScrollMode(ScrollMode),
789    SetPageName,
790    RemovePageName,
791    HighlightSelection,
792    AnnotateSelection,
793    DefineSelection,
794    SearchForSelection,
795    AdjustSelection,
796    Annotations,
797    Bookmarks,
798    RemoveAnnotation([TextLocation; 2]),
799    EditAnnotationNote([TextLocation; 2]),
800    RemoveAnnotationNote([TextLocation; 2]),
801    GoTo(usize),
802    GoToSelectedPageName,
803    SearchDirection(LinearDir),
804    SetButtonScheme(ButtonScheme),
805    SetFontFamily(String),
806    SetFontSize(i32),
807    SetTextAlign(TextAlign),
808    SetMarginWidth(i32),
809    SetLineHeight(i32),
810    SetContrastExponent(i32),
811    SetContrastGray(i32),
812    SetRotationLock(Option<RotationLock>),
813    SetSearchTarget(Option<String>),
814    SetInputText(ViewId, String),
815    SetKeyboardLayout(String),
816    SetLocale(Option<LanguageIdentifier>),
817    // TODO(ogkevin): Make one entryId for settings editor
818    EditLibraryName,
819    EditLibraryPath,
820    DeleteLibrary(usize),
821    SetFinishedAction(FinishedAction),
822    SetLibraryFinishedAction(usize, FinishedAction),
823    ClearLibraryFinishedAction(usize),
824    SetIntermission(settings::IntermKind, settings::IntermissionDisplay),
825    EditIntermissionImage(settings::IntermKind),
826    ToggleShowHidden,
827    #[deprecated(note = "Use ToggleEvent::Settings instead")]
828    ToggleSleepCover,
829    #[deprecated(note = "Use ToggleEvent::Settings instead")]
830    ToggleAutoShare,
831    EditAutoSuspend,
832    EditAutoPowerOff,
833    EditAutoFrontlightBrightness,
834    EditAutoFrontlightManualCoordinates,
835    EditSettingsRetention,
836    SetLogLevel(tracing::Level),
837    EditOtlpEndpoint,
838    EditPyroscopeEndpoint,
839    ToggleFuzzy,
840    ToggleInverted,
841    ToggleDithered,
842    ToggleWifi,
843    Rotate(i8),
844    Launch(AppCmd),
845    SetPenSize(i32),
846    SetPenColor(Color),
847    TogglePenDynamism,
848    ReloadDictionaries,
849    RequestDictionaryDownload(String),
850    DownloadDictionary(String),
851    DeleteDictionary(String),
852    RequestForceImport,
853    New,
854    Refresh,
855    TakeScreenshot,
856    Restart,
857    Reboot,
858    Quit,
859    Suspend,
860    PowerOff,
861    CheckForUpdates,
862    FileEntry(PathBuf),
863    Ota(OtaEntryId),
864    /// Open the per-kind refresh rate editor for the given file extension.
865    EditRefreshRateByKind(settings::FileExtension),
866    /// Delete the per-kind refresh rate override for the given file extension.
867    DeleteRefreshRateByKind(settings::FileExtension),
868    /// Add a new per-kind refresh rate override (opens extension picker submenu).
869    AddRefreshRateByKind,
870    /// Toggle whether a file extension is indexed during import.
871    ToggleAllowedKind(settings::FileExtension),
872    /// Toggle whether a file extension is rendered with dithering.
873    ToggleDitheredKind(settings::FileExtension),
874    SyncTime,
875}
876
877impl EntryKind {
878    pub fn is_separator(&self) -> bool {
879        matches!(*self, EntryKind::Separator)
880    }
881
882    pub fn text(&self) -> &str {
883        match *self {
884            EntryKind::Message(ref s, ..)
885            | EntryKind::Command(ref s, ..)
886            | EntryKind::CheckBox(ref s, ..)
887            | EntryKind::RadioButton(ref s, ..)
888            | EntryKind::SubMenu(ref s, ..) => s,
889            EntryKind::More(..) => "More",
890            _ => "",
891        }
892    }
893
894    pub fn get(&self) -> Option<bool> {
895        match *self {
896            EntryKind::CheckBox(_, _, v) | EntryKind::RadioButton(_, _, v) => Some(v),
897            _ => None,
898        }
899    }
900
901    pub fn set(&mut self, value: bool) {
902        match *self {
903            EntryKind::CheckBox(_, _, ref mut v) | EntryKind::RadioButton(_, _, ref mut v) => {
904                *v = value
905            }
906            _ => (),
907        }
908    }
909}
910
911pub struct RenderData {
912    pub id: Option<Id>,
913    pub rect: Rectangle,
914    pub mode: UpdateMode,
915    pub wait: bool,
916}
917
918impl RenderData {
919    pub fn new(id: Id, rect: Rectangle, mode: UpdateMode) -> RenderData {
920        RenderData {
921            id: Some(id),
922            rect,
923            mode,
924            wait: true,
925        }
926    }
927
928    pub fn no_wait(id: Id, rect: Rectangle, mode: UpdateMode) -> RenderData {
929        RenderData {
930            id: Some(id),
931            rect,
932            mode,
933            wait: false,
934        }
935    }
936
937    pub fn expose(rect: Rectangle, mode: UpdateMode) -> RenderData {
938        RenderData {
939            id: None,
940            rect,
941            mode,
942            wait: true,
943        }
944    }
945}
946
947pub struct UpdateData {
948    pub token: u32,
949    pub time: Instant,
950    pub rect: Rectangle,
951}
952
953pub const MAX_UPDATE_DELAY: Duration = Duration::from_millis(600);
954
955impl UpdateData {
956    pub fn has_completed(&self) -> bool {
957        self.time.elapsed() >= MAX_UPDATE_DELAY
958    }
959}
960
961type RQ = FxHashMap<(UpdateMode, bool), Vec<(Option<Id>, Rectangle)>>;
962pub struct RenderQueue(RQ);
963
964impl RenderQueue {
965    pub fn new() -> RenderQueue {
966        RenderQueue(FxHashMap::default())
967    }
968
969    pub fn add(&mut self, data: RenderData) {
970        self.entry((data.mode, data.wait))
971            .or_insert_with(|| Vec::new())
972            .push((data.id, data.rect));
973    }
974
975    #[cfg(test)]
976    pub fn is_empty(&self) -> bool {
977        self.0.is_empty()
978    }
979
980    #[cfg(test)]
981    pub fn len(&self) -> usize {
982        self.0.values().map(|v| v.len()).sum()
983    }
984}
985
986impl Default for RenderQueue {
987    fn default() -> Self {
988        Self::new()
989    }
990}
991
992impl Deref for RenderQueue {
993    type Target = RQ;
994
995    fn deref(&self) -> &Self::Target {
996        &self.0
997    }
998}
999
1000impl DerefMut for RenderQueue {
1001    fn deref_mut(&mut self) -> &mut Self::Target {
1002        &mut self.0
1003    }
1004}
1005
1006pub static ID_FEEDER: IdFeeder = IdFeeder::new(1);
1007pub struct IdFeeder(AtomicU64);
1008pub type Id = u64;
1009
1010impl IdFeeder {
1011    pub const fn new(id: Id) -> Self {
1012        IdFeeder(AtomicU64::new(id))
1013    }
1014
1015    pub fn next(&self) -> Id {
1016        self.0.fetch_add(1, Ordering::Relaxed)
1017    }
1018}