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