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);
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);
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}