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