Skip to main content

fc_dioxus/behavior/
roving.rs

1//! Roving focus: one tab stop for a set of items, arrows to move within it.
2//!
3//! A port of the arrow handling in upstream's `tabs.ts` and `toolbar.ts`: arrows follow
4//! the writing direction (in RTL, Left moves forward), orientation decides which arrows
5//! apply, Home and End jump to the ends, and the step wraps.
6
7use crate::{core::state::Orientation, theme::Direction};
8use dioxus::prelude::*;
9use std::rc::Rc;
10
11/// Where a key asks focus to go.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Move {
14    /// The next item, wrapping.
15    Next,
16    /// The previous item, wrapping.
17    Previous,
18    /// The first item.
19    First,
20    /// The last item.
21    Last,
22}
23
24/// The move a key makes, or `None` for keys the set leaves alone, such as Left and Right
25/// in a vertical list, so the page can still scroll sideways.
26#[must_use]
27pub fn intent(key: &Key, orientation: Orientation, direction: Direction) -> Option<Move> {
28    let rtl = direction == Direction::Rtl;
29    match (key, orientation) {
30        (Key::Home, _) => Some(Move::First),
31        (Key::End, _) => Some(Move::Last),
32        (Key::ArrowDown, Orientation::Vertical) => Some(Move::Next),
33        (Key::ArrowUp, Orientation::Vertical) => Some(Move::Previous),
34        (Key::ArrowRight, Orientation::Horizontal) => {
35            Some(if rtl { Move::Previous } else { Move::Next })
36        }
37        (Key::ArrowLeft, Orientation::Horizontal) => {
38            Some(if rtl { Move::Next } else { Move::Previous })
39        }
40        _ => None,
41    }
42}
43
44/// The index focus moves to from `current`, skipping items `reachable` rejects.
45/// `None` when nothing is reachable.
46///
47/// ```
48/// use fc_dioxus::behavior::roving::{target, Move};
49/// let enabled = [true, false, true, true];
50/// let reachable = |i: usize| enabled.get(i).copied().unwrap_or(false);
51/// assert_eq!(target(4, 0, Move::Next, reachable), Some(2));
52/// assert_eq!(target(4, 3, Move::Next, reachable), Some(0));
53/// assert_eq!(target(4, 0, Move::Previous, reachable), Some(3));
54/// assert_eq!(target(4, 2, Move::First, reachable), Some(0));
55/// assert_eq!(target(4, 0, Move::Last, reachable), Some(3));
56/// assert_eq!(target(0, 0, Move::Next, reachable), None);
57/// ```
58#[must_use]
59pub fn target(
60    len: usize,
61    current: usize,
62    step: Move,
63    reachable: impl Fn(usize) -> bool,
64) -> Option<usize> {
65    if len == 0 {
66        return None;
67    }
68    match step {
69        Move::First => (0..len).find(|&i| reachable(i)),
70        Move::Last => (0..len).rev().find(|&i| reachable(i)),
71        Move::Next => (1..=len)
72            .map(|n| (current + n) % len)
73            .find(|&i| reachable(i)),
74        Move::Previous => (1..=len)
75            .map(|n| (current + len - n % len) % len)
76            .find(|&i| reachable(i)),
77    }
78}
79
80/// One member of a roving set.
81#[derive(Clone)]
82pub(crate) struct Member {
83    pub(crate) key: u64,
84    pub(crate) value: String,
85    /// Can take focus at all (not natively disabled).
86    pub(crate) focusable: bool,
87    /// Can be chosen (neither disabled nor unavailable).
88    pub(crate) enabled: bool,
89    pub(crate) mounted: Option<Rc<MountedData>>,
90}
91
92/// The members of a set in the order they first rendered, which is document order for
93/// children written in place.
94#[derive(Clone, Copy)]
95pub(crate) struct Roster {
96    members: Signal<Vec<Member>>,
97    next: Signal<u64>,
98}
99impl Roster {
100    pub(crate) fn new() -> Self {
101        Self {
102            members: Signal::new(Vec::new()),
103            next: Signal::new(0),
104        }
105    }
106    /// Snapshot without subscribing.
107    pub(crate) fn peek(&self) -> Vec<Member> {
108        self.members.peek().clone()
109    }
110    /// Snapshot, re-rendering the caller when membership changes.
111    pub(crate) fn read(&self) -> Vec<Member> {
112        self.members.read().clone()
113    }
114    fn join(mut self, value: String, focusable: bool, enabled: bool) -> u64 {
115        let key = *self.next.peek();
116        self.next.set(key + 1);
117        self.members.write().push(Member {
118            key,
119            value,
120            focusable,
121            enabled,
122            mounted: None,
123        });
124        key
125    }
126    fn update(mut self, key: u64, value: &str, focusable: bool, enabled: bool) {
127        let stale = self.members.peek().iter().any(|m| {
128            m.key == key && (m.value != value || m.focusable != focusable || m.enabled != enabled)
129        });
130        if stale {
131            if let Some(member) = self.members.write().iter_mut().find(|m| m.key == key) {
132                value.clone_into(&mut member.value);
133                member.focusable = focusable;
134                member.enabled = enabled;
135            }
136        }
137    }
138    pub(crate) fn mount(mut self, key: u64, mounted: Rc<MountedData>) {
139        if let Some(member) = self.members.write().iter_mut().find(|m| m.key == key) {
140            member.mounted = Some(mounted);
141        }
142    }
143    fn leave(mut self, key: u64) {
144        self.members.write().retain(|m| m.key != key);
145    }
146}
147
148/// Join the nearest roster for as long as the calling component lives. Returns the
149/// member's key, or `None` outside a roster.
150pub(crate) fn use_member(value: &str, focusable: bool, enabled: bool) -> Option<(Roster, u64)> {
151    let roster = try_consume_context::<Roster>();
152    let joined = use_hook(|| roster.map(|r| (r, r.join(value.to_owned(), focusable, enabled))));
153    if let Some((roster, key)) = joined {
154        roster.update(key, value, focusable, enabled);
155    }
156    use_drop(move || {
157        if let Some((roster, key)) = joined {
158            roster.leave(key);
159        }
160    });
161    joined
162}
163
164/// Move focus to a member after a key press.
165pub(crate) fn focus(member: &Member) {
166    if let Some(mounted) = member.mounted.clone() {
167        spawn(async move {
168            let _ = mounted.set_focus(true).await;
169        });
170    }
171}