Skip to main content

qframe/widgets/tab_rail/
mod.rs

1//! Vertical tabs for switching between projects or workspaces.
2//!
3//! [`TabRail`] and its options live here; `layout` places the rows, `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 layout;
8mod paint;
9#[cfg(test)]
10mod tests;
11
12use crate::event::{Event, MouseButton, MouseEvent, MouseKind};
13use crate::geometry::{Rect, Size, clamp_u16};
14use crate::keymap::Key;
15use crate::style::CellStyle;
16use crate::text;
17use crate::theme::State;
18use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
19
20use super::cells;
21use super::close_mark;
22use super::context_item::ContextItem;
23use super::context_menu;
24use super::edge_scroll::Edge;
25use super::row::LEAD;
26use super::scrollbar::{self, ScrollMetrics};
27use super::tab_model::{self, Direction, TabModel, drop_target, preview_order};
28
29/// Width of a collapsed rail: pillar, a one-cell label that never moves, a cell of air and the
30/// column kept for the scrollbar, so the label and the scrollbar never touch.
31const COLLAPSED: u16 = 4;
32
33/// What the one-cell marker of a tab in a collapsed [`TabRail`] shows.
34///
35/// Set it for the whole rail with [`TabRail::collapsed_marker`] and for one tab with
36/// [`RailTab::marker`], so users can pick a marker per tab as in the activity bar of an editor.
37/// The expanded rail is not affected.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum CollapsedMarker {
40    /// The tab's icon, or its initial when it has none. The default.
41    #[default]
42    Icon,
43    /// The first character of the name, upper case when the character has a single upper case
44    /// form.
45    Initial,
46    /// The tab's position counted from 1. Positions past 9 do not fit one cell and show `…`,
47    /// rather than a digit that would name another tab.
48    Number,
49}
50
51/// One tab of a [`TabRail`].
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct RailTab {
54    name: String,
55    icon: Option<String>,
56    badge: Option<String>,
57    status: Option<String>,
58    marker: Option<CollapsedMarker>,
59}
60
61impl RailTab {
62    /// A tab called `name`.
63    #[must_use]
64    pub fn new(name: impl Into<String>) -> Self {
65        Self { name: name.into(), icon: None, badge: None, status: None, marker: None }
66    }
67
68    /// What a collapsed rail shows for this tab, instead of the rail's
69    /// [`collapsed_marker`](TabRail::collapsed_marker).
70    #[must_use]
71    pub fn marker(mut self, marker: CollapsedMarker) -> Self {
72        self.marker = Some(marker);
73        self
74    }
75
76    /// Icon key drawn before the name; a collapsed rail shows only the icon (or the name's first
77    /// letter when there is none) unless its marker says otherwise.
78    #[must_use]
79    pub fn icon(mut self, key: impl Into<String>) -> Self {
80        self.icon = Some(key.into());
81        self
82    }
83
84    /// Short faint text at the right, such as the number of running containers.
85    #[must_use]
86    pub fn badge(mut self, text: impl Into<String>) -> Self {
87        self.badge = Some(text.into());
88        self
89    }
90
91    /// A status dot in theme colour `token` (`success`, `warning`, `danger`, `info`) before the
92    /// badge; a collapsed rail colours the tab's icon instead.
93    #[must_use]
94    pub fn status(mut self, token: impl Into<String>) -> Self {
95        self.status = Some(token.into());
96        self
97    }
98}
99
100/// Tabs stacked down the side of the screen, such as the open projects of an IDE.
101///
102/// Every tab is a row with an icon, the name and optional status dot and badge. The open tab is
103/// raised with the accent pillar, which breathes while the rail has keyboard focus; a hovered tab
104/// rises softly and its icon and name slide one cell while the status dot, badge and close mark stay
105/// put. [`collapsed`](Self::collapsed) narrows the rail to a thin strip of four cells: the pillar, a
106/// one-cell icon (or the name's first letter) that keeps its column when its tab is hovered or
107/// open, a cell of air and a column that becomes the scrollbar when tabs overflow.
108/// [`collapsed_marker`](Self::collapsed_marker) and [`RailTab::marker`] make that cell the tab's
109/// initial or position instead. [`on_add`](Self::on_add) ends the rail with an add (+) row.
110///
111/// [`row_height`](Self::row_height) makes every row a raised block several lines tall, with its
112/// content on the middle line, the close mark in its top right corner and the pillar down the
113/// whole block. Blocks taller than a line are one canvas line apart, so they do not read as one
114/// column; [`gap`](Self::gap) sets another distance, and `gap(0)` stacks them. Hover, presses,
115/// dragging, scrolling and the scrollbar count whole blocks; the collapsed strip, its name card and
116/// the add row keep the same height.
117///
118/// In a collapsed rail a name card beside the row shows the name and badge of the hovered tab, or
119/// of the open tab while the rail has keyboard focus. The pointer can move onto the card: it stays
120/// while the pointer is on the row or the card, rises under the pointer and opens its tab when
121/// clicked. With closing on, the card ends in the tab's close mark, on the card's middle line.
122///
123/// Keys while focused: ↑/↓ (or k/j) open the neighbouring tab, Home/End the first and last. The
124/// options are the same as [`Tabs`](super::Tabs) and behave the same:
125/// [`closable`](Self::closable) (a faint `×`, middle click, ctrl+w), [`pinned`](Self::pinned) and
126/// [`reorderable`](Self::reorderable) (drag with a ghost over the slot where the tab lands,
127/// ctrl+shift+↑/↓; held on the first or last visible block or past the rail's end, a dragged tab
128/// scrolls the rail one block after 400 ms and then every 150 ms, sooner the further past the end,
129/// with the scrollbar lit and the drop slot following, see [`on_drag_scroll`](Self::on_drag_scroll))
130/// and [`context_menu`](Self::context_menu) (a right click on a tab opens its
131/// menu at the pointer, the menu key or shift+F10 the menu of the open tab; without it a right click
132/// does nothing). Rows that do not fit scroll with the wheel or the scrollbar (click or drag it) and
133/// follow the open tab.
134///
135/// Style keys: `rail-tab` with `hover`, `selected`, `focus` and the variant `tall` for blocks taller
136/// than a line; `rail-badge` with the same states; `rail-hint` (`bg`, `fg`) with `hover` for the
137/// name card of a collapsed rail; `rail-add` with `hover` and `tall` for the add row;
138/// `close-mark`, `tab-ghost` and `tab-drop` shared with [`Tabs`](super::Tabs); `scrollbar`; the
139/// keys of [`ContextItem`](super::ContextItem) for the context menu.
140pub struct TabRail<Msg> {
141    tabs: Vec<RailTab>,
142    collapsed: bool,
143    marker: CollapsedMarker,
144    row_height: u16,
145    /// Canvas lines between rows when set; otherwise [`TabRail::spacing`] decides.
146    gap: Option<u16>,
147    on_add: Option<Box<dyn Fn() -> Msg>>,
148    model: TabModel<Msg>,
149}
150
151#[derive(Debug, Default)]
152struct RailMemory {
153    offset: usize,
154    followed: Option<usize>,
155    /// The collapsed row whose name card is shown beside the rail: a tab, or the add row.
156    hint: Option<RailRow>,
157    /// The name card painted in the last frame and its row. The card stays while the pointer is on
158    /// it, and presses on it act on its row.
159    card: Option<(RailRow, Rect)>,
160    /// Whether the pointer is dragging the scrollbar.
161    dragging_scrollbar: bool,
162}
163
164impl RailMemory {
165    /// Scrolls the least that shows row `index` when `visible` rows fit.
166    fn reveal(&mut self, index: usize, visible: usize) {
167        if index < self.offset {
168            self.offset = index;
169        } else if index >= self.offset + visible {
170            self.offset = index + 1 - visible;
171        }
172    }
173}
174
175/// A row of the rail.
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177enum RailRow {
178    Tab(usize),
179    Add,
180}
181
182impl<Msg: 'static> TabRail<Msg> {
183    /// A rail of `tabs`; the first is open.
184    #[must_use]
185    pub fn new(tabs: impl IntoIterator<Item = RailTab>) -> Self {
186        let tabs: Vec<RailTab> = tabs.into_iter().collect();
187        let model = TabModel::new(tabs.len());
188        Self { tabs, collapsed: false, marker: CollapsedMarker::Icon, row_height: 1, gap: None, on_add: None, model }
189    }
190
191    /// The open tab.
192    #[must_use]
193    pub fn active(mut self, index: usize) -> Self {
194        self.model.active = index;
195        self
196    }
197
198    /// Message for opening tab `index`.
199    #[must_use]
200    pub fn on_select(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
201        self.model.on_select = Some(Box::new(message));
202        self
203    }
204
205    /// Narrows the rail to a four-cell strip of icons; a name card beside a hovered tab names it
206    /// and takes clicks.
207    #[must_use]
208    pub fn collapsed(mut self, collapsed: bool) -> Self {
209        self.collapsed = collapsed;
210        self
211    }
212
213    /// What the collapsed strip shows for tabs without their own [`RailTab::marker`]: their icon
214    /// (the default), initial or position.
215    #[must_use]
216    pub fn collapsed_marker(mut self, marker: CollapsedMarker) -> Self {
217        self.marker = marker;
218        self
219    }
220
221    /// Makes every row a block `lines` tall (at least one), like the project tabs of an IDE: the
222    /// name, icon, status and badge sit on the middle line (the upper one of an even block), the
223    /// close mark flush right on the first line, and resting tabs are raised blocks too. One line,
224    /// the default, is a plain list of rows. Blocks taller than one line are one canvas line apart
225    /// unless [`gap`](Self::gap) is set. Any height works; the rail scrolls by whole blocks.
226    #[must_use]
227    pub fn row_height(mut self, lines: u16) -> Self {
228        self.row_height = lines.max(1);
229        self
230    }
231
232    /// Leaves `lines` of canvas between rows. By default one line separates rows taller than one
233    /// line and none separates one-line rows; `gap(0)` stacks tall blocks.
234    #[must_use]
235    pub fn gap(mut self, lines: u16) -> Self {
236        self.gap = Some(lines);
237        self
238    }
239
240    /// Ends the rail with an add row (`+`) that sends `message` when clicked, e.g. to open a
241    /// new project.
242    #[must_use]
243    pub fn on_add(mut self, message: impl Fn() -> Msg + 'static) -> Self {
244        self.on_add = Some(Box::new(message));
245        self
246    }
247
248    /// Makes tabs closable: `message(index)` asks the application to close tab `index`.
249    /// [`TabEdit::Close`](super::TabEdit) applies it to the application's list.
250    #[must_use]
251    pub fn closable(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
252        self.model.set_on_close(message);
253        self
254    }
255
256    /// Tabs that cannot be closed. They show no close mark.
257    #[must_use]
258    pub fn pinned(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
259        self.model.pinned = indices.into_iter().collect();
260        self
261    }
262
263    /// Makes tabs reorderable: `message(from, to)` asks the application to move a tab.
264    /// [`TabEdit::Move`](super::TabEdit) applies it to the application's list.
265    #[must_use]
266    pub fn reorderable(mut self, message: impl Fn(usize, usize) -> Msg + 'static) -> Self {
267        self.model.set_on_move(message);
268        self
269    }
270
271    /// Message for each step a dragged tab scrolls an overflowing rail, with the first row now in
272    /// view (counted from 0), e.g. to log it. Only reorderable rails scroll this way.
273    #[must_use]
274    pub fn on_drag_scroll(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
275        self.model.on_drag_scroll = Some(Box::new(message));
276        self
277    }
278
279    /// Gives every tab a context menu: `items(index)` builds the entries for tab `index`, such as
280    /// Close, Close others or Pin. A right click on a tab (or on the name card of a collapsed rail)
281    /// opens its menu at the pointer; the menu key or shift+F10 opens the menu of the open tab below
282    /// its block, scrolling the tab into view first. Choosing an entry sends its message.
283    #[must_use]
284    pub fn context_menu(mut self, items: impl Fn(usize) -> Vec<ContextItem<Msg>> + 'static) -> Self {
285        self.model.set_context_menu(items);
286        self
287    }
288}
289
290impl<Msg: 'static> Widget<Msg> for TabRail<Msg> {
291    fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
292        let height = self.natural_height(self.rows());
293        if self.collapsed {
294            return Size::new(COLLAPSED, height).min(available);
295        }
296        let widest = (0..self.tabs.len())
297            .map(|i| {
298                let icon = if self.tabs[i].icon.is_some() { 2 } else { 0 };
299                cells::sum([LEAD, icon, text::width(&self.tabs[i].name), self.trailing_width(i, self.row_height), 3])
300            })
301            .max()
302            .unwrap_or(0);
303        Size::new(widest, height).min(available)
304    }
305
306    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
307        cx.register_hit(area);
308        let card = {
309            let memory = cx.memory::<RailMemory>();
310            memory.hint = None;
311            memory.card.take()
312        };
313        if self.rows() == 0 || area.is_empty() {
314            return;
315        }
316        let focused = cx.is_focus_visible();
317        let pointer = cx.pointer();
318        let active = self.model.active();
319        let drag = self.model.drag(cx);
320        let visible = self.visible_blocks(area.height);
321        let offset = {
322            let memory = cx.memory::<RailMemory>();
323            if drag.is_none() && memory.followed != Some(active) {
324                memory.reveal(active, visible);
325                memory.followed = Some(active);
326            }
327            memory.offset = memory.offset.min(self.rows().saturating_sub(visible));
328            memory.offset
329        };
330        let (content, metrics) = self.content(area, offset);
331        let resting = self.slots(&self.identity(), offset, content);
332        let target = drag.map(|d| (d.index, drop_target(&resting, d.index, d.pointer, Direction::Down)));
333        let order = preview_order(self.tabs.len(), target);
334
335        // The row whose name card the pointer is on keeps its card and stays raised, so the
336        // pointer can travel from the row onto the card.
337        let carded = card
338            .filter(|(_, rect)| drag.is_none() && pointer.is_some_and(|(x, y)| rect.contains(x, y)))
339            .map(|(row, _)| row);
340        // With keyboard focus and the pointer elsewhere, a collapsed rail names its open tab.
341        let named = (self.collapsed && focused && pointer.is_none() && drag.is_none()).then_some(RailRow::Tab(active));
342        // An open context menu takes the overlay and the pointer: no name card competes with it,
343        // and only the tab it acts on stays raised.
344        let menu_tab = self.model.menu_tab(cx);
345        let menu = menu_tab.is_some();
346        if menu {
347            cx.request_overlay(area);
348        }
349        let carded = carded.filter(|_| !menu);
350        let named = named.filter(|_| !menu);
351        let pointer = pointer.filter(|_| !menu);
352
353        for (index, rect) in self.slots(&order, offset, content) {
354            if drag.is_some_and(|d| d.index == index) {
355                tab_model::paint_drop_slot(cx, rect);
356                continue;
357            }
358            let mut states = Vec::new();
359            let hovered = drag.is_none() && pointer.is_some_and(|(x, y)| rect.contains(x, y));
360            if hovered || carded == Some(RailRow::Tab(index)) || menu_tab == Some(index) {
361                states.push(State::Hover);
362            }
363            if self.collapsed && !menu && (hovered || carded.or(named) == Some(RailRow::Tab(index))) {
364                cx.memory::<RailMemory>().hint = Some(RailRow::Tab(index));
365                cx.request_overlay(rect);
366            }
367            if index == active {
368                states.push(State::Selected);
369                if focused {
370                    states.push(State::Focus);
371                }
372            }
373            self.paint_row(cx, rect, index, &states, None);
374        }
375
376        if let Some(rect) = self.add_rect(content, offset) {
377            let hovered =
378                drag.is_none() && (pointer.is_some_and(|(x, y)| rect.contains(x, y)) || carded == Some(RailRow::Add));
379            if hovered && self.collapsed && !menu {
380                cx.memory::<RailMemory>().hint = Some(RailRow::Add);
381                cx.request_overlay(rect);
382            }
383            self.paint_add(cx, rect, hovered);
384        }
385
386        if let Some(drag) = drag {
387            let height = resting.first().map_or(1, |(_, rect)| rect.height);
388            let top = content.y;
389            let bottom = (content.bottom() - i32::from(height)).max(top);
390            let y = (drag.pointer.1 - drag.grab.1).clamp(top, bottom);
391            let rect = Rect::new(content.x, y, content.width, height);
392            tab_model::paint_ghost_surface(cx, rect);
393            self.paint_row(cx, rect, drag.index, &[], Some("ghost"));
394        }
395
396        if metrics.overflows() {
397            let bar = Rect::new(area.right() - 1, area.y, 1, area.height);
398            // A dragged tab held against an end that can still scroll lights the scrollbar, the
399            // rail's own sign that it is moving.
400            let held =
401                drag.and_then(|drag| self.drag_zone(area, content, metrics, drag.pointer.1)).is_some_and(|zone| {
402                    match zone.edge {
403                        Edge::Back => offset > 0,
404                        Edge::Forward => offset < metrics.max_offset(),
405                    }
406                });
407            let active = held
408                || cx.memory::<RailMemory>().dragging_scrollbar
409                || (carded.is_none() && pointer.is_some_and(|(x, _)| x == bar.x));
410            scrollbar::paint(cx, bar, metrics, active, None);
411        }
412    }
413
414    fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
415        if self.model.paint_menu(cx, anchor) {
416            return;
417        }
418        let Some(row) = cx.memory::<RailMemory>().hint else {
419            return;
420        };
421        let (name, badge, closable) = match row {
422            RailRow::Tab(index) => {
423                (self.tabs[index].name.clone(), self.tabs[index].badge.clone(), self.model.closable(index))
424            }
425            RailRow::Add => (cx.env().i18n().translate("quvyta.tab-rail.add", &[]), None, false),
426        };
427        let card = Self::card(cx.clip(), anchor, &name, badge.as_deref(), closable);
428        let line = Self::middle(card);
429        let close = closable.then(|| close_mark::rect(card.right() - i32::from(close_mark::WIDTH), line));
430        let on_card = cx.pointer_anywhere().is_some_and(|(x, y)| card.contains(x, y));
431        let on_close = cx.pointer_anywhere().is_some_and(|(x, y)| close.is_some_and(|close| close.contains(x, y)));
432        let states = if on_card && !on_close { vec![State::Hover] } else { Vec::new() };
433        let style = cx.style("rail-hint", None, &states).text();
434        cx.clear(card, style.bg.unwrap_or_else(|| cx.color("overlay")));
435        cx.register_hit(card);
436
437        // The name, then the badge and the close mark anchored to the card's right end, as in a
438        // wide rail.
439        let mut right = card.right() - i32::from(close.map_or(1, |close| close.width));
440        if let Some(badge) = &badge {
441            let badge_style = cx.style("rail-badge", None, &states).text();
442            let width = text::width(badge);
443            right -= i32::from(width);
444            cx.text(right, line, badge, CellStyle { bg: None, ..badge_style }, width);
445            right -= 2;
446        }
447        let budget = clamp_u16(right - (card.x + 1));
448        let shown = text::truncate(&name, budget).into_owned();
449        cx.text(card.x + 1, line, &shown, CellStyle { bg: None, ..style }, budget);
450        if let Some(close) = close {
451            close_mark::paint(cx, close.x, close.y, true);
452        }
453        cx.memory::<RailMemory>().card = Some((row, card));
454    }
455
456    fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
457        if self.rows() == 0 {
458            return false;
459        }
460        let area = cx.area();
461        if self.menu_event(cx, event) {
462            return true;
463        }
464        match event {
465            Event::Key(_) if self.tabs.is_empty() => false,
466            Event::Key(key) => {
467                if key.is_plain(Key::Home) {
468                    return self.model.open(cx, 0);
469                }
470                if key.is_plain(Key::End) {
471                    return self.model.open(cx, self.tabs.len() - 1);
472                }
473                self.model.key(cx, key, Direction::Down)
474            }
475            Event::Mouse(mouse) => {
476                let offset = cx.memory::<RailMemory>().offset;
477                let (content, metrics) = self.content(area, offset);
478                if matches!(mouse.kind, MouseKind::ScrollUp | MouseKind::ScrollDown) {
479                    let memory = cx.memory::<RailMemory>();
480                    memory.offset = if mouse.kind == MouseKind::ScrollUp {
481                        memory.offset.saturating_sub(1)
482                    } else {
483                        (memory.offset + 1).min(metrics.max_offset())
484                    };
485                    return true;
486                }
487                // The name card starts over the scrollbar column and is on top of it, except while
488                // the scrollbar is being dragged.
489                let card = cx.memory::<RailMemory>().card.filter(|(_, rect)| rect.contains(mouse.x, mouse.y));
490                if (card.is_none() || cx.memory::<RailMemory>().dragging_scrollbar)
491                    && self.scrollbar(cx, mouse, metrics)
492                {
493                    return true;
494                }
495                // Presses on the add row (or its card) are its own; drags and releases pass on, so a
496                // tab dragged down onto it still lands.
497                let on_add = match card {
498                    Some((row, _)) => row == RailRow::Add,
499                    None => self.add_rect(content, offset).is_some_and(|add| add.contains(mouse.x, mouse.y)),
500                };
501                if on_add && let MouseKind::Down(button) = mouse.kind {
502                    if button == MouseButton::Left
503                        && let Some(message) = &self.on_add
504                    {
505                        cx.emit(message());
506                    }
507                    return true;
508                }
509                if self.tabs.is_empty() {
510                    return false;
511                }
512                let slots = self.slots(&self.identity(), offset, content);
513                let hit = match card {
514                    Some((RailRow::Tab(index), rect)) if index < self.tabs.len() => {
515                        Some(Self::card_hit(rect, index, self.model.closable(index), mouse.x, mouse.y))
516                    }
517                    _ => self.hit(&slots, mouse.x, mouse.y),
518                };
519                let used = self.model.pointer(cx, mouse, hit, &slots, Direction::Down);
520                if mouse.kind == MouseKind::Drag(MouseButton::Left) {
521                    let zone = self.drag_zone(area, content, metrics, mouse.y);
522                    self.model.edge_scroll(cx, zone, |cx, edge| {
523                        let memory = cx.memory::<RailMemory>();
524                        memory.offset = match edge {
525                            Edge::Back => memory.offset.checked_sub(1)?,
526                            Edge::Forward => Some(memory.offset + 1).filter(|next| *next <= metrics.max_offset())?,
527                        };
528                        Some(memory.offset)
529                    });
530                }
531                used
532            }
533            _ => false,
534        }
535    }
536
537    fn focusable(&self) -> bool {
538        !self.tabs.is_empty()
539    }
540}
541
542impl<Msg: 'static> TabRail<Msg> {
543    /// Offers `event` to the context menu with the tab under the pointer (on a row or on its name
544    /// card) and the open tab's block; true when the menu used it. The menu key first scrolls the
545    /// open tab into view, so its menu opens below its block even after the wheel moved it away.
546    fn menu_event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
547        if let Event::Key(key) = event
548            && self.model.has_menu()
549            && context_menu::is_menu_key(key)
550            && !context_menu::is_open_in(cx)
551        {
552            let visible = self.visible_blocks(cx.area().height);
553            cx.memory::<RailMemory>().reveal(self.model.active(), visible);
554        }
555        let offset = cx.memory::<RailMemory>().offset;
556        let (content, _) = self.content(cx.area(), offset);
557        let slots = self.slots(&self.identity(), offset, content);
558        let hit = match event {
559            Event::Mouse(mouse) => match cx.memory::<RailMemory>().card {
560                Some((RailRow::Tab(index), rect)) if rect.contains(mouse.x, mouse.y) && index < self.tabs.len() => {
561                    Some(Self::card_hit(rect, index, self.model.closable(index), mouse.x, mouse.y))
562                }
563                Some((RailRow::Add, rect)) if rect.contains(mouse.x, mouse.y) => None,
564                _ => self.hit(&slots, mouse.x, mouse.y),
565            },
566            _ => None,
567        };
568        let open_tab = slots.iter().find(|(index, _)| *index == self.model.active()).map(|(_, rect)| *rect);
569        self.model.menu_event(cx, event, hit, open_tab)
570    }
571
572    /// Clicks and drags on the scrollbar column; true when the event was for the scrollbar.
573    fn scrollbar(&self, cx: &mut EventCx<'_, Msg>, mouse: &MouseEvent, metrics: ScrollMetrics) -> bool {
574        let area = cx.area();
575        let row = clamp_u16(mouse.y - area.y);
576        match mouse.kind {
577            MouseKind::Down(MouseButton::Left) if metrics.overflows() && mouse.x == area.right() - 1 => {
578                cx.capture_pointer();
579                let memory = cx.memory::<RailMemory>();
580                memory.dragging_scrollbar = true;
581                memory.offset = metrics.offset_at(row, area.height);
582                true
583            }
584            MouseKind::Drag(MouseButton::Left) if cx.memory::<RailMemory>().dragging_scrollbar => {
585                cx.memory::<RailMemory>().offset = metrics.offset_at(row, area.height);
586                true
587            }
588            MouseKind::Up(MouseButton::Left) if cx.memory::<RailMemory>().dragging_scrollbar => {
589                cx.memory::<RailMemory>().dragging_scrollbar = false;
590                true
591            }
592            _ => false,
593        }
594    }
595}