1use crate::theme::MacTheme;
7use egui::{CornerRadius, Key, Modifiers, Response, Stroke, StrokeKind, Ui, Vec2};
8use std::ops::RangeInclusive;
9
10#[derive(Clone, Debug, Default)]
11struct InputCellTransactionState {
12 active: bool,
13 snapshot: String,
14 buffer: String,
15}
16
17fn decimal_places_if_valid(s: &str) -> usize {
18 let t = s.trim();
19 if t.parse::<f64>().is_ok() {
20 t.split_once('.')
21 .map_or(0, |(_, f)| f.split(['e', 'E']).next().unwrap_or("").len())
22 } else {
23 0
24 }
25}
26
27fn format_stepped_number(reference: &str, fallback_ref: &str, step: f64, val: f64) -> String {
28 let ref_decimals =
29 decimal_places_if_valid(reference).max(decimal_places_if_valid(fallback_ref));
30 let step_decimals = if (step - step.round()).abs() > f64::EPSILON {
31 format!("{step}")
32 .split_once('.')
33 .map_or(1, |(_, f)| f.len())
34 } else {
35 0
36 };
37 let prec = ref_decimals.max(step_decimals);
38 format!("{val:.prec$}")
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum MacControlSize {
44 Regular,
46 Small,
48 Mini,
50 Large,
52 #[default]
54 CompactInfobox,
55}
56
57pub struct MacInputCell<'a> {
80 id: egui::Id,
81 value: &'a mut String,
82 theme: Option<&'a MacTheme>,
83 width: Option<f32>,
84 height: Option<f32>,
85 font_size: Option<f32>,
86 corner_radius: Option<f32>,
87 control_size: MacControlSize,
88 enabled: bool,
89 next_focus_id: Option<egui::Id>,
90 prev_focus_id: Option<egui::Id>,
91 placeholder: Option<&'a str>,
92 numeric_config: Option<(RangeInclusive<f64>, f64)>,
93}
94
95impl<'a> MacInputCell<'a> {
96 pub fn new(id_source: impl std::hash::Hash + std::fmt::Debug, value: &'a mut String) -> Self {
98 let id = egui::Id::NULL.with(id_source);
99 Self::with_id(id, value)
100 }
101
102 pub fn with_id(id: egui::Id, value: &'a mut String) -> Self {
104 Self {
105 id,
106 value,
107 theme: None,
108 width: None,
109 height: None,
110 font_size: None,
111 corner_radius: None,
112 control_size: MacControlSize::CompactInfobox,
113 enabled: true,
114 next_focus_id: None,
115 prev_focus_id: None,
116 placeholder: None,
117 numeric_config: None,
118 }
119 }
120
121 pub fn numeric(mut self, range: RangeInclusive<f64>, step: f64) -> Self {
124 self.numeric_config = Some((range, step));
125 self
126 }
127
128 pub fn control_size(mut self, control_size: MacControlSize) -> Self {
130 self.control_size = control_size;
131 self
132 }
133
134 pub fn enabled(mut self, enabled: bool) -> Self {
136 self.enabled = enabled;
137 self
138 }
139
140 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
142 self.theme = Some(theme);
143 self
144 }
145
146 pub fn width(mut self, width: f32) -> Self {
148 self.width = Some(width);
149 self
150 }
151
152 pub fn height(mut self, height: f32) -> Self {
154 self.height = Some(height);
155 self
156 }
157
158 pub fn font_size(mut self, font_size: f32) -> Self {
160 self.font_size = Some(font_size);
161 self
162 }
163
164 pub fn placeholder(mut self, text: &'a str) -> Self {
166 self.placeholder = Some(text);
167 self
168 }
169
170 pub fn next_focus(mut self, id: egui::Id) -> Self {
172 self.next_focus_id = Some(id);
173 self
174 }
175
176 pub fn prev_focus(mut self, id: egui::Id) -> Self {
178 self.prev_focus_id = Some(id);
179 self
180 }
181
182 pub fn show(self, ui: &mut Ui) -> Response {
184 let theme = MacTheme::resolve(self.theme, ui.ctx());
185
186 let tf_tokens = &theme.control_metrics().text_field;
187 let metrics = match self.control_size {
188 MacControlSize::Regular => &tf_tokens.regular,
189 MacControlSize::Small => &tf_tokens.small,
190 MacControlSize::Mini => &tf_tokens.mini,
191 MacControlSize::Large => &tf_tokens.large,
192 MacControlSize::CompactInfobox => &tf_tokens.compact_infobox,
193 };
194
195 let height = self.height.unwrap_or_else(|| metrics.height.points());
196 let width = self.width.unwrap_or_else(|| metrics.default_width.points());
197 let font_size = self.font_size.unwrap_or_else(|| metrics.font_size.points());
198 let corner_radius = self
199 .corner_radius
200 .unwrap_or_else(|| metrics.corner_radius.points());
201
202 let desired_size = Vec2::new(width, height);
203 let sense = if self.enabled {
204 egui::Sense::click_and_drag()
205 } else {
206 egui::Sense::hover()
207 };
208 let (rect, mut response) = ui.allocate_exact_size(desired_size, sense);
209
210 if self.enabled && (response.clicked() || response.has_focus()) {
211 ui.memory_mut(|m| m.request_focus(self.id));
212 }
213
214 let is_focused = self.enabled && ui.memory(|m| m.has_focus(self.id));
215 let alpha_factor = if self.enabled {
216 1.0
217 } else {
218 theme.disabled_opacity()
219 };
220
221 let tx_key = self.id.with("mac_input_tx_state");
223 let (mut tx, injected_buf) = ui.data_mut(|d| {
224 (
225 d.get_temp::<InputCellTransactionState>(tx_key),
226 d.remove_temp::<String>(self.id.with("mac_input_buffer")),
227 )
228 });
229 let mut tx_dirty = false;
230 let was_active = tx.as_ref().is_some_and(|s| s.active);
231
232 let esc_pressed = (is_focused || was_active)
233 && (ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape))
234 || ui.input(|i| {
235 i.raw.events.iter().any(|e| {
236 matches!(
237 e,
238 egui::Event::Key {
239 key: Key::Escape,
240 pressed: true,
241 ..
242 }
243 )
244 })
245 }));
246
247 if is_focused && !was_active && !esc_pressed {
248 let initial = self.value.clone();
249 tx = Some(InputCellTransactionState {
250 active: true,
251 snapshot: initial.clone(),
252 buffer: initial,
253 });
254 tx_dirty = true;
255 } else if !is_focused && was_active && !esc_pressed {
256 if let Some(ref mut st) = tx {
257 *self.value = st.buffer.clone();
258 st.active = false;
259 tx_dirty = true;
260 }
261 }
262 if let Some(override_buf) = injected_buf {
263 let st = tx.get_or_insert_with(|| InputCellTransactionState {
264 active: is_focused,
265 snapshot: self.value.clone(),
266 buffer: String::new(),
267 });
268 st.buffer = override_buf;
269 tx_dirty = true;
270 }
271
272 if is_focused || was_active || self.next_focus_id.is_some() || self.prev_focus_id.is_some()
274 {
275 ui.memory_mut(|m| m.move_focus(egui::FocusDirection::None));
276 }
277
278 let mut focus_transition_to: Option<egui::Id> = None;
280 let mut surrender_now = false;
281
282 if esc_pressed {
283 if let Some(ref mut st) = tx {
284 *self.value = st.snapshot.clone();
285 st.buffer = st.snapshot.clone();
286 st.active = false;
287 tx_dirty = true;
288 response.mark_changed();
289 }
290 ui.memory_mut(|m| m.surrender_focus(self.id));
291 surrender_now = true;
292 } else if is_focused {
293 if let Some((ref range, step)) = self.numeric_config {
294 let stepper_tokens = &theme.control_metrics().stepper;
295 let shift_mult = stepper_tokens.shift_multiplier.points() as f64;
296 let cmd_mult = stepper_tokens.cmd_multiplier.points() as f64;
297 let num_delta =
298 super::stepper::consume_step_arrow_mult(ui, Key::ArrowUp, cmd_mult, shift_mult)
299 .map(|m| step * m)
300 .or_else(|| {
301 super::stepper::consume_step_arrow_mult(
302 ui,
303 Key::ArrowDown,
304 cmd_mult,
305 shift_mult,
306 )
307 .map(|m| -step * m)
308 });
309 if let Some(delta) = num_delta {
310 let cur_str = tx
311 .as_ref()
312 .map(|s| s.buffer.as_str())
313 .unwrap_or(self.value.as_str());
314 let snap_str = tx
315 .as_ref()
316 .map(|s| s.snapshot.as_str())
317 .unwrap_or(self.value.as_str());
318 let cur_val = cur_str
319 .trim()
320 .parse::<f64>()
321 .or_else(|_| snap_str.trim().parse::<f64>())
322 .unwrap_or(*range.start());
323 let next_val = (cur_val + delta).clamp(*range.start(), *range.end());
324 let formatted = format_stepped_number(cur_str, snap_str, step, next_val);
325 *self.value = formatted.clone();
326 let st = tx.get_or_insert_with(InputCellTransactionState::default);
327 st.buffer = formatted.clone();
328 st.snapshot = formatted;
329 tx_dirty = true;
330 response.mark_changed();
331 }
332 }
333
334 let mut commit_and_surrender = |ui: &mut Ui, update_snapshot: bool| {
335 if let Some(ref mut st) = tx {
336 *self.value = st.buffer.clone();
337 st.active = false;
338 if update_snapshot {
339 st.snapshot = self.value.clone();
340 }
341 tx_dirty = true;
342 }
343 ui.memory_mut(|m| m.surrender_focus(self.id));
344 };
345
346 if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter)) {
347 commit_and_surrender(ui, true);
348 surrender_now = true;
349 } else if let Some(prev) = self
350 .prev_focus_id
351 .filter(|_| ui.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::Tab)))
352 {
353 commit_and_surrender(ui, false);
354 focus_transition_to = Some(prev);
355 } else if let Some(next) = self
356 .next_focus_id
357 .filter(|_| ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Tab)))
358 {
359 commit_and_surrender(ui, false);
360 focus_transition_to = Some(next);
361 }
362 }
363
364 let painter = ui.painter_at(rect);
365
366 if is_focused && !surrender_now {
368 theme.paint_focus_ring(&painter, rect, corner_radius);
369 }
370
371 let bg_color = theme.control_background().gamma_multiply(alpha_factor);
373 let border_stroke = if is_focused && !surrender_now {
374 Stroke::new(metrics.border_width.points(), theme.control_accent())
375 } else {
376 Stroke::new(
377 metrics.border_width.points(),
378 theme.separator().gamma_multiply(alpha_factor),
379 )
380 };
381
382 painter.rect(
383 rect,
384 CornerRadius::same(corner_radius as u8),
385 bg_color,
386 border_stroke,
387 StrokeKind::Inside,
388 );
389
390 let shrink_pad = metrics
392 .padding_horizontal
393 .as_ref()
394 .map(|d| d.points())
395 .unwrap_or_else(|| metrics.corner_radius.points());
396 let mut child_ui = ui.new_child(
397 egui::UiBuilder::new()
398 .id_salt((self.id, "input_cell_inner"))
399 .max_rect(rect.shrink2(Vec2::new(shrink_pad, 0.0)))
400 .layout(egui::Layout::left_to_right(egui::Align::Center)),
401 );
402
403 let mut edit_buf = if is_focused && !surrender_now {
404 tx.as_ref()
405 .map(|s| s.buffer.clone())
406 .unwrap_or_else(|| self.value.clone())
407 } else {
408 self.value.clone()
409 };
410
411 let mut text_edit = egui::TextEdit::singleline(&mut edit_buf)
412 .id(self.id)
413 .font(egui::FontId::proportional(font_size))
414 .text_color(theme.label().gamma_multiply(alpha_factor))
415 .desired_width((width - shrink_pad * 2.0).max(0.0))
416 .frame(egui::Frame::NONE)
417 .lock_focus(true)
418 .interactive(self.enabled);
419
420 if let Some(hint) = self.placeholder {
421 text_edit = text_edit.hint_text(hint);
422 }
423
424 let text_res = child_ui.add(text_edit);
425 if text_res.changed() && !surrender_now {
426 *self.value = edit_buf.clone();
427 let st = tx.get_or_insert_with(InputCellTransactionState::default);
428 st.buffer = edit_buf;
429 tx_dirty = true;
430 response.mark_changed();
431 }
432
433 if is_focused && !surrender_now {
434 ui.input_mut(|i| {
436 let _ = i.consume_key(Modifiers::NONE, Key::Backspace);
437 let _ = i.consume_key(Modifiers::NONE, Key::Delete);
438 let _ = i.consume_key(Modifiers::NONE, Key::Space);
439 let _ = i.consume_key(Modifiers::NONE, Key::ArrowLeft);
440 let _ = i.consume_key(Modifiers::NONE, Key::ArrowRight);
441 if !i.modifiers.command && !i.modifiers.ctrl && !i.modifiers.mac_cmd {
442 i.keys_down.clear();
443 }
444 i.events.retain(|ev| match ev {
445 egui::Event::Key { modifiers, .. } => {
446 modifiers.command || modifiers.ctrl || modifiers.mac_cmd
447 }
448 _ => true,
449 });
450 });
451 }
452
453 let post_focused = ui.memory(|m| m.has_focus(self.id));
454 if is_focused && !post_focused && !surrender_now && focus_transition_to.is_none() {
455 if let Some(ref mut st) = tx {
457 *self.value = st.buffer.clone();
458 st.active = false;
459 tx_dirty = true;
460 }
461 }
462
463 if tx_dirty {
464 if let Some(st) = tx {
465 ui.data_mut(|d| d.insert_temp(tx_key, st));
466 }
467 }
468
469 if surrender_now {
470 ui.memory_mut(|m| m.surrender_focus(self.id));
471 } else if let Some(target_id) = focus_transition_to {
472 ui.memory_mut(|m| {
473 m.move_focus(egui::FocusDirection::None);
474 m.request_focus(target_id);
475 });
476 } else if post_focused {
477 ui.memory_mut(|m| {
478 m.set_focus_lock_filter(
479 self.id,
480 egui::EventFilter {
481 tab: true,
482 horizontal_arrows: true,
483 vertical_arrows: true,
484 escape: true,
485 },
486 );
487 });
488 }
489
490 text_res.union(response).with_new_rect(rect)
491 }
492}