Skip to main content

kas_core/event/
event.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Event handling: `Event` type and dependencies
7
8use cast::CastApprox;
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12use super::{EventCx, IsUsed, TimerHandle, Unused, Used};
13#[allow(unused)] use super::{EventState, GrabMode};
14use super::{Key, KeyEvent, NamedKey, PhysicalKey, Press, PressStart};
15use crate::geom::{Affine, Offset, Vec2};
16#[allow(unused)] use crate::{Events, window::Popup};
17use crate::{Id, dir::Direction, window::WindowId};
18
19/// Input Method Editor events
20///
21/// This enum describes [input method](https://en.wikipedia.org/wiki/Input_method) events.
22///
23/// This `enum` closely follows the specification of [`winit::event::Ime`],
24/// hence its documentation may prove useful.
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum Ime<'a> {
27    /// Notifies when the IME was enabled.
28    ///
29    /// This may mean, for example, that a virtual keyboard is now active with
30    /// focus on the recipient. This event does not mean that physical keyboard
31    /// input is disabled (or that it should be ignored).
32    ///
33    /// The widget should call [`EventState::set_ime_cursor_area`] immediately
34    /// and each time the area changes (relative to the widget's coordinate
35    /// space), until [`Ime::Disabled`] is received. Failure to do so will
36    /// result in the widget's entire `rect` being used as the IME cursor area.
37    Enabled,
38
39    /// Notifies when the IME was disabled.
40    ///
41    /// Any pending (uncommitted) pre-edit should be cleared (cancelled).
42    Disabled,
43
44    /// Notifies when a new composing text should be set at the cursor position.
45    ///
46    /// When `text.is_empty()` the pre-edit was cleared. Synthetic events with
47    /// empty text and `cursor: None` are generated immediately before an
48    /// [`Ime::Commit`] event.
49    ///
50    /// `cursor` represents the selection-start and cursor indices within
51    /// `text`. If `cursor.is_none()` then no cursor should be shown.
52    Preedit {
53        text: &'a str,
54        cursor: Option<(usize, usize)>,
55    },
56
57    /// Notifies when text should be inserted into the editor widget.
58    ///
59    /// Right before this event winit will send empty [`Self::Preedit`] event.
60    Commit { text: &'a str },
61
62    /// Delete text surrounding the cursor or selection.
63    ///
64    /// This event does not affect the pre-edit string.
65    DeleteSurrounding {
66        /// Bytes to remove before the selection
67        ///
68        /// Assuming the selection starts at `s` (or the cursor is at `s` with
69        /// an empty selection), bytes in the range `s - before_bytes .. s`
70        /// should be deleted.
71        before_bytes: usize,
72        /// Bytes to remove after the selection
73        ///
74        /// Assuming the selection ends at `c`, bytes in the range
75        /// `c .. c + after_bytes` should be deleted.
76        after_bytes: usize,
77    },
78}
79
80/// Events addressed to a widget
81///
82/// Note that a few events are received by disabled widgets; see
83/// [`Event::pass_when_disabled`].
84#[non_exhaustive]
85#[derive(Clone, Debug, PartialEq)]
86pub enum Event<'a> {
87    /// Command input
88    ///
89    /// A generic "command". The source is often but not always a key press.
90    /// In many cases (but not all) the target widget has navigation focus.
91    ///
92    /// A [`PhysicalKey`] is attached when the command is caused by a key press.
93    /// The recipient may use this to call [`EventCx::depress_with_key`].
94    ///
95    /// If a widget has keyboard input focus (see
96    /// [`EventCx::request_key_focus`]) it will instead receive
97    /// [`Event::Key`] for key presses (but may still receive `Event::Command`
98    /// from other sources).
99    Command(Command, Option<PhysicalKey>),
100    /// Keyboard input: `event, is_synthetic`
101    ///
102    /// This is only received by a widget with
103    /// [keyboard focus](EventCx::request_key_focus).
104    ///
105    /// On some platforms, synthetic key events are generated when a window
106    /// gains or loses focus with a key held (see documentation of
107    /// [`winit::event::WindowEvent::KeyboardInput`]). This is indicated by the
108    /// second parameter, `is_synthetic`. Unless you need to track key states
109    /// it is advised only to match `Event::Key(event, false)`.
110    ///
111    /// Some key presses can be mapped to a [`Command`]. To do this (normally
112    /// only when `event.state == ElementState::Pressed && !is_synthetic`), use
113    /// `cx.config().shortcuts(|s| s.try_match(cx.modifiers(), &event.logical_key)`
114    /// or (omitting shortcut matching) `Command::new(event.logical_key)`.
115    /// Note that if the handler returns [`Unused`] the widget might
116    /// then receive [`Event::Command`] for the same key press, but this is not
117    /// guaranteed (behaviour may change in future versions).
118    ///
119    /// For standard text input, simply consume `event.text` when
120    /// `event.state == ElementState::Pressed && !is_synthetic`.
121    /// NOTE: unlike Winit, we force `text = None` for control chars and when
122    /// <kbd>Ctrl</kbd>, <kbd>Alt</kbd> or <kbd>Super</kbd> modifier keys are
123    /// pressed. This is subject to change.
124    Key(&'a KeyEvent, bool),
125    /// An Input Method Editor event
126    ///
127    /// IME events are only received with
128    /// [IME focus](EventCx::replace_ime_focus).
129    ///
130    /// On [`Ime::Enabled`],
131    /// the widget should call [`EventState::set_ime_cursor_area`] immediately
132    /// and each time the area changes (relative to the widget's coordinate
133    /// space), until [`Ime::Disabled`] is received. Failure to do so will
134    /// result in the widget's entire `rect` being used as the IME cursor area.
135    Ime(Ime<'a>),
136    /// A mouse or touchpad scroll event
137    Scroll(ScrollDelta),
138    /// A mouse or touch-screen move/zoom/rotate event
139    ///
140    /// This event is sent for certain types of grab ([`PressStart::grab`]),
141    /// enabling two-finger scale/rotate gestures as well as translation.
142    ///
143    /// Mouse-grabs generate translation (`delta` component) only. Touch grabs
144    /// optionally also generate rotation and scaling components, depending on
145    /// the [`GrabMode`].
146    Pan(Affine),
147    /// Movement of mouse pointer without press
148    ///
149    /// This event is only sent one case: when the mouse is moved while a
150    /// [`Popup`] is open and there is not an active [`PressStart::grab`] on the
151    /// mouse pointer.
152    ///
153    /// This event may be sent 10+ times per frame, thus it is important that
154    /// the handler be fast. It may be useful to schedule a pre-draw update
155    /// with [`EventState::request_frame_timer`] to handle any post-move
156    /// updates.
157    PointerMove { press: Press },
158    /// A mouse button was pressed or touch event started
159    ///
160    /// Call [`PressStart::grab`] in order to "grab" corresponding motion
161    /// and release events.
162    ///
163    /// This event is sent to the widget under the mouse or touch position. If
164    /// no such widget is found, this event is not sent.
165    PressStart(PressStart),
166    /// Movement of mouse or a touch press
167    ///
168    /// This event is only sent when a ([`PressStart::grab`]) is active.
169    /// Motion events for the grabbed mouse pointer or touched finger are sent.
170    ///
171    /// If `cur_id` is `None`, no widget was found at the coordinate (either
172    /// outside the window or [`crate::Tile::try_probe`] failed).
173    ///
174    /// This event may be sent 10+ times per frame, thus it is important that
175    /// the handler be fast. It may be useful to schedule a pre-draw update
176    /// with [`EventState::request_frame_timer`] to handle any post-move
177    /// updates.
178    PressMove { press: Press, delta: Vec2 },
179    /// End of a click/touch press
180    ///
181    /// If `success`, this is a button-release or touch finish; otherwise this
182    /// is a cancelled/interrupted grab. "Activation events" (e.g. clicking of a
183    /// button or menu item) should only happen on `success`. "Movement events"
184    /// such as panning, moving a slider or opening a menu should not be undone
185    /// when cancelling: the panned item or slider should be released as is, or
186    /// the menu should remain open.
187    ///
188    /// This event is only sent when a ([`PressStart::grab`]) is active.
189    /// Release/cancel events for the same mouse button or touched finger are
190    /// sent.
191    ///
192    /// If `cur_id` is `None`, no widget was found at the coordinate (either
193    /// outside the window or [`crate::Tile::try_probe`] failed).
194    PressEnd { press: Press, success: bool },
195    /// Update from a timer
196    ///
197    /// This event must be requested by [`EventState::request_timer`].
198    Timer(TimerHandle),
199    /// Notification that a popup has been closed
200    ///
201    /// This is sent to the popup when closed.
202    /// Since popups may be removed directly by the [`EventCx`], the parent should
203    /// clean up any associated state here.
204    #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
205    #[cfg_attr(docsrs, doc(cfg(internal_doc)))]
206    PopupClosed(WindowId),
207    /// Notification that a widget has gained navigation focus
208    ///
209    /// Navigation focus implies that the widget is highlighted and will be the
210    /// primary target of [`Event::Command`], and is thus able to receive basic
211    /// keyboard input (e.g. arrow keys). To receive full keyboard input
212    /// ([`Event::Key`]), call [`EventCx::request_key_focus`].
213    ///
214    /// With [`FocusSource::Pointer`] the widget should already have received
215    /// [`Event::PressStart`].
216    ///
217    /// With [`FocusSource::Key`], [`EventCx::set_scroll`] is
218    /// called automatically (to ensure that the widget is visible) and the
219    /// response will be forced to [`Used`].
220    NavFocus(FocusSource),
221    /// Notification that a widget has lost navigation focus
222    LostNavFocus,
223    /// Notification that a widget has gained selection focus
224    ///
225    /// This focus must be requested by calling
226    /// [`EventCx::request_sel_focus`] or [`EventCx::request_key_focus`].
227    SelFocus(FocusSource),
228    /// Notification that a widget has lost selection focus
229    ///
230    /// In the case the widget also had character focus, [`Event::LostKeyFocus`] is
231    /// received first.
232    LostSelFocus,
233    /// Notification that a widget has gained keyboard input focus
234    ///
235    /// This focus must be requested by calling
236    /// [`EventCx::request_key_focus`].
237    ///
238    /// This is always preceeded by [`Event::SelFocus`] and is received prior to
239    /// [`Event::Key`] events.
240    KeyFocus,
241    /// Notification that a widget has lost keyboard input focus
242    LostKeyFocus,
243    /// Notification that the mouse moves over or leaves a widget
244    ///
245    /// The state is `true` on mouse over, `false` when the mouse leaves.
246    MouseOver(bool),
247}
248
249impl<'a> std::ops::Add<Offset> for Event<'a> {
250    type Output = Self;
251
252    #[inline]
253    fn add(mut self, offset: Offset) -> Self {
254        self += offset;
255        self
256    }
257}
258
259impl<'a> std::ops::AddAssign<Offset> for Event<'a> {
260    fn add_assign(&mut self, offset: Offset) {
261        match self {
262            Event::PointerMove { press } => {
263                press.coord += offset;
264            }
265            Event::PressStart(press) => {
266                *press += offset;
267            }
268            Event::PressMove { press, .. } => {
269                press.coord += offset;
270            }
271            Event::PressEnd { press, .. } => {
272                press.coord += offset;
273            }
274            _ => (),
275        }
276    }
277}
278
279impl<'a> Event<'a> {
280    /// Call `f` on "click" or any "activation" event
281    ///
282    /// This is a convenience method which calls `f` on any of the following:
283    ///
284    /// -   Mouse click and release on the same widget
285    /// -   Touchscreen press and release on the same widget
286    /// -   `Event::Command(cmd, _)` where [`cmd.is_activate()`](Command::is_activate)
287    ///
288    /// This method has some side effects:
289    ///
290    /// -   [`Event::PressStart`] is grabbed using [`GrabMode::Click`]
291    /// -   [`Event::Command`] with a key will cause `id` to be depressed until
292    ///     that key is released (see [`EventCx::depress_with_key`]).
293    pub fn on_click<F: FnOnce(&mut EventCx)>(self, cx: &mut EventCx, id: Id, f: F) -> IsUsed {
294        match self {
295            Event::Command(cmd, code) if cmd.is_activate() => {
296                cx.depress_with_key(id, code);
297                f(cx);
298                Used
299            }
300            Event::PressStart(press) if press.is_primary() => press.grab_click(id).complete(cx),
301            Event::PressEnd { press, success, .. } => {
302                if success && id == press.id {
303                    f(cx);
304                }
305                Used
306            }
307            _ => Unused,
308        }
309    }
310
311    /// Pass to disabled widgets?
312    ///
313    /// When a widget is disabled:
314    ///
315    /// -   New input events (`Command`, `PressStart`, `Scroll`) are not passed
316    /// -   Continuing input actions (`PressMove`, `PressEnd`) are passed (or
317    ///     the input sequence may be terminated).
318    /// -   New focus notifications are not passed
319    /// -   Focus-loss notifications are passed
320    /// -   Requested events like `Timer` are passed
321    pub fn pass_when_disabled(&self) -> bool {
322        use Event::*;
323        match self {
324            Command(_, _) => false,
325            Key(_, _) | Scroll(_) => false,
326            PointerMove { .. } | PressStart(_) => false,
327            Pan { .. } | PressMove { .. } | PressEnd { .. } => true,
328            Timer(_) | PopupClosed(_) => true,
329            NavFocus { .. } | SelFocus(_) | KeyFocus | MouseOver(true) => false,
330            LostNavFocus | LostKeyFocus | LostSelFocus | MouseOver(false) => true,
331            Ime(super::Ime::Disabled) => true,
332            Ime(_) => false,
333        }
334    }
335
336    /// Can the event be received by [`Events::handle_event`] during unwinding?
337    ///
338    /// Some events are sent to the widget with navigation focus (e.g.
339    /// [`Event::Command`]). Others are sent to the widget under the mouse (e.g.
340    /// [`Event::PressStart`]). All these events may be "reused" by an ancestor
341    /// widget if not [`Used`] by the original target.
342    ///
343    /// Other events are sent to a specific widget as a result of a request
344    /// (e.g. [`Event::Key`], [`Event::PressEnd`]), or as a notification of
345    /// focus change (e.g. [`Event::LostKeyFocus`]). These events may never be
346    /// "reused".
347    ///
348    /// Note: this could alternatively be seen as a property of the addressing
349    /// mechanism, currently just an [`Id`].
350    pub fn is_reusable(&self) -> bool {
351        use Event::*;
352        match self {
353            // Events sent to navigation focus given some code,
354            // otherwise sent to a specific target.
355            Command(_, code) => code.is_some(),
356
357            // Events sent to mouse focus
358            Scroll(_) | Pan { .. } => true,
359            PointerMove { .. } | PressStart(_) => true,
360
361            // Events sent to requester
362            Key(_, _) | Ime(_) => false,
363            PressMove { .. } | PressEnd { .. } => false,
364            Timer(_) => false,
365
366            // Notifications of focus/status change
367            PopupClosed(_) => false,
368            NavFocus { .. } | LostNavFocus => false,
369            SelFocus(_) | LostSelFocus => false,
370            KeyFocus | LostKeyFocus => false,
371            MouseOver(_) => false,
372        }
373    }
374}
375
376/// Command input ([`Event::Command`])
377///
378/// `Command` events are mostly produced as a result of OS-specific keyboard
379/// bindings; for example,  [`Command::Copy`] is produced by pressing
380/// <kbd>Command+C</kbd> on MacOS or <kbd>Ctrl+C</kbd> on other platforms.
381/// See [`crate::config::Shortcuts`] for more on these bindings.
382///
383/// A `Command` event does not necessarily come from keyboard input; for example
384/// some menu widgets send [`Command::Activate`] to trigger an entry as a result
385/// of mouse input.
386///
387/// *Most* `Command` entries represent an action (such as `Copy` or `FindNext`)
388/// but some represent an important key whose action may be context-dependent
389/// (e.g. `Escape`, `Space`).
390#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
391#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
392#[non_exhaustive]
393pub enum Command {
394    /// Escape key
395    ///
396    /// Each press of this key should somehow relax control. It is expected that
397    /// widgets receiving this key repeatedly eventually (soon) have no more
398    /// use for this themselves and return it via [`Unused`].
399    ///
400    /// This is in some cases remapped to [`Command::Deselect`].
401    Escape,
402    /// Programmatic activation
403    ///
404    /// A synthetic event to activate widgets. Consider matching
405    /// [`Command::is_activate`] or using using [`Event::on_click`]
406    /// instead for generally applicable activation.
407    Activate,
408    /// Return / enter key
409    ///
410    /// This may insert a line-break or may activate something.
411    Enter,
412    /// Space bar key
413    Space,
414    /// Tab key
415    ///
416    /// This key is used to insert (horizontal) tabulators as well as to
417    /// navigate focus (in reverse when combined with Shift).
418    ///
419    /// This is usually not sent to widgets but instead used for navigation.
420    Tab,
421
422    /// Move view up without affecting selection
423    ViewUp,
424    /// Move view down without affecting selection
425    ViewDown,
426
427    /// Move left
428    Left,
429    /// Move right
430    Right,
431    /// Move up
432    Up,
433    /// Move down
434    Down,
435    /// Move left one word
436    WordLeft,
437    /// Move right one word
438    WordRight,
439    /// Move to start (of the line)
440    Home,
441    /// Move to end (of the line)
442    End,
443    /// Move to start of the document
444    DocHome,
445    /// Move to end of the document
446    DocEnd,
447    /// Move up a page
448    PageUp,
449    /// Move down a page
450    PageDown,
451
452    /// Capture a screenshot
453    Snapshot,
454    /// Lock output of screen
455    ScrollLock,
456    /// Pause key
457    Pause,
458    /// Insert key
459    Insert,
460
461    /// Delete forwards
462    Delete,
463    /// Delete backwards (Backspace key)
464    DelBack,
465    /// Delete forwards one word
466    DelWord,
467    /// Delete backwards one word
468    DelWordBack,
469
470    /// Clear any selections
471    Deselect,
472    /// Select all contents
473    SelectAll,
474
475    /// Find (start)
476    Find,
477    /// Find and replace (start)
478    FindReplace,
479    /// Find next
480    FindNext,
481    /// Find previous
482    FindPrevious,
483
484    /// Make text bold
485    Bold,
486    /// Make text italic
487    Italic,
488    /// Underline text
489    Underline,
490    /// Insert a link
491    Link,
492
493    /// Copy to clipboard and clear
494    Cut,
495    /// Copy to clipboard
496    Copy,
497    /// Copy from clipboard
498    Paste,
499    /// Undo the last action
500    Undo,
501    /// Redo the last undone action
502    Redo,
503
504    /// New document
505    New,
506    /// Open document
507    Open,
508    /// Save document
509    Save,
510    /// Print document
511    Print,
512
513    /// Navigate forwards one page/item
514    NavNext,
515    /// Navigate backwards one page/item
516    NavPrevious,
517    /// Navigate to the parent item
518    ///
519    /// May be used to browse "up" to a parent directory.
520    NavParent,
521    /// Navigate "down"
522    ///
523    /// This is an opposite to `NavParent`, and will mostly not be used.
524    NavDown,
525
526    /// Open a new tab
527    TabNew,
528    /// Navigate to next tab
529    TabNext,
530    /// Navigate to previous tab
531    TabPrevious,
532
533    /// Show help
534    Help,
535    /// Rename
536    Rename,
537    /// Refresh
538    Refresh,
539    /// Debug
540    Debug,
541    /// Spell-check tool
542    SpellCheck,
543    /// Open the context menu
544    ContextMenu,
545    /// Open or activate the application menu / menubar
546    Menu,
547    /// Make view fullscreen
548    Fullscreen,
549
550    /// Close window/tab/popup
551    Close,
552    /// Exit program (e.g. Ctrl+Q)
553    Exit,
554}
555
556impl Command {
557    /// Try constructing from a [`winit::keyboard::Key`]
558    pub fn new(key: &Key) -> Option<Self> {
559        match key {
560            Key::Named(named) => Some(match named {
561                NamedKey::ScrollLock => Command::ScrollLock,
562                NamedKey::Enter => Command::Enter,
563                NamedKey::Tab => Command::Tab,
564                NamedKey::ArrowDown => Command::Down,
565                NamedKey::ArrowLeft => Command::Left,
566                NamedKey::ArrowRight => Command::Right,
567                NamedKey::ArrowUp => Command::Up,
568                NamedKey::End => Command::End,
569                NamedKey::Home => Command::Home,
570                NamedKey::PageDown => Command::PageDown,
571                NamedKey::PageUp => Command::PageUp,
572                NamedKey::Backspace => Command::DelBack,
573                NamedKey::Clear => Command::Deselect,
574                NamedKey::Copy => Command::Copy,
575                NamedKey::Cut => Command::Cut,
576                NamedKey::Delete => Command::Delete,
577                NamedKey::Insert => Command::Insert,
578                NamedKey::Paste => Command::Paste,
579                NamedKey::Redo | NamedKey::Again => Command::Redo,
580                NamedKey::Undo => Command::Undo,
581                NamedKey::ContextMenu => Command::ContextMenu,
582                NamedKey::Escape => Command::Escape,
583                NamedKey::Execute => Command::Activate,
584                NamedKey::Find => Command::Find,
585                NamedKey::Help => Command::Help,
586                NamedKey::Pause => Command::Pause,
587                NamedKey::Select => Command::SelectAll,
588                NamedKey::PrintScreen => Command::Snapshot,
589                // NamedKey::Close => CloseDocument ?
590                NamedKey::New => Command::New,
591                NamedKey::Open => Command::Open,
592                NamedKey::Print => Command::Print,
593                NamedKey::Save => Command::Save,
594                NamedKey::SpellCheck => Command::SpellCheck,
595                NamedKey::BrowserBack | NamedKey::GoBack => Command::NavPrevious,
596                NamedKey::BrowserForward => Command::NavNext,
597                NamedKey::BrowserRefresh => Command::Refresh,
598                NamedKey::Exit => Command::Exit,
599                _ => return None,
600            }),
601            Key::Character(s) if s == " " => Some(Command::Space),
602            _ => None,
603        }
604    }
605
606    /// True for "activation" commands
607    ///
608    /// This matches:
609    ///
610    /// -   [`Self::Activate`] — programmatic activation
611    /// -   [`Self::Enter`] —  <kbd>Enter</kbd> and <kbd>Return</kbd> keys
612    /// -   [`Self::Space`] — <kbd>Space</kbd> key
613    pub fn is_activate(self) -> bool {
614        use Command::*;
615        matches!(self, Activate | Enter | Space)
616    }
617
618    /// Convert to selection-focus command
619    ///
620    /// Certain limited commands may be sent to widgets with selection focus but
621    /// not character or navigation focus.
622    pub fn suitable_for_sel_focus(self) -> bool {
623        use Command::*;
624        matches!(self, Escape | Cut | Copy | Deselect)
625    }
626
627    /// Convert arrow keys to a direction
628    pub fn as_direction(self) -> Option<Direction> {
629        match self {
630            Command::Left => Some(Direction::Left),
631            Command::Right => Some(Direction::Right),
632            Command::Up => Some(Direction::Up),
633            Command::Down => Some(Direction::Down),
634            _ => None,
635        }
636    }
637}
638
639/// Reason that navigation focus is received
640#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
641pub enum FocusSource {
642    /// Focus is received as a result of a mouse or touch event
643    Pointer,
644    /// Focus is received as a result of keyboard navigation (usually
645    /// <kbd>Tab</kbd>) or a command ([`Event::Command`])
646    Key,
647    /// Focus is received from a programmatic event
648    Synthetic,
649}
650
651impl FocusSource {
652    pub fn key_or_synthetic(self) -> bool {
653        match self {
654            FocusSource::Pointer => false,
655            FocusSource::Key | FocusSource::Synthetic => true,
656        }
657    }
658}
659
660/// Type used by [`Event::Scroll`]
661#[derive(Clone, Copy, Debug, PartialEq)]
662pub enum ScrollDelta {
663    /// Scroll a given number of lines
664    ///
665    /// Positive values indicate that the content that is being scrolled should
666    /// move right and down (revealing more content left and up).
667    /// Typically values are integral but this is not guaranteed.
668    Lines(f32, f32),
669    /// Scroll a given number of pixels
670    ///
671    /// For a ‘natural scrolling’ touch pad (that acts like a touch screen) this
672    /// means moving your fingers right and down should give positive values,
673    /// and move the content right and down (to reveal more things left and up).
674    PixelDelta(Vec2),
675}
676
677impl ScrollDelta {
678    /// True if the x-axis delta is zero
679    pub fn is_vertical(self) -> bool {
680        match self {
681            ScrollDelta::Lines(0.0, _) => true,
682            ScrollDelta::PixelDelta(Vec2(0.0, _)) => true,
683            _ => false,
684        }
685    }
686
687    /// True if the y-axis delta is zero
688    pub fn is_horizontal(self) -> bool {
689        match self {
690            ScrollDelta::Lines(_, 0.0) => true,
691            ScrollDelta::PixelDelta(Vec2(_, 0.0)) => true,
692            _ => false,
693        }
694    }
695
696    /// Convert to a pan offset
697    ///
698    /// Line deltas are converted to a distance based on `scroll_distance` configuration.
699    pub fn as_offset(self, cx: &EventState) -> Vec2 {
700        match self {
701            ScrollDelta::Lines(x, y) => cx.config().event().scroll_distance((x, y)),
702            ScrollDelta::PixelDelta(d) => d,
703        }
704    }
705
706    /// Convert to a zoom factor
707    pub fn as_factor(self, _: &EventState) -> f32 {
708        // TODO: this should be configurable?
709        match self {
710            ScrollDelta::Lines(_, y) => -0.5 * y,
711            ScrollDelta::PixelDelta(Vec2(_, y)) => -0.01 * y,
712        }
713    }
714
715    /// Convert to a pan offset or zoom factor
716    ///
717    /// This is used for surfaces where panning/scrolling is preferred over
718    /// zooming, though both are supported (for example, a web page).
719    /// The <kbd>Ctrl</kbd> key is used to select between the two modes.
720    pub fn as_offset_or_factor(self, cx: &EventState) -> Result<Vec2, f32> {
721        if cx.modifiers().control_key() {
722            Err(self.as_factor(cx))
723        } else {
724            Ok(self.as_offset(cx))
725        }
726    }
727
728    /// Convert to a zoom factor or pan offset
729    ///
730    /// This is used for surfaces where zooming is preferred over panning,
731    /// though both are supported (for example, a map view where click-and-drag
732    /// may also be used to pan). Mouse wheel actions always zoom while the
733    /// touchpad scrolling may cause either effect.
734    pub fn as_factor_or_offset(self, cx: &EventState) -> Result<f32, Vec2> {
735        if matches!(self, ScrollDelta::Lines(_, _)) || cx.modifiers().control_key() {
736            Ok(self.as_factor(cx))
737        } else {
738            Err(self.as_offset(cx))
739        }
740    }
741
742    /// Attempt to interpret as a mouse wheel action
743    ///
744    /// Infers the "scroll delta" as an integral step, if appropriate.
745    /// This may be used e.g. to change the selected value of a `ComboBox`.
746    ///
747    /// Positive values indicate scrolling up.
748    pub fn as_wheel_action(self, cx: &EventState) -> Option<i32> {
749        match self {
750            ScrollDelta::Lines(_, y) if cx.config().event().mouse_wheel_actions() => {
751                y.try_cast_approx().ok()
752            }
753            _ => None,
754        }
755    }
756}
757
758#[cfg(test)]
759#[test]
760fn sizes() {
761    use core::mem::size_of;
762    assert_eq!(size_of::<Command>(), 1);
763    assert_eq!(size_of::<PhysicalKey>(), 8);
764    assert_eq!(size_of::<KeyEvent>(), 128);
765    assert_eq!(size_of::<ScrollDelta>(), 12);
766    assert_eq!(size_of::<Affine>(), 32);
767    assert_eq!(size_of::<Press>(), 24);
768    assert_eq!(size_of::<TimerHandle>(), 8);
769    assert_eq!(size_of::<WindowId>(), 4);
770    assert_eq!(size_of::<FocusSource>(), 1);
771    assert_eq!(size_of::<Event>(), 40);
772}