Skip to main content

qframe/widgets/tabs/
mod.rs

1//! Tab strips.
2//!
3//! [`Tabs`] and its options live here; `strip` lays the tabs out, `paint` draws them and the
4//! shared behaviour of every tab view (opening, closing, reordering) is in
5//! [`tab_model`](super::tab_model).
6
7mod add;
8#[cfg(test)]
9mod add_tests;
10#[cfg(test)]
11mod badge_tests;
12mod paint;
13mod strip;
14#[cfg(test)]
15mod tests;
16
17use std::time::Duration;
18
19use crate::event::{Event, MouseButton, MouseKind};
20use crate::geometry::{Rect, Size, clamp_u16};
21use crate::keymap::{Key, Modifiers};
22use crate::theme::State;
23use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
24
25use super::close_mark;
26use super::context_item::ContextItem;
27use super::context_menu;
28use super::edge_scroll::{Edge, Zone};
29use super::popup_menu::{PopupAction, PopupMenu};
30use super::tab_model::{self, Direction, TabModel, drop_target, preview_order};
31use strip::Strip;
32
33/// How wide each tab of a [`Tabs`] strip is.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum TabWidth {
36    /// As wide as its label. The default.
37    #[default]
38    Fit,
39    /// Exactly this many cells; longer labels end in `…`.
40    Fixed(u16),
41    /// The tabs share the strip's width equally; when there are too many to fit, they keep a
42    /// readable minimum and the strip overflows.
43    Fill,
44}
45
46/// What a [`Tabs`] strip does when its tabs do not fit.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum Overflow {
49    /// Arrow controls at both ends scroll the strip one tab at a time, without opening anything.
50    /// They are small buttons: hovered they brighten and raise the pillar, pressed they flash one
51    /// tone brighter, and at the end of the strip they fade and ignore presses. ctrl+PgUp,
52    /// ctrl+PgDn and the wheel scroll the same way, and opening a tab brings it into view. The
53    /// default.
54    #[default]
55    Arrows,
56    /// A control at the end shows how many tabs are hidden and opens a menu of them; ↓ opens it
57    /// from the keyboard.
58    Menu,
59}
60
61/// Width of the gap between tabs.
62const GAP: u16 = 1;
63/// Cells before a label.
64const PAD: u16 = 2;
65/// Extra cells a close mark takes: a gap after the label and the three cells of the mark, which
66/// end where the right padding begins.
67const CLOSE: u16 = 1 + close_mark::WIDTH - 1;
68/// The narrowest tab `TabWidth::Fill` shrinks to before the strip overflows.
69const FILL_MIN: u16 = 12;
70/// Width of a scroll arrow: a space, the chevron and a space.
71const ARROW: u16 = 3;
72/// Width of the add button: a space, the `+` and a space, like an arrow.
73const ADD: u16 = 3;
74
75/// A row of tabs. The open tab is a raised surface; tabs are never boxed or bracketed.
76///
77/// With no options it is the plainest strip: keys while focused are ←/→ (or h/l) to open the
78/// neighbouring tab, and 1–9 open a tab by number when numbers are shown. When the tabs do not
79/// fit, arrow controls appear at both ends: a click scrolls one tab, and ctrl+PgUp, ctrl+PgDn
80/// and the wheel do the same. Opening a tab brings it into view.
81///
82/// Capabilities are independent options:
83/// - [`closable`](Self::closable): a faint `×` on every tab that brightens on hover; clicking
84///   it, a middle click on the tab or ctrl+w closes the tab. [`pinned`](Self::pinned) tabs
85///   cannot be closed.
86/// - [`tab_width`](Self::tab_width): fit, fixed or filling tabs.
87/// - [`overflow`](Self::overflow): scroll arrows or a menu of hidden tabs.
88/// - [`reorderable`](Self::reorderable): drag a tab to move it; a ghost follows the pointer and
89///   the tabs make room where it will land. ctrl+shift+←/→ moves the open tab. Held on a scroll
90///   arrow or past an end of an overflowing strip, the dragged tab scrolls the strip: one tab after
91///   400 ms, then one every 150 ms (sooner the further past the end) until the pointer leaves or the
92///   strip ends; the arrow lights up meanwhile and the drop slot follows the tabs coming into view.
93///   [`on_drag_scroll`](Self::on_drag_scroll) reports each step.
94/// - [`context_menu`](Self::context_menu): a right click on a tab opens a menu of actions for
95///   it at the pointer; the menu key or shift+F10 opens the menu of the open tab. Without it a
96///   right click does nothing.
97/// - [`badge`](Self::badge): a count right after a tab's name, such as the updates waiting in an
98///   Updates tab. A narrow tab shortens its name and keeps the count.
99/// - [`on_add`](Self::on_add): a `+` button right after the last tab, or at the strip's right end
100///   while tabs hide, that asks for a new tab. Tab from the tabs reaches it; Enter and Space press
101///   it; resting on it, or reaching it with the keyboard, shows what it does. A tab dragged onto it
102///   moves to the end.
103///
104/// Style keys: `tab` with `hover`, `selected`, `focus`; `tab-index` for numbers; `tab-badge` with
105/// the same states for counts; `close-mark`
106/// (with `active` on a raised tab, `hover` under the pointer); `tab-arrow` (`bg`, `fg`, `pillar`)
107/// with `hover`, `pressed`, `disabled`; `tab-menu` (`bg`, `fg`, `pillar`) with `hover`, `active`
108/// while its menu is open; `tab-ghost` and `tab-drop` (`bg`) while dragging; `popup-menu`,
109/// `popup-item`, `popup-check` for the menu of hidden tabs; `tab-add` (`bg`, `fg`, `pillar`) with
110/// `hover`, `focus`, `pressed` and `tooltip` for its hint; the keys of
111/// [`ContextItem`](super::ContextItem) for the context menu.
112pub struct Tabs<Msg> {
113    labels: Vec<String>,
114    badges: Vec<u32>,
115    numbered: bool,
116    width: TabWidth,
117    overflow: Overflow,
118    on_add: Option<Box<dyn Fn() -> Msg>>,
119    model: TabModel<Msg>,
120}
121
122#[derive(Debug, Default)]
123struct TabsMemory {
124    offset: usize,
125    followed: Option<usize>,
126    hidden: Vec<usize>,
127    /// The arrow last pressed (by click or key) and when, for its flash.
128    pressed: Option<(Arrow, Duration)>,
129    /// Whether keyboard focus inside the strip is on the add button rather than the tabs.
130    on_add: bool,
131    /// When the add button was last pressed, for its flash.
132    add_pressed: Option<Duration>,
133    /// Since when the pointer rests on the add button, and since when its hint shows.
134    add_hovered_since: Option<Duration>,
135    add_hint_since: Option<Duration>,
136    /// The add button while its hint shows, for the overlay.
137    add_hint: Option<Rect>,
138}
139
140/// One of the two scroll arrows, named by the end of the strip it scrolls towards.
141use Edge as Arrow;
142
143impl<Msg: 'static> Tabs<Msg> {
144    /// Tabs with `labels`; the first is open.
145    #[must_use]
146    pub fn new(labels: impl IntoIterator<Item = impl Into<String>>) -> Self {
147        let labels: Vec<String> = labels.into_iter().map(Into::into).collect();
148        let model = TabModel::new(labels.len());
149        let badges = vec![0; labels.len()];
150        Self { labels, badges, numbered: false, width: TabWidth::Fit, overflow: Overflow::Arrows, on_add: None, model }
151    }
152
153    /// The open tab.
154    #[must_use]
155    pub fn active(mut self, index: usize) -> Self {
156        self.model.active = index;
157        self
158    }
159
160    /// Shows 1, 2, 3… before the labels and lets number keys open tabs.
161    #[must_use]
162    pub fn numbered(mut self, numbered: bool) -> Self {
163        self.numbered = numbered;
164        self
165    }
166
167    /// Shows `count` right after the name of tab `index`, one space after it, in a quieter tone
168    /// than the name, such as the updates waiting in an Updates tab; counts above 99 read `99+`.
169    /// A count of zero shows nothing and takes no room, so the count can be passed as it is. The
170    /// count stays in place while the name slides, and a tab too narrow for both shortens its
171    /// name and keeps the count. An `index` past the last tab is ignored.
172    #[must_use]
173    pub fn badge(mut self, index: usize, count: u32) -> Self {
174        if let Some(badge) = self.badges.get_mut(index) {
175            *badge = count;
176        }
177        self
178    }
179
180    /// Message for opening tab `index`.
181    #[must_use]
182    pub fn on_select(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
183        self.model.on_select = Some(Box::new(message));
184        self
185    }
186
187    /// Makes tabs closable: `message(index)` asks the application to close tab `index`.
188    /// [`TabEdit::Close`](super::TabEdit) applies it to the application's list.
189    #[must_use]
190    pub fn closable(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
191        self.model.set_on_close(message);
192        self
193    }
194
195    /// Tabs that cannot be closed, such as a pinned start page. They show no close mark.
196    #[must_use]
197    pub fn pinned(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
198        self.model.pinned = indices.into_iter().collect();
199        self
200    }
201
202    /// How wide the tabs are; [`TabWidth::Fit`] by default.
203    #[must_use]
204    pub fn tab_width(mut self, width: TabWidth) -> Self {
205        self.width = width;
206        self
207    }
208
209    /// What happens when the tabs do not fit; [`Overflow::Arrows`] by default.
210    #[must_use]
211    pub fn overflow(mut self, overflow: Overflow) -> Self {
212        self.overflow = overflow;
213        self
214    }
215
216    /// Makes tabs reorderable: `message(from, to)` asks the application to move a tab.
217    /// [`TabEdit::Move`](super::TabEdit) applies it to the application's list.
218    #[must_use]
219    pub fn reorderable(mut self, message: impl Fn(usize, usize) -> Msg + 'static) -> Self {
220        self.model.set_on_move(message);
221        self
222    }
223
224    /// Message for each step a dragged tab scrolls an overflowing strip, with the position of the
225    /// first tab now in view (counted from 0), e.g. to log it. Only reorderable strips scroll this
226    /// way.
227    #[must_use]
228    pub fn on_drag_scroll(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
229        self.model.on_drag_scroll = Some(Box::new(message));
230        self
231    }
232
233    /// Adds a `+` button that sends `message` when pressed, e.g. to open a new tab. It stands one
234    /// gap after the last tab; while tabs hide it keeps its place at the strip's right end, after
235    /// the arrows or the menu control, and with no tabs it starts the strip. Tab moves keyboard
236    /// focus from the tabs to it; Enter and Space press it.
237    #[must_use]
238    pub fn on_add(mut self, message: impl Fn() -> Msg + 'static) -> Self {
239        self.on_add = Some(Box::new(message));
240        self
241    }
242
243    /// Gives every tab a context menu: `items(index)` builds the entries for tab `index`, such as
244    /// Close, Close others or Pin. A right click on a tab opens its menu at the pointer; the menu
245    /// key or shift+F10 opens the menu of the open tab below it, scrolling the tab into view first.
246    /// Choosing an entry sends its message.
247    #[must_use]
248    pub fn context_menu(mut self, items: impl Fn(usize) -> Vec<ContextItem<Msg>> + 'static) -> Self {
249        self.model.set_context_menu(items);
250        self
251    }
252}
253
254impl<Msg: 'static> Widget<Msg> for Tabs<Msg> {
255    fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
256        let width = match self.width {
257            TabWidth::Fill => available.width,
258            TabWidth::Fit | TabWidth::Fixed(_) => {
259                Self::total_width(&self.widths(available.width)).saturating_add(self.add_room())
260            }
261        };
262        Size::new(width, 1).min(available)
263    }
264
265    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
266        cx.register_hit(area);
267        let on_add = self.add_focus_paint(cx);
268        if self.labels.is_empty() {
269            let strip = self.strip(area, &[], 0, &[]);
270            let pointer = cx.pointer();
271            self.paint_add(cx, area, &strip, pointer, on_add);
272            return;
273        }
274        let focused = cx.is_focus_visible() && !on_add;
275        let pointer = cx.pointer();
276        let widths = self.widths(area.width);
277        let active = self.model.active();
278        let drag = self.model.drag(cx);
279        let identity = self.identity();
280
281        // Scroll to the open tab: always with a menu, only when it changed with arrows, so the
282        // arrows can look around without being pulled back.
283        let offset = {
284            let memory = cx.memory::<TabsMemory>();
285            let (offset, followed) = (memory.offset, memory.followed);
286            let follows = drag.is_none() && (self.overflow == Overflow::Menu || followed != Some(active));
287            let offset = if follows {
288                self.follow(area, &identity, offset, active, &widths)
289            } else {
290                offset.min(self.labels.len() - 1)
291            };
292            let offset = if drag.is_none() { self.settle(area, &identity, offset, &widths) } else { offset };
293            let memory = cx.memory::<TabsMemory>();
294            memory.offset = offset;
295            memory.followed = Some(active);
296            offset
297        };
298
299        let resting = self.strip(area, &identity, offset, &widths);
300        let slots = self.drop_slots(&resting);
301        let target = drag.map(|d| (d.index, drop_target(&slots, d.index, d.pointer, Direction::Across)));
302        let order = preview_order(self.labels.len(), target);
303        let strip = if target.is_some() { self.strip(area, &order, offset, &widths) } else { resting };
304
305        // An open context menu takes the pointer; only the tab it acts on stays raised.
306        let menu_tab = self.model.menu_tab(cx);
307        if menu_tab.is_some() {
308            cx.request_overlay(area);
309        }
310        let pointer = pointer.filter(|_| menu_tab.is_none());
311
312        for (position, (index, rect)) in strip.tabs.iter().enumerate() {
313            if drag.is_some_and(|d| d.index == *index) {
314                tab_model::paint_drop_slot(cx, *rect);
315                continue;
316            }
317            let mut states = Vec::new();
318            if menu_tab == Some(*index) || (drag.is_none() && pointer.is_some_and(|(x, y)| rect.contains(x, y))) {
319                states.push(State::Hover);
320            }
321            if *index == active {
322                states.push(State::Selected);
323                if focused {
324                    states.push(State::Focus);
325                }
326            }
327            self.paint_tab(cx, *rect, *index, offset + position, &states);
328        }
329
330        // The arrows and the menu control take no hover while a tab is dragged, except the arrow
331        // of the end the dragged tab is held against, which lights up as it scrolls the strip.
332        let control_pointer = drag.is_none().then_some(pointer).flatten();
333        let held = drag.and_then(|drag| self.drag_zone(area, &strip, drag.pointer.0)).map(|zone| zone.edge);
334        match self.overflow {
335            Overflow::Arrows => self.paint_arrows(cx, &strip, offset, control_pointer, held),
336            Overflow::Menu => self.paint_menu_control(cx, &strip, control_pointer),
337        }
338        self.paint_add(cx, area, &strip, control_pointer, on_add);
339
340        if let Some(drag) = drag {
341            self.paint_ghost(cx, area, &strip, drag, widths[drag.index], &order);
342        }
343    }
344
345    fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
346        if self.model.paint_menu(cx, anchor) {
347            return;
348        }
349        if !PopupMenu::is_open_paint(cx) {
350            self.paint_add_hint(cx);
351            return;
352        }
353        let hidden = cx.memory::<TabsMemory>().hidden.clone();
354        let labels = self.menu_labels(&hidden);
355        // The strip keeps the open tab in view whenever it has room for a tab at all; a strip with
356        // room only for the control lists every tab and checks the open one.
357        let current = hidden.iter().position(|i| *i == self.model.active());
358        PopupMenu::paint(cx, anchor, &labels, current);
359    }
360
361    fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
362        let area = cx.area();
363        if self.labels.is_empty() {
364            return self.add_event(cx, event, &self.strip(area, &[], 0, &[]));
365        }
366        let widths = self.widths(area.width);
367        let identity = self.identity();
368        let mut offset = cx.memory::<TabsMemory>().offset;
369        // The menu key opens the open tab's menu below that tab, so it first scrolls the tab back
370        // into view when the arrows moved it away.
371        if let Event::Key(key) = event
372            && self.model.has_menu()
373            && context_menu::is_menu_key(key)
374            && !context_menu::is_open_in(cx)
375        {
376            offset = self.follow(area, &identity, offset, self.model.active(), &widths);
377            cx.memory::<TabsMemory>().offset = offset;
378        }
379        let strip = self.strip(area, &identity, offset, &widths);
380        if self.overflow == Overflow::Menu && PopupMenu::is_open(cx) {
381            let hidden = cx.memory::<TabsMemory>().hidden.clone();
382            let labels = self.menu_labels(&hidden);
383            match PopupMenu::event(cx, event, &labels) {
384                PopupAction::Chosen(row) => {
385                    self.model.open(cx, hidden[row]);
386                    return true;
387                }
388                // A press elsewhere on the strip closes the menu and still does what it pressed,
389                // so one click on a tab both closes the menu and opens the tab. A press on the
390                // control that opened the menu only closes it.
391                PopupAction::Closed if Self::presses_beside(event, strip.menu) => {}
392                PopupAction::Used | PopupAction::Closed => return true,
393                PopupAction::Ignored => {}
394            }
395        }
396        let hit = match event {
397            Event::Mouse(mouse) => self.hit(&strip, mouse.x, mouse.y),
398            _ => None,
399        };
400        let open_tab = strip.tabs.iter().find(|(index, _)| *index == self.model.active()).map(|(_, rect)| *rect);
401        if self.model.menu_event(cx, event, hit, open_tab) {
402            return true;
403        }
404        if self.add_event(cx, event, &strip) {
405            return true;
406        }
407        match event {
408            Event::Key(key) => {
409                if self.model.key(cx, key, Direction::Across) {
410                    return true;
411                }
412                if self.numbered
413                    && let Key::Char(c @ '1'..='9') = key.chord.key
414                    && key.chord.mods == Modifiers::default()
415                {
416                    let index = usize::try_from(u32::from(c) - u32::from('1')).unwrap_or(usize::MAX);
417                    return self.model.open(cx, index);
418                }
419                let ctrl = Modifiers { ctrl: true, ..Modifiers::default() };
420                // Scrolling keys only mean something while tabs are hidden; a strip that fits
421                // leaves ctrl+PgUp and ctrl+PgDn to the application.
422                if self.scrolls(&strip) && key.chord.mods == ctrl {
423                    let arrow = match key.chord.key {
424                        Key::PageUp => Arrow::Back,
425                        Key::PageDown => Arrow::Forward,
426                        _ => return false,
427                    };
428                    self.scroll(cx, &strip, arrow, true);
429                    return true;
430                }
431                if strip.menu.is_some() && key.is_plain(Key::Down) {
432                    self.open_menu(cx, &strip);
433                    return true;
434                }
435                false
436            }
437            Event::Mouse(mouse) => {
438                let left_down = mouse.kind == MouseKind::Down(MouseButton::Left);
439                // Presses on the arrows and the menu control are theirs; drags and releases pass on,
440                // so a tab dragged over an arrow still lands.
441                let down = matches!(mouse.kind, MouseKind::Down(_));
442                if down && let Some(arrow) = strip.arrow_at(mouse.x, mouse.y) {
443                    if left_down {
444                        self.scroll(cx, &strip, arrow, true);
445                    }
446                    return true;
447                }
448                if self.scrolls(&strip) && matches!(mouse.kind, MouseKind::ScrollUp | MouseKind::ScrollDown) {
449                    let arrow = if mouse.kind == MouseKind::ScrollUp { Arrow::Back } else { Arrow::Forward };
450                    self.scroll(cx, &strip, arrow, false);
451                    return true;
452                }
453                if down && strip.menu.is_some_and(|menu| menu.contains(mouse.x, mouse.y)) {
454                    if left_down {
455                        self.open_menu(cx, &strip);
456                    }
457                    return true;
458                }
459                let used = self.model.pointer(cx, mouse, hit, &self.drop_slots(&strip), Direction::Across);
460                if mouse.kind == MouseKind::Drag(MouseButton::Left) {
461                    let zone = self.drag_zone(area, &strip, mouse.x);
462                    self.model.edge_scroll(cx, zone, |cx, arrow| {
463                        self.scroll(cx, &strip, arrow, true).then(|| cx.memory::<TabsMemory>().offset)
464                    });
465                }
466                used
467            }
468            _ => false,
469        }
470    }
471
472    fn focusable(&self) -> bool {
473        !self.labels.is_empty() || self.on_add.is_some()
474    }
475}
476
477impl<Msg: 'static> Tabs<Msg> {
478    /// Whether `event` is a press somewhere other than on the menu control `menu`.
479    fn presses_beside(event: &Event, menu: Option<Rect>) -> bool {
480        matches!(event, Event::Mouse(mouse)
481            if matches!(mouse.kind, MouseKind::Down(_)) && !menu.is_some_and(|rect| rect.contains(mouse.x, mouse.y)))
482    }
483
484    /// The entries of the menu of hidden tabs `hidden`: each name, followed by its count when it
485    /// has one, so a count is not lost when its tab hides.
486    fn menu_labels(&self, hidden: &[usize]) -> Vec<String> {
487        hidden
488            .iter()
489            .map(|i| match self.badge_text(*i) {
490                Some(count) => format!("{}  {count}", self.labels[*i]),
491                None => self.labels[*i].clone(),
492            })
493            .collect()
494    }
495
496    /// Whether `strip` scrolls: it hides tabs and has no menu for them. A strip too narrow for
497    /// its arrows still scrolls with the wheel and the keys.
498    fn scrolls(&self, strip: &Strip) -> bool {
499        self.overflow == Overflow::Arrows && strip.tabs.len() < self.labels.len()
500    }
501
502    /// Opens the menu of hidden tabs, starting on the open tab when the strip had no room to show
503    /// it.
504    fn open_menu(&self, cx: &mut EventCx<'_, Msg>, strip: &Strip) {
505        let hidden = Self::hidden(self.labels.len(), strip);
506        PopupMenu::open(cx, hidden.iter().position(|i| *i == self.model.active()).unwrap_or(0));
507    }
508
509    /// The tabs `strip` does not show, in index order.
510    fn hidden(count: usize, strip: &Strip) -> Vec<usize> {
511        (0..count).filter(|i| !strip.tabs.iter().any(|(visible, _)| visible == i)).collect()
512    }
513
514    /// Whether `arrow` can scroll the strip `strip`, which starts at position `offset`.
515    fn can_scroll(&self, strip: &Strip, offset: usize, arrow: Arrow) -> bool {
516        match arrow {
517            Arrow::Back => offset > 0,
518            Arrow::Forward => offset + strip.tabs.len() < self.labels.len(),
519        }
520    }
521
522    /// The end of the strip a tab dragged to column `x` is held against, if the strip scrolls: a
523    /// scroll arrow, or past the strip's edge when it has no room for arrows. Only the column counts,
524    /// so a pointer that strays off the strip's line while dragging still scrolls.
525    fn drag_zone(&self, area: Rect, strip: &Strip, x: i32) -> Option<Zone> {
526        if !self.scrolls(strip) || strip.add.is_some_and(|add| x >= add.x) {
527            return None;
528        }
529        let (back_end, forward_start) =
530            strip.arrows.map_or((area.x, area.right()), |(back, forward)| (back.right(), forward.x));
531        if x < back_end {
532            Some(Zone { edge: Edge::Back, beyond: clamp_u16(area.x - x) })
533        } else if x >= forward_start {
534            Some(Zone { edge: Edge::Forward, beyond: clamp_u16(x + 1 - area.right()) })
535        } else {
536            None
537        }
538    }
539
540    /// Scrolls the strip one tab with `arrow`; `flash` lights the arrow as pressed, for clicks,
541    /// keys and a dragged tab. An arrow at the end of the strip does nothing. True when it scrolled.
542    fn scroll(&self, cx: &mut EventCx<'_, Msg>, strip: &Strip, arrow: Arrow, flash: bool) -> bool {
543        let now = cx.now();
544        let memory = cx.memory::<TabsMemory>();
545        if !self.can_scroll(strip, memory.offset, arrow) {
546            return false;
547        }
548        memory.offset = match arrow {
549            Arrow::Back => memory.offset - 1,
550            Arrow::Forward => memory.offset + 1,
551        };
552        if flash {
553            memory.pressed = Some((arrow, now));
554        }
555        true
556    }
557}