Skip to main content

egui_macview/controls/input_cell/
mod.rs

1//! Authentic macOS NSTextFieldCell primitive with Tab-chain and Transactional Buffer.
2//!
3//! Grounded in macOS HIG and W3C DTCG tokens via [`MacTheme`].
4//! Fully data-driven: zero hardcoded colors, radiuses, or padding metrics.
5
6mod editing;
7
8use crate::theme::MacTheme;
9use egui::{Align, Id, Rect, Response, Ui};
10use std::ops::RangeInclusive;
11
12/// Standard AppKit `NSControlSize` variants for `MacInputCell` and AppKit NIB controls.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum MacControlSize {
15    /// `NSControlSizeRegular` (`22pt` height, `13pt` font).
16    #[default]
17    Regular,
18    /// `NSControlSizeSmall` (`19pt` height, `11pt` font).
19    Small,
20    /// `NSControlSizeMini` (`15pt`–`16pt` height, `9pt` font).
21    Mini,
22    /// `NSControlSizeLarge` (`28pt` height, `16pt` font).
23    Large,
24    /// Dense inspector / HUD infobox cell (`18pt` height, `11pt` font).
25    CompactInfobox,
26}
27
28/// Authentic macOS text entry cell (AppKit `NSTextFieldCell` style).
29///
30/// Features transactional input buffering, Escape rollback, Return confirmation,
31/// deterministic Tab-chain cyclic focus navigation, automatic NIB height-based
32/// `MacControlSize` inference, AppKit baseline centering, and horizontal text alignment.
33///
34/// # Example
35///
36/// ```rust
37/// use egui_macview::{MacInputCell, MacTheme};
38///
39/// # let ctx = egui::Context::default();
40/// # let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
41/// let theme = MacTheme::default();
42/// let mut text = String::from("Hello macOS");
43///
44/// MacInputCell::new("input_demo", &mut text)
45///     .theme(&theme)
46///     .placeholder("Type here...")
47///     .show(ui);
48/// # });
49/// # output.textures_delta.clear();
50/// ```
51pub struct MacInputCell<'a> {
52    pub(super) id: Id,
53    pub(super) value: &'a mut String,
54    pub(super) theme: Option<&'a MacTheme>,
55    pub(super) width: Option<f32>,
56    pub(super) height: Option<f32>,
57    pub(super) font_size: Option<f32>,
58    pub(super) corner_radius: Option<f32>,
59    pub(super) control_size: MacControlSize,
60    pub(super) explicit_control_size: bool,
61    pub(super) fixed_rect: Option<Rect>,
62    pub(super) text_align: Align,
63    pub(super) enabled: bool,
64    pub(super) next_focus_id: Option<Id>,
65    pub(super) prev_focus_id: Option<Id>,
66    pub(super) placeholder: Option<&'a str>,
67    pub(super) numeric_config: Option<(RangeInclusive<f64>, f64)>,
68}
69
70impl<'a> MacInputCell<'a> {
71    /// Creates a new `MacInputCell` bound to `value` and identified by `id_source`.
72    pub fn new(id_source: impl std::hash::Hash + std::fmt::Debug, value: &'a mut String) -> Self {
73        let id = Id::NULL.with(id_source);
74        Self::with_id(id, value)
75    }
76
77    /// Creates a cell directly with an existing `egui::Id` without double-hashing.
78    pub fn with_id(id: Id, value: &'a mut String) -> Self {
79        Self {
80            id,
81            value,
82            theme: None,
83            width: None,
84            height: None,
85            font_size: None,
86            corner_radius: None,
87            control_size: MacControlSize::Regular,
88            explicit_control_size: false,
89            fixed_rect: None,
90            text_align: Align::Min,
91            enabled: true,
92            next_focus_id: None,
93            prev_focus_id: None,
94            placeholder: None,
95            numeric_config: None,
96        }
97    }
98
99    /// Configures numeric arrow-key stepping (`ArrowUp`/`ArrowDown` by `±step`,
100    /// `Shift` by `±10 * step`, `Cmd`/`Ctrl` by `±100 * step`, clamped to `range`).
101    pub fn numeric(mut self, range: RangeInclusive<f64>, step: f64) -> Self {
102        self.numeric_config = Some((range, step));
103        self
104    }
105
106    /// Sets the AppKit control size preset (`Regular`, `Small`, `Mini`, `Large`, `CompactInfobox`).
107    pub fn control_size(mut self, control_size: MacControlSize) -> Self {
108        self.control_size = control_size;
109        self.explicit_control_size = true;
110        self
111    }
112
113    /// Pins the input cell to an exact NIB rectangle.
114    ///
115    /// When `.control_size(...)` was not explicitly overridden, `MacControlSize` is automatically
116    /// inferred from `rect.height()` (`>= 21.0pt` -> `Regular`, `18.5..21.0pt` -> `Small`,
117    /// `17.0..18.5pt` -> `CompactInfobox`, `< 17.0pt` -> `Mini`).
118    pub fn fixed_rect(mut self, rect: Rect) -> Self {
119        self.fixed_rect = Some(rect);
120        self
121    }
122
123    /// Sets horizontal text alignment inside the cell (`Align::Min` left or `Align::Max` right).
124    pub fn text_align(mut self, align: Align) -> Self {
125        self.text_align = align;
126        self
127    }
128
129    /// Returns the resolved [`MacControlSize`] for this cell (accounting for `.fixed_rect(rect)` auto-detection).
130    pub fn resolved_control_size(&self) -> MacControlSize {
131        if self.explicit_control_size {
132            return self.control_size;
133        }
134        let Some(rect) = self.fixed_rect else {
135            return self.control_size;
136        };
137        let theme_cow = MacTheme::resolve_or_default(self.theme);
138        let tf = theme_cow.text_field();
139        let h = rect.height();
140        if h >= tf.auto_size_regular_min_height.points() {
141            MacControlSize::Regular
142        } else if h >= tf.auto_size_small_min_height.points() {
143            MacControlSize::Small
144        } else if h >= tf.auto_size_compact_min_height.points() {
145            MacControlSize::CompactInfobox
146        } else {
147            MacControlSize::Mini
148        }
149    }
150
151    /// Enables or disables user interaction on the input cell.
152    pub fn enabled(mut self, enabled: bool) -> Self {
153        self.enabled = enabled;
154        self
155    }
156
157    /// Injects dynamic theme to eliminate hardcoded metrics and colors.
158    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
159        self.theme = Some(theme);
160        self
161    }
162
163    /// Overrides cell width in points.
164    pub fn width(mut self, width: f32) -> Self {
165        self.width = Some(width);
166        self
167    }
168
169    /// Overrides cell height in points (defaults to token text field height).
170    pub fn height(mut self, height: f32) -> Self {
171        self.height = Some(height);
172        self
173    }
174
175    /// Overrides font size in points.
176    pub fn font_size(mut self, font_size: f32) -> Self {
177        self.font_size = Some(font_size);
178        self
179    }
180
181    /// Overrides corner radius in points.
182    pub fn corner_radius(mut self, radius: f32) -> Self {
183        self.corner_radius = Some(radius);
184        self
185    }
186
187    /// Sets placeholder text displayed when the text field is empty and unfocused.
188    pub fn placeholder(mut self, text: &'a str) -> Self {
189        self.placeholder = Some(text);
190        self
191    }
192
193    /// Sets the deterministic Tab navigation target.
194    pub fn next_focus(mut self, id: Id) -> Self {
195        self.next_focus_id = Some(id);
196        self
197    }
198
199    /// Sets the deterministic Shift+Tab navigation target.
200    pub fn prev_focus(mut self, id: Id) -> Self {
201        self.prev_focus_id = Some(id);
202        self
203    }
204
205    /// Renders the input cell into `ui` and handles keyboard/focus interaction.
206    pub fn show(self, ui: &mut Ui) -> Response {
207        editing::show_input_cell(self, ui)
208    }
209}