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