use super::{MacControlSize, MacInputCell};
use crate::layout::{subpixel_snap, subpixel_snap_rect};
use crate::theme::MacTheme;
use egui::{Align, CornerRadius, FontId, Key, Modifiers, Pos2, Response, Stroke, StrokeKind, Ui, Vec2};
#[derive(Clone, Debug, Default)]
struct InputCellTransactionState {
active: bool,
snapshot: String,
buffer: String,
}
fn decimal_places_if_valid(s: &str) -> usize {
let t = s.trim();
if t.parse::<f64>().is_ok() {
t.split_once('.')
.map_or(0, |(_, f)| f.split(['e', 'E']).next().unwrap_or("").len())
} else {
0
}
}
fn format_stepped_number(reference: &str, fallback_ref: &str, step: f64, val: f64) -> String {
let ref_decimals =
decimal_places_if_valid(reference).max(decimal_places_if_valid(fallback_ref));
let step_decimals = if (step - step.round()).abs() > f64::EPSILON {
format!("{step}")
.split_once('.')
.map_or(1, |(_, f)| f.len())
} else {
0
};
let prec = ref_decimals.max(step_decimals);
format!("{val:.prec$}")
}
pub(super) fn show_input_cell(cell: MacInputCell<'_>, ui: &mut Ui) -> Response {
let resolved_size = cell.resolved_control_size();
let theme = MacTheme::resolve(cell.theme, ui.ctx());
let tf_tokens = &theme.control_metrics().text_field;
let metrics = match resolved_size {
MacControlSize::Regular => &tf_tokens.regular,
MacControlSize::Small => &tf_tokens.small,
MacControlSize::Mini => &tf_tokens.mini,
MacControlSize::Large => &tf_tokens.large,
MacControlSize::CompactInfobox => &tf_tokens.compact_infobox,
};
let height = cell.fixed_rect.map(|r| r.height()).or(cell.height).unwrap_or_else(|| metrics.height.points());
let width = cell.fixed_rect.map(|r| r.width()).or(cell.width).unwrap_or_else(|| metrics.default_width.points());
let font_size = cell.font_size.unwrap_or_else(|| metrics.font_size.points());
let corner_radius = cell.corner_radius.unwrap_or_else(|| metrics.corner_radius.points());
let sense = if cell.enabled { egui::Sense::click_and_drag() } else { egui::Sense::hover() };
let ppp = ui.ctx().pixels_per_point();
let (rect, mut response) = if let Some(fixed) = cell.fixed_rect {
let snapped = subpixel_snap_rect(fixed, ppp);
(snapped, ui.interact(snapped, cell.id.with("fixed_hit"), sense))
} else {
let (r, resp) = ui.allocate_exact_size(Vec2::new(width, height), sense);
(subpixel_snap_rect(r, ppp), resp)
};
if cell.enabled && (response.clicked() || response.has_focus()) {
ui.memory_mut(|m| m.request_focus(cell.id));
}
let is_focused = cell.enabled && ui.memory(|m| m.has_focus(cell.id));
let alpha_factor = if cell.enabled { 1.0 } else { theme.disabled_opacity() };
let tx_key = cell.id.with("mac_input_tx_state");
let (mut tx, injected_buf) = ui.data_mut(|d| {
(
d.get_temp::<InputCellTransactionState>(tx_key),
d.remove_temp::<String>(cell.id.with("mac_input_buffer")),
)
});
let mut tx_dirty = false;
let was_active = tx.as_ref().is_some_and(|s| s.active);
let esc_pressed = (is_focused || was_active)
&& (ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape))
|| ui.input(|i| i.raw.events.iter().any(|e| matches!(e, egui::Event::Key { key: Key::Escape, pressed: true, .. }))));
if is_focused && !was_active && !esc_pressed {
let initial = cell.value.clone();
tx = Some(InputCellTransactionState { active: true, snapshot: initial.clone(), buffer: initial });
tx_dirty = true;
} else if !is_focused && was_active && !esc_pressed {
if let Some(ref mut st) = tx {
*cell.value = st.buffer.clone();
st.active = false;
tx_dirty = true;
}
}
if let Some(override_buf) = injected_buf {
let st = tx.get_or_insert_with(|| InputCellTransactionState {
active: is_focused,
snapshot: cell.value.clone(),
buffer: String::new(),
});
st.buffer = override_buf;
tx_dirty = true;
}
if is_focused || was_active || cell.next_focus_id.is_some() || cell.prev_focus_id.is_some() {
ui.memory_mut(|m| m.move_focus(egui::FocusDirection::None));
}
let mut focus_transition_to: Option<egui::Id> = None;
let mut surrender_now = false;
if esc_pressed {
if let Some(ref mut st) = tx {
*cell.value = st.snapshot.clone();
st.buffer = st.snapshot.clone();
st.active = false;
tx_dirty = true;
response.mark_changed();
}
ui.memory_mut(|m| m.surrender_focus(cell.id));
surrender_now = true;
} else if is_focused {
if let Some((ref range, step)) = cell.numeric_config {
let stepper_tokens = &theme.control_metrics().stepper;
let shift_mult = stepper_tokens.shift_multiplier.points() as f64;
let cmd_mult = stepper_tokens.cmd_multiplier.points() as f64;
let num_delta = super::super::stepper::consume_step_arrow_mult(ui, Key::ArrowUp, cmd_mult, shift_mult)
.map(|m| step * m)
.or_else(|| super::super::stepper::consume_step_arrow_mult(ui, Key::ArrowDown, cmd_mult, shift_mult).map(|m| -step * m));
if let Some(delta) = num_delta {
let cur_str = tx.as_ref().map(|s| s.buffer.as_str()).unwrap_or(cell.value.as_str());
let snap_str = tx.as_ref().map(|s| s.snapshot.as_str()).unwrap_or(cell.value.as_str());
let cur_val = cur_str.trim().parse::<f64>().or_else(|_| snap_str.trim().parse::<f64>()).unwrap_or(*range.start());
let next_val = (cur_val + delta).clamp(*range.start(), *range.end());
let formatted = format_stepped_number(cur_str, snap_str, step, next_val);
*cell.value = formatted.clone();
let st = tx.get_or_insert_with(InputCellTransactionState::default);
st.buffer = formatted.clone();
st.snapshot = formatted;
tx_dirty = true;
response.mark_changed();
}
}
let mut commit_and_surrender = |ui: &mut Ui, update_snapshot: bool| {
if let Some(ref mut st) = tx {
*cell.value = st.buffer.clone();
st.active = false;
if update_snapshot {
st.snapshot = cell.value.clone();
}
tx_dirty = true;
}
ui.memory_mut(|m| m.surrender_focus(cell.id));
};
if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter)) {
commit_and_surrender(ui, true);
surrender_now = true;
} else if let Some(prev) = cell.prev_focus_id.filter(|_| ui.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::Tab))) {
commit_and_surrender(ui, false);
focus_transition_to = Some(prev);
} else if let Some(next) = cell.next_focus_id.filter(|_| ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Tab))) {
commit_and_surrender(ui, false);
focus_transition_to = Some(next);
}
}
let painter = ui.painter_at(rect);
if is_focused && !surrender_now {
theme.paint_focus_ring(&painter, rect, corner_radius);
}
let bg_color = theme.control_background().gamma_multiply(alpha_factor);
let border_stroke = if is_focused && !surrender_now {
Stroke::new(metrics.border_width.points(), theme.control_accent())
} else {
Stroke::new(metrics.border_width.points(), theme.separator().gamma_multiply(alpha_factor))
};
painter.rect(rect, CornerRadius::same(corner_radius as u8), bg_color, border_stroke, StrokeKind::Inside);
let shrink_pad = metrics.padding_horizontal.points();
let inner_rect = rect.shrink2(Vec2::new(shrink_pad, 0.0));
let mut child_ui = ui.new_child(
egui::UiBuilder::new()
.id_salt((cell.id, "input_cell_inner"))
.max_rect(inner_rect)
.layout(egui::Layout::left_to_right(Align::Center)),
);
let mut edit_buf = if is_focused && !surrender_now {
tx.as_ref().map(|s| s.buffer.clone()).unwrap_or_else(|| cell.value.clone())
} else {
cell.value.clone()
};
let font_id = FontId::proportional(font_size);
let text_color = theme.label().gamma_multiply(alpha_factor);
let hint_color = theme.placeholder_text().gamma_multiply(alpha_factor);
if !(is_focused && !surrender_now) {
let display_str = if cell.value.is_empty() { cell.placeholder.unwrap_or("") } else { cell.value.as_str() };
if !display_str.is_empty() {
let draw_color = if cell.value.is_empty() { hint_color } else { text_color };
let galley = theme.layout_tracked_galley(ui, display_str, font_id.clone(), draw_color, inner_rect.width().max(0.0));
let lm = theme.label_metrics();
let ascender = match resolved_size {
MacControlSize::Regular | MacControlSize::Large => lm.regular_ascender.points(),
MacControlSize::Small | MacControlSize::CompactInfobox => lm.small_ascender.points(),
MacControlSize::Mini => lm.mini_ascender.points(),
};
let text_y = subpixel_snap(rect.top() + metrics.first_baseline_offset_from_top.points() - ascender, ppp);
let text_x = match cell.text_align {
Align::Max => (inner_rect.right() - galley.size().x).max(inner_rect.left()),
Align::Center => inner_rect.center().x - galley.size().x * 0.5,
Align::Min => inner_rect.left(),
};
painter.with_clip_rect(inner_rect).galley(Pos2::new(text_x, text_y), galley, draw_color);
}
}
let theme_ref = &theme;
let layout_font = font_id.clone();
let mut layouter = |ui: &Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
theme_ref.layout_tracked_galley(ui, buf.as_str(), layout_font.clone(), text_color, wrap_width)
};
let text_res = if is_focused && !surrender_now {
let mut text_edit = egui::TextEdit::singleline(&mut edit_buf)
.id(cell.id)
.font(font_id)
.align(egui::Align2([cell.text_align, Align::Center]))
.text_color(text_color)
.desired_width((width - shrink_pad * 2.0).max(0.0))
.frame(egui::Frame::NONE)
.lock_focus(true)
.interactive(cell.enabled)
.layouter(&mut layouter);
if let Some(hint) = cell.placeholder {
text_edit = text_edit.hint_text(hint);
}
let r = child_ui.add(text_edit);
if r.changed() {
*cell.value = edit_buf.clone();
let st = tx.get_or_insert_with(InputCellTransactionState::default);
st.buffer = edit_buf;
tx_dirty = true;
response.mark_changed();
}
r
} else {
egui::text_edit::TextEditState::load(ui.ctx(), cell.id)
.unwrap_or_default()
.store(ui.ctx(), cell.id);
child_ui.interact(inner_rect, cell.id, sense)
};
if is_focused && !surrender_now {
ui.input_mut(|i| {
for k in [Key::Backspace, Key::Delete, Key::Space, Key::ArrowLeft, Key::ArrowRight] {
let _ = i.consume_key(Modifiers::NONE, k);
}
if !i.modifiers.command && !i.modifiers.ctrl && !i.modifiers.mac_cmd {
i.keys_down.clear();
}
i.events.retain(|ev| match ev {
egui::Event::Key { modifiers, .. } => modifiers.command || modifiers.ctrl || modifiers.mac_cmd,
_ => true,
});
});
}
let post_focused = ui.memory(|m| m.has_focus(cell.id));
if is_focused && !post_focused && !surrender_now && focus_transition_to.is_none() {
if let Some(ref mut st) = tx {
*cell.value = st.buffer.clone();
st.active = false;
tx_dirty = true;
}
}
if tx_dirty {
if let Some(st) = tx {
ui.data_mut(|d| d.insert_temp(tx_key, st));
}
}
if surrender_now {
ui.memory_mut(|m| m.surrender_focus(cell.id));
} else if let Some(target_id) = focus_transition_to {
ui.memory_mut(|m| {
m.move_focus(egui::FocusDirection::None);
m.request_focus(target_id);
});
} else if post_focused {
ui.memory_mut(|m| {
m.move_focus(egui::FocusDirection::None);
m.set_focus_lock_filter(cell.id, egui::EventFilter { tab: true, horizontal_arrows: true, vertical_arrows: true, escape: true });
});
}
response | text_res
}