navigable.rs

  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}