Skip to main content

egui_macview/controls/segmented/
mod.rs

1//! Authentic macOS `NSSegmentedControl` primitive.
2//!
3//! Grounded in Apple macOS Human Interface Guidelines (HIG) and W3C DTCG tokens via [`MacTheme`].
4//! Fully data-driven: zero hardcoded metrics or ad-hoc token arithmetic.
5
6mod interaction;
7mod item;
8mod layout;
9mod multi;
10mod render;
11mod storage;
12
13pub use item::{
14    MacSegmentEvent, MacSegmentItem, SegmentContent, SegmentEvent, SegmentIconFn,
15    SegmentIconPainter, SegmentItem, SegmentMenuTrigger,
16};
17
18use crate::controls::MacControlSize;
19use crate::symbols::MacSymbol;
20use crate::theme::MacTheme;
21use egui::{Rect, Response, Ui};
22use std::borrow::Cow;
23use storage::{InlineBuf, INLINE_CAP};
24
25/// Authentic macOS segmented pill selector (`NSSegmentedControl` equivalent).
26///
27/// Supports `CASpring` sliding pill physics, per-segment widths, template-tinted vector icons,
28/// per-segment enablement, `isHighlighted` pressed feedback, Sonoma active-pill divider suppression,
29/// `AccessKit` (`egui::Role::RadioGroup` / `egui::Role::Toolbar`) semantics, stack-inline storage,
30/// and AppKit menu triggers (`PressAndHold`, `PullDown`, `SecondaryClick`, `Keyboard`).
31///
32/// # Example
33/// ```rust
34/// use egui_macview::{MacSegmentedControl, MacTheme};
35/// # let ctx = egui::Context::default();
36/// # let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
37/// let theme = MacTheme::default();
38/// let mut selected = 1;
39/// MacSegmentedControl::new(&["Day", "Week", "Month", "Year"], selected)
40///     .accessibility_label("Calendar Scale")
41///     .theme(&theme)
42///     .show(ui, |i| selected = i);
43/// # });
44/// # output.textures_delta.clear();
45/// ```
46pub struct MacSegmentedControl<'a> {
47    pub(crate) segments: InlineBuf<SegmentItem<'a>>,
48    pub(crate) selected_idx: usize,
49    pub(crate) enabled: bool,
50    pub(crate) theme: Option<&'a MacTheme>,
51    pub(crate) height: Option<f32>,
52    pub(crate) control_size: MacControlSize,
53    pub(crate) small_square: bool,
54    pub(crate) momentary: bool,
55    pub(crate) fixed_rect: Option<Rect>,
56    pub(crate) id_salt: Option<egui::Id>,
57    pub(crate) a11y_label: Option<Cow<'a, str>>,
58}
59
60impl<'a> MacSegmentedControl<'a> {
61    /// Maximum number of segments stored inline on the stack (`8`) before spilling to heap storage.
62    pub const INLINE_SEGMENT_CAPACITY: usize = INLINE_CAP;
63
64    /// Creates a segmented control from borrowed text option labels and currently selected index.
65    pub fn new(options: &'a [&'a str], selected_idx: usize) -> Self {
66        Self::with_custom_segments(options.iter().map(|&opt| SegmentItem::new(opt)), selected_idx)
67    }
68
69    /// Creates a segmented control from any array or iterator of [`SegmentItem`] descriptors.
70    pub fn with_custom_segments(
71        segments: impl IntoIterator<Item = SegmentItem<'a>>,
72        selected_idx: usize,
73    ) -> Self {
74        Self {
75            segments: segments.into_iter().collect(),
76            selected_idx,
77            enabled: true,
78            theme: None,
79            height: None,
80            control_size: MacControlSize::Regular,
81            small_square: false,
82            momentary: false,
83            fixed_rect: None,
84            id_salt: None,
85            a11y_label: None,
86        }
87    }
88
89    /// Creates a momentary segmented action strip (`NSSegmentedSwitchTrackingMomentary`) with no persistent active pill.
90    pub fn momentary(segments: impl IntoIterator<Item = SegmentItem<'a>>) -> Self {
91        let mut ctrl = Self::with_custom_segments(segments, 0);
92        ctrl.momentary = true;
93        ctrl
94    }
95
96    /// Creates a segmented control from a slice of [`MacSymbol`] icons.
97    pub fn with_icons(symbols: &'a [MacSymbol], selected_idx: usize) -> Self {
98        Self::with_custom_segments(symbols.iter().map(|&sym| SegmentItem::from_symbol(sym)), selected_idx)
99    }
100
101    /// Returns `true` when the segment descriptors reside inline on the stack without heap allocation.
102    #[inline]
103    pub const fn is_inline_allocated(&self) -> bool {
104        self.segments.is_inline()
105    }
106
107    /// Explicitly binds an ID salt so spring physics and gesture state stay invariant across conditional peer layouts.
108    pub fn id_salt(mut self, salt: impl std::hash::Hash + std::fmt::Debug) -> Self {
109        self.id_salt = Some(egui::Id::NULL.with(salt));
110        self
111    }
112
113    /// Sets the `NSAccessibility` / `AccessKit` group label announced by assistive technologies.
114    pub fn accessibility_label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
115        self.a11y_label = Some(label.into());
116        self
117    }
118
119    /// Enables or disables the entire segmented control.
120    pub const fn enabled(mut self, enabled: bool) -> Self {
121        self.enabled = enabled;
122        self
123    }
124
125    /// Injects an explicit [`MacTheme`] reference.
126    pub const fn theme(mut self, theme: &'a MacTheme) -> Self {
127        self.theme = Some(theme);
128        self
129    }
130
131    /// Overrides control height in points (defaults to size-tier segmented height token).
132    pub const fn height(mut self, height: f32) -> Self {
133        self.height = Some(height);
134        self
135    }
136
137    /// Sets the AppKit control size (`Regular` `22pt`, `Small` `18pt`, or `Mini` `15pt`).
138    pub const fn control_size(mut self, size: MacControlSize) -> Self {
139        self.control_size = size;
140        self
141    }
142
143    /// Configures AppKit `NSSegmentedStyleSmallSquare` (`2.0pt` corner radius and full-height dividers).
144    pub const fn small_square(mut self) -> Self {
145        self.small_square = true;
146        self
147    }
148
149    /// Pins the segmented control to an exact NIB rectangle.
150    pub const fn fixed_rect(mut self, rect: Rect) -> Self {
151        self.fixed_rect = Some(rect);
152        self
153    }
154
155    /// Renders a multi-select segmented strip (`NSSegmentedSwitchTrackingSelectAny`).
156    pub fn show_multi(self, ui: &mut Ui, active: &mut [bool]) -> Option<usize> {
157        multi::render_multi(self, ui, active)
158    }
159
160    /// Renders the segmented control and invokes `on_select` when a primary selection occurs.
161    pub fn show(self, ui: &mut Ui, mut on_select: impl FnMut(usize)) -> Response {
162        self.show_with_events(ui, |ev| {
163            if !ev.opens_menu() {
164                on_select(ev.index);
165            }
166        })
167    }
168
169    /// Renders the segmented control and dispatches [`SegmentEvent`] values for both selection and menu triggers.
170    pub fn show_with_events(self, ui: &mut Ui, on_event: impl FnMut(SegmentEvent)) -> Response {
171        render::render_segmented(self, ui, on_event)
172    }
173
174    /// Renders a momentary segmented strip (`NSSegmentedSwitchTrackingMomentary`) and invokes `on_action` on click/activation.
175    pub fn show_momentary(mut self, ui: &mut Ui, mut on_action: impl FnMut(usize)) -> Response {
176        self.momentary = true;
177        self.show_with_events(ui, |ev| {
178            if !ev.opens_menu() {
179                on_action(ev.index);
180            }
181        })
182    }
183
184    /// Renders a momentary segmented strip (`NSSegmentedSwitchTrackingMomentary`) and dispatches both click and menu [`SegmentEvent`]s.
185    pub fn show_momentary_with_events(
186        mut self,
187        ui: &mut Ui,
188        on_event: impl FnMut(SegmentEvent),
189    ) -> Response {
190        self.momentary = true;
191        render::render_segmented(self, ui, on_event)
192    }
193}