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