Skip to main content

teksilo_widgets/segmented_control/
id.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Stable per-segment identifiers.
5//!
6//! [`SegmentId`] is the identity of a segment as segments are added,
7//! removed, reordered, or *contributed* by another crate. Selection, the
8//! `on_change` callback, and the overflow menu are all keyed by
9//! `SegmentId` rather than by position — so inserting a segment never
10//! silently re-points the selection at a different one.
11//!
12//! Mirrors [`TabId`](crate::tab_widget::TabId): apps either let the
13//! framework allocate fresh ids ([`SegmentId::fresh`]) or wrap their own
14//! external keys via [`SegmentId::from_raw`] / [`SegmentId::from_u64`].
15
16use std::num::NonZeroU64;
17use std::sync::atomic::{AtomicU64, Ordering};
18
19/// Stable identity of a segment. Cheap to copy; survives rebuilds,
20/// locale changes, and segments being inserted around it.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct SegmentId(NonZeroU64);
23
24/// Framework-allocated ids start here, leaving everything below it to
25/// apps. Without the split, `SegmentId::from_u64(1)` — the obvious first
26/// constant anyone writes — would collide with the first
27/// [`SegmentId::fresh`] of the process.
28const FRESH_BASE: u64 = 1 << 48;
29
30impl SegmentId {
31    /// Allocate a new, never-before-seen id. Backed by a monotonic
32    /// global counter — overflow is theoretically possible after 2^64
33    /// calls, at which point the universe has had bigger problems.
34    ///
35    /// [`Segment::new`](super::Segment::new) calls this for you, so a
36    /// control that never persists its selection needs no explicit ids.
37    ///
38    /// Allocations start at 2^48, so they can never collide with a small
39    /// constant an app declared through [`from_u64`](Self::from_u64).
40    pub fn fresh() -> Self {
41        static COUNTER: AtomicU64 = AtomicU64::new(FRESH_BASE);
42        let raw = COUNTER.fetch_add(1, Ordering::Relaxed);
43        // The counter starts well above zero and only ever increments, so
44        // the value is non-zero in any practical run.
45        Self(NonZeroU64::new(raw).expect("SegmentId counter wrapped to zero"))
46    }
47
48    /// Wrap an externally-allocated key. Use this when the segment's
49    /// identity comes from an app-side store (a view-mode enum
50    /// discriminant, a plugin key hash, …) — calling [`SegmentId::fresh`]
51    /// would allocate a *new* id every restart, breaking a persisted
52    /// selection.
53    pub const fn from_raw(value: NonZeroU64) -> Self {
54        Self(value)
55    }
56
57    /// `const` convenience over [`from_raw`](Self::from_raw), so an app
58    /// can declare its segments as constants:
59    ///
60    /// ```
61    /// # use teksilo_widgets::SegmentId;
62    /// const SYNOPSIS: SegmentId = SegmentId::from_u64(1);
63    /// const CHAPTER: SegmentId = SegmentId::from_u64(2);
64    /// ```
65    ///
66    /// # Panics
67    ///
68    /// If `value` is zero. Because this is a `const fn`, a literal zero
69    /// is caught at compile time rather than at run time.
70    pub const fn from_u64(value: u64) -> Self {
71        match NonZeroU64::new(value) {
72            Some(v) => Self(v),
73            None => panic!("SegmentId::from_u64 requires a non-zero value"),
74        }
75    }
76
77    /// The underlying non-zero `u64`. Serialize this to persist a
78    /// selection across sessions; restore via [`from_raw`](Self::from_raw)
79    /// or [`from_u64`](Self::from_u64).
80    pub const fn raw(self) -> NonZeroU64 {
81        self.0
82    }
83
84    /// The underlying value as a plain `u64`.
85    pub const fn get(self) -> u64 {
86        self.0.get()
87    }
88}
89
90impl std::fmt::Display for SegmentId {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        write!(f, "SegmentId({})", self.0.get())
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn fresh_ids_are_unique_and_non_zero() {
102        let a = SegmentId::fresh();
103        let b = SegmentId::fresh();
104        assert_ne!(a, b);
105        assert!(a.get() > 0);
106        assert!(b.get() > 0);
107    }
108
109    #[test]
110    fn raw_round_trips() {
111        let id = SegmentId::from_u64(42);
112        assert_eq!(id.get(), 42);
113        assert_eq!(SegmentId::from_raw(id.raw()), id);
114    }
115
116    #[test]
117    fn const_construction_is_usable_in_a_const_item() {
118        const A: SegmentId = SegmentId::from_u64(7);
119        assert_eq!(A.get(), 7);
120    }
121
122    #[test]
123    fn fresh_ids_never_collide_with_small_app_constants() {
124        // `from_u64(1)` is the first constant anyone writes; a counter
125        // starting at 1 would hand out the same id to an unrelated
126        // segment and silently merge two selections.
127        const APP: SegmentId = SegmentId::from_u64(1);
128        for _ in 0..64 {
129            assert_ne!(SegmentId::fresh(), APP);
130        }
131        assert!(SegmentId::fresh().get() >= FRESH_BASE);
132    }
133}