Skip to main content

ui/components/
navigable.rs

1use crate::prelude::*;
2use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle};
3
4/// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use
5#[derive(IntoElement)]
6pub struct Navigable {
7    child: AnyElement,
8    selectable_children: Vec<NavigableEntry>,
9}
10
11/// An entry of [Navigable] that can be navigated to.
12#[derive(Clone)]
13pub struct NavigableEntry {
14    #[allow(missing_docs)]
15    pub focus_handle: FocusHandle,
16    #[allow(missing_docs)]
17    pub scroll_anchor: Option<ScrollAnchor>,
18}
19
20impl NavigableEntry {
21    /// Creates a new [NavigableEntry] for a given scroll handle.
22    pub fn new(scroll_handle: &ScrollHandle, cx: &App) -> Self {
23        Self {
24            focus_handle: cx.focus_handle(),
25            scroll_anchor: Some(ScrollAnchor::for_handle(scroll_handle.clone())),
26        }
27    }
28    /// Create a new [NavigableEntry] that cannot be scrolled to.
29    pub fn focusable(cx: &App) -> Self {
30        Self {
31            focus_handle: cx.focus_handle(),
32            scroll_anchor: None,
33        }
34    }
35}
36impl Navigable {
37    /// Creates new empty [Navigable] wrapper.
38    pub fn new(child: AnyElement) -> Self {
39        Self {
40            child,
41            selectable_children: vec![],
42        }
43    }
44
45    /// Add a new entry that can be navigated to via keyboard.
46    ///
47    /// The order of calls to [Navigable::entry] determines the order of traversal of
48    /// elements via successive uses of `menu:::SelectNext/SelectPrevious`
49    pub fn entry(mut self, child: NavigableEntry) -> Self {
50        self.selectable_children.push(child);
51        self
52    }
53
54    fn find_focused(
55        selectable_children: &[NavigableEntry],
56        window: &mut Window,
57        cx: &mut App,
58    ) -> Option<usize> {
59        selectable_children
60            .iter()
61            .position(|entry| entry.focus_handle.contains_focused(window, cx))
62    }
63}
64
65impl RenderOnce for Navigable {
66    fn render(self, _window: &mut Window, _: &mut App) -> impl crate::IntoElement {
67        div()
68            .on_action({
69                let children = self.selectable_children.clone();
70
71                move |_: &menu::SelectNext, window, cx| {
72                    let target = Self::find_focused(&children, window, cx)
73                        .and_then(|index| {
74                            index.checked_add(1).filter(|index| *index < children.len())
75                        })
76                        .unwrap_or(0);
77                    if let Some(entry) = children.get(target) {
78                        entry.focus_handle.focus(window, cx);
79                        if let Some(anchor) = &entry.scroll_anchor {
80                            anchor.scroll_to(window, cx);
81                        }
82                    }
83                }
84            })
85            .on_action({
86                let children = self.selectable_children;
87                move |_: &menu::SelectPrevious, window, cx| {
88                    let target = Self::find_focused(&children, window, cx)
89                        .and_then(|index| index.checked_sub(1))
90                        .or(children.len().checked_sub(1));
91                    if let Some(entry) = target.and_then(|target| children.get(target)) {
92                        entry.focus_handle.focus(window, cx);
93                        if let Some(anchor) = &entry.scroll_anchor {
94                            anchor.scroll_to(window, cx);
95                        }
96                    }
97                }
98            })
99            .size_full()
100            .child(self.child)
101    }
102}