Skip to main content

fc_dioxus/core/
id.rs

1//! Validated identifiers and deterministic, root-scoped generated IDs.
2
3use dioxus::core::current_scope_id;
4use dioxus::prelude::*;
5use std::{cell::Cell, fmt, rc::Rc};
6
7/// A non-empty HTML identifier without whitespace or control characters.
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub struct Id(String);
10impl Id {
11    /// The identifier, suitable for an ARIA reference.
12    #[must_use]
13    pub fn as_str(&self) -> &str {
14        &self.0
15    }
16    /// A related identifier, such as a field's hint. The suffix is a literal without
17    /// whitespace, so the result is valid whenever `self` is.
18    #[must_use]
19    pub(crate) fn suffixed(&self, suffix: &'static str) -> Self {
20        Self(format!("{}-{suffix}", self.0))
21    }
22}
23impl TryFrom<&str> for Id {
24    type Error = InvalidId;
25    /// # Errors
26    /// Rejects empty identifiers, whitespace, and control characters.
27    fn try_from(value: &str) -> Result<Self, Self::Error> {
28        if value.is_empty() || value.chars().any(|c| c.is_whitespace() || c.is_control()) {
29            Err(InvalidId)
30        } else {
31            Ok(Self(value.to_owned()))
32        }
33    }
34}
35impl TryFrom<String> for Id {
36    type Error = InvalidId;
37    fn try_from(value: String) -> Result<Self, Self::Error> {
38        Self::try_from(value.as_str())
39    }
40}
41impl fmt::Display for Id {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(&self.0)
44    }
45}
46/// The input cannot be an HTML identifier.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct InvalidId;
49impl fmt::Display for InvalidId {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str("id must be non-empty without whitespace or controls")
52    }
53}
54impl std::error::Error for InvalidId {}
55
56#[derive(Clone)]
57pub(crate) struct IdAllocator {
58    prefix: usize,
59    next: Rc<Cell<u64>>,
60}
61impl IdAllocator {
62    pub(crate) fn new() -> Self {
63        Self {
64            prefix: current_scope_id().0,
65            next: Rc::new(Cell::new(0)),
66        }
67    }
68    fn next(&self) -> Id {
69        let next = self.next.get();
70        self.next.set(next.saturating_add(1));
71        Id(format!("fc-{}-{next}", self.prefix))
72    }
73}
74
75/// Allocate once per hook, in render order. Identical trees produce identical IDs.
76///
77/// Prefer a `FutureCss` root. Outside one, IDs are scoped to the current Dioxus
78/// component and hook order rather than a process-global counter.
79#[must_use]
80pub fn use_id() -> Id {
81    let allocator = use_hook(|| {
82        try_consume_context::<IdAllocator>().unwrap_or_else(|| provide_context(IdAllocator::new()))
83    });
84    use_hook(|| allocator.next())
85}