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}