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}