Fields: Shift+Enter applies a panel field's value and keeps the field active

Shift+Enter in a panel or Control bar field ended its editing, as Enter
does. The reference app's default keyboard shortcuts list "Apply value
and keep text box active" under Use panels and "Apply a value and keep
focus in edit field" under Transform panel, both Shift+Enter on Windows
and Shift+Return on macOS.

- A focused single-line field of the shared widgets (num_field,
  mixed_field, plain_field, range_field, spin_field, spin_field_auto,
  spin_plain, text_field, exact_text_field, opt_field and hex_field)
  takes Shift+Enter before its text box reads it, applies the text as
  Enter does and keeps the focus. In the next frame it shows the value
  as it now is, all selected, so typing replaces it.
- A text box of several lines keeps Shift+Enter as a line break.
- While a dialog is open, Shift+Enter applies the field's value and
  confirms the dialog, as before. The app sets widgets::set_dialog_open
  at the start of each frame.

Docs: docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-17 14:46:20 +07:00
committed by bflatastic
parent 877aac130f
commit 0e26b258eb
5 changed files with 234 additions and 28 deletions
+62
View File
@@ -632,6 +632,68 @@ fn a_dialog_opens_with_its_first_field_focused_so_typing_and_enter_apply() {
assert_eq!(node.appearance.effects.last().map(|e| e.params["scaleH"].clone()), Some(json!(50.0)));
}
/// In a dialog, Shift+Enter in a field applies its value and confirms the dialog, as Enter does:
/// the Move dialog moves the art, and the New Document dialog creates a document as wide as typed.
/// Panel fields keep the focus on Shift+Enter (`tests_numfields`).
#[test]
fn shift_enter_in_a_dialog_field_applies_it_and_confirms_the_dialog() {
let mut app = app();
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 20})).unwrap()["id"].as_u64().unwrap();
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let screen_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1280.0, 900.0)));
let mut time = 0.0;
// Whole app frames: the panels, then the dialog.
let mut frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
time += 0.1;
let mut out = ctx.run_ui(egui::RawInput { events, time: Some(time), screen_rect, ..Default::default() }, |ui| {
app.logic(ui.ctx());
app.ui(ui);
});
out.textures_delta.clear();
out
};
let shift_enter =
|| egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::SHIFT };
let bounds = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap();
// Move…: type 15 over its first field and press Shift+Enter.
let before = bounds(&app);
crate::menus::invoke(&mut app, "object.move", json!({}));
frame(&mut app, vec![]);
frame(&mut app, vec![]);
frame(&mut app, vec![egui::Event::Text("15".into())]);
frame(&mut app, vec![shift_enter()]);
assert!(app.ui.dialog.is_none(), "Shift+Enter confirms the Move dialog");
let after = bounds(&app);
assert!((after.x0 - before.x0 - 15.0).abs() < 1e-6 && (after.y0 - before.y0).abs() < 1e-6, "{before:?} → {after:?}");
// New Document (the recent 200 × 200 pt size): click into Width, type 300 and press Shift+Enter.
app.run("file.newDialog", json!({})).unwrap();
frame(&mut app, vec![]);
let out = frame(&mut app, vec![]);
fn walk(s: &egui::Shape, out: &mut Vec<(String, egui::Pos2)>) {
match s {
egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, out)),
egui::Shape::Text(t) => out.push((t.galley.text().to_string(), t.visual_bounding_rect().center())),
_ => {}
}
}
let mut texts = vec![];
out.shapes.iter().for_each(|c| walk(&c.shape, &mut texts));
// The Width field comes right after its caption.
let width = texts.iter().position(|(t, _)| t == "Width").and_then(|i| texts.get(i + 1));
assert_eq!(width.map(|(t, _)| t.as_str()), Some("200 pt"), "{texts:?}");
let at = width.map(|(_, p)| *p).unwrap();
let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, vec![egui::Event::PointerMoved(at), press(true)]);
frame(&mut app, vec![press(false)]);
frame(&mut app, vec![]);
frame(&mut app, vec![egui::Event::Text("300".into())]);
frame(&mut app, vec![shift_enter()]);
assert!(app.ui.dialog.is_none(), "Shift+Enter creates the document");
assert_eq!(app.session.active().unwrap().doc.artboards[0].rect.width(), 300.0);
}
/// #1367: a dialog opened by a click (its menu item) is drawn in the frame of that click, where egui
/// takes the focus from a field the click missed: its first field still has the focus, each time it
/// opens.
+1
View File
@@ -1353,6 +1353,7 @@ impl VectorcraftApp {
return;
}
let t0 = now_ms();
widgets::set_dialog_open(&ctx, self.ui.dialog.is_some());
scrub::begin_frame(self, &ctx);
font_menu::end_stale_preview(self, &ctx);
floating::track(self, &ctx);
+88 -1
View File
@@ -2,7 +2,7 @@
//! on keyboard focus, a released focus click or double-click so typing replaces it; a drag
//! keeps the pressed character as its selection anchor.
use std::cell::Cell;
use std::cell::{Cell, RefCell};
use egui::{Event, Key, Modifiers, PointerButton, Pos2, Rect, vec2};
use vectorcraft_doc::Unit;
@@ -316,6 +316,93 @@ fn plain_fields_do_math_with_their_suffix() {
assert_eq!(f.frame(vec![enter()], &draw), Some(90.0));
}
/// Shift+Enter applies what is typed into a field and keeps the field active, its new value all
/// selected so typing replaces it; Enter then ends its editing. In a dialog, Shift+Enter ends the
/// editing as Enter does.
#[test]
fn shift_enter_applies_the_value_and_keeps_the_field_active() {
let shift_enter = || key(Key::Enter, Modifiers::SHIFT);
let has_focus = |f: &Field| f.ctx.memory(|m| m.has_focus(f.id.get()));
// A unit field, a plain field with a suffix and a field that may be empty, each applying what
// it returns. → (text typed, value applied, text shown afterwards).
let px = Cell::new(100.0);
let num = |ui: &mut egui::Ui| widgets::num_field(ui, "f", Some(px.get()), Unit::Pixels, 80.0).inspect(|&v| px.set(v));
let deg = Cell::new(10.0);
let plain = |ui: &mut egui::Ui| widgets::plain_field(ui, "f", deg.get(), "°", 2, 80.0).inspect(|&v| deg.set(v));
let dash = Cell::new(Some(20.0));
let opt = |ui: &mut egui::Ui| widgets::opt_field(ui, "f", dash.get(), Unit::Points, 60.0).map(|v| dash.set(v)).and(dash.get());
type Draw<'a> = &'a dyn Fn(&mut egui::Ui) -> Option<f64>;
let fields: [(&str, Draw, &str, f64, &str); 3] =
[("num_field", &num, "150", 150.0, "150 px"), ("plain_field", &plain, "45", 45.0, "45°"), ("opt_field", &opt, "2+3", 5.0, "5")];
for (name, draw, typed, want, shows) in fields {
let mut f = Field::new();
f.frame(vec![], draw);
f.frame(f.click(1), draw);
f.frame(vec![], draw);
f.frame(vec![Event::Text(typed.into())], draw);
assert_eq!(f.frame(vec![shift_enter()], draw), Some(want), "{name}: Shift+Enter applies the value");
assert!(has_focus(&f), "{name}: the field keeps the focus");
assert_eq!(f.frame(vec![], draw), None, "{name}: applied once");
assert_eq!(f.selection(), Some((0, shows.chars().count())), "{name}: the value shown is selected");
assert_eq!(f.ctx.data(|d| d.get_temp::<String>(f.id.get())).as_deref(), Some(shows), "{name}");
// Typing replaces it, and Enter applies that and ends the editing.
f.frame(vec![Event::Text("2".into())], draw);
assert_eq!(f.frame(vec![enter()], draw), Some(2.0), "{name}: Enter after it");
assert!(!has_focus(&f), "{name}: Enter ends the editing");
}
// A text field and a hex field (its color shown in capitals) the same way; a field of several
// lines takes Shift+Enter as a line break.
let (text, hex, hex_id) = (Cell::new(None), Cell::new(None), Cell::new(egui::Id::NULL));
let color = RefCell::new(String::from("FF8000"));
let draw = |ui: &mut egui::Ui| {
text.set(widgets::text_field(ui, "f", Some("Name"), 120.0, 1).or(text.take()));
hex_id.set(ui.id().with("hex"));
let shown = color.borrow().clone();
let applied = widgets::hex_field(ui, "hex", &shown);
if let Some(h) = &applied {
color.replace(h.to_uppercase());
}
hex.set(applied.or(hex.take()));
None
};
let mut f = Field::new();
f.frame(vec![], &draw);
f.frame(vec![key(Key::Tab, Modifiers::NONE)], &draw);
f.frame(vec![key(Key::A, Modifiers::COMMAND)], &draw);
f.frame(vec![Event::Text("Logo".into()), shift_enter()], &draw);
assert_eq!(text.take().as_deref(), Some("Logo"));
assert!(has_focus(&f), "the text field keeps the focus");
f.frame(vec![key(Key::Tab, Modifiers::NONE)], &draw);
f.frame(vec![key(Key::A, Modifiers::COMMAND)], &draw);
f.frame(vec![Event::Text("00ff00".into()), shift_enter()], &draw);
assert_eq!((text.take(), hex.take().as_deref()), (None, Some("00ff00")));
assert!(f.ctx.memory(|m| m.has_focus(hex_id.get())), "the hex field keeps the focus");
f.frame(vec![], &draw);
assert_eq!(f.ctx.data(|d| d.get_temp::<String>(hex_id.get())).as_deref(), Some("00FF00"), "the hex field shows the color applied");
let selected = egui::TextEdit::load_state(&f.ctx, hex_id.get()).and_then(|st| st.cursor.char_range()).map(|r| r.as_sorted_char_range());
assert_eq!(selected.map(|r| (r.start.0, r.end.0)), Some((0, 6)), "the hex field's color is selected");
let lines = Cell::new(None);
let draw = |ui: &mut egui::Ui| {
lines.set(widgets::text_field(ui, "f", Some(""), 120.0, 3).or(lines.take()));
None
};
let mut f = Field::new();
f.frame(vec![], &draw);
f.frame(f.click(1), &draw);
f.frame(vec![Event::Text("a".into()), shift_enter(), Event::Text("b".into())], &draw);
assert!(has_focus(&f) && lines.take().is_none(), "a line break, nothing applied");
f.frame(vec![key(Key::Escape, Modifiers::NONE)], &draw);
// In a dialog, Shift+Enter applies the value and ends the editing.
let mut f = Field::new();
widgets::set_dialog_open(&f.ctx, true);
px.set(100.0);
f.frame(vec![], &num);
f.frame(f.click(1), &num);
f.frame(vec![Event::Text("150".into())], &num);
assert_eq!(f.frame(vec![shift_enter()], &num), Some(150.0));
assert!(!has_focus(&f), "a dialog's field ends its editing");
}
/// Preferences › Units › Numbers Without Units Are Points (#394): on (the default), a number typed
/// with no unit into a field in picas is read in points; a typed unit still wins (`2p6`, `1in`);
/// off, it is in picas. A field in any other unit reads it in its unit either way.
+82 -27
View File
@@ -208,9 +208,9 @@ pub(crate) fn typed_unit(ctx: &egui::Context, unit: Unit) -> Unit {
}
/// A recessed numeric field showing `value` (points) in `unit`. Returns the new value (points)
/// when the user commits (Enter / focus loss). Supports unit suffixes and arithmetic; a number
/// without a unit is in `unit` ([`typed_unit`]: in points in a picas field with Numbers Without
/// Units Are Points on).
/// when the user commits (Enter / focus loss / Shift+Enter, which keeps the focus outside dialogs).
/// Supports unit suffixes and arithmetic; a number without a unit is in `unit` ([`typed_unit`]: in
/// points in a picas field with Numbers Without Units Are Points on).
pub fn num_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: Option<f64>, unit: Unit, width: f32) -> Option<f64> {
unit_field(ui, id, value, unit, width, false)
}
@@ -228,13 +228,13 @@ fn unit_field(
let id = ui.id().with(id);
let shown = value.map(|v| unit.format(v)).unwrap_or_default();
take_dialog_focus(ui, id, &shown);
let (mut buf, resp, rect) = recessed_text(ui, id, &shown, width, 1);
let (mut buf, commit, resp, rect) = recessed_text(ui, id, &shown, width, 1);
select_all_on_focus(ui, &resp, &buf);
// ↑/↓ and the wheel step in the field's unit; a drag on its label scrubs it.
let read = typed_unit(ui.ctx(), unit);
let stepped = step_value(ui, &resp, &mut buf, |b| Some(unit.from_pt(read.parse(b)?)), |v| unit.format(unit.to_pt(v)));
let scrubbed = scrub::field(ui, id, rect, &buf, value.map(|v| unit.from_pt(v)), STEP_DECIMALS);
let commit = resp.lost_focus() && buf != shown;
let commit = commit && buf != shown;
ui.data_mut(|d| d.insert_temp(id, buf.clone()));
if commit {
read.parse(&buf).or_else(|| if share_of_value { share_of(&buf, value?) } else { None })
@@ -384,6 +384,45 @@ fn select_all(ui: &Ui, id: egui::Id, text: &str) {
st.store(ui.ctx(), id);
}
/// The flag that tells the fields a dialog is open in this frame ([`set_dialog_open`]).
fn dialog_open_flag() -> egui::Id {
egui::Id::new("dialog-open")
}
/// The app sets this at the start of each frame: while a dialog is open, Shift+Enter in a field
/// applies its value and confirms the dialog as Enter does ([`shift_enter_keeps_focus`]).
pub(crate) fn set_dialog_open(ctx: &egui::Context, open: bool) {
ctx.data_mut(|d| d.insert_temp(dialog_open_flag(), open));
}
/// Shift+Enter (Shift+Return) in single-line field `id` while it has the keyboard focus and no
/// dialog is open: the press is taken from this frame's events before the field's text box reads
/// them, so the field keeps the focus. Returns true when it was pressed; the field then applies its
/// text as Enter does, and in the next frame shows its value with all of it selected
/// ([`after_shift_enter`]).
fn shift_enter_keeps_focus(ui: &Ui, id: egui::Id) -> bool {
if !ui.memory(|m| m.has_focus(id)) || ui.data(|d| d.get_temp::<bool>(dialog_open_flag())).unwrap_or(false) {
return false;
}
let pressed = ui.input_mut(|i| {
let before = i.events.len();
i.events.retain(|e| {
!matches!(e, egui::Event::Key { key: egui::Key::Enter, pressed: true, modifiers, .. } if modifiers.matches_exact(egui::Modifiers::SHIFT))
});
i.events.len() < before
});
if pressed {
ui.data_mut(|d| d.insert_temp(id.with("shift-enter"), true));
}
pressed
}
/// Whether field `id`, still focused, took Shift+Enter in the last frame
/// ([`shift_enter_keeps_focus`]). Clears that record.
fn after_shift_enter(ui: &Ui, id: egui::Id) -> bool {
ui.data_mut(|d| d.remove_temp::<bool>(id.with("shift-enter"))).unwrap_or(false) && ui.memory(|m| m.has_focus(id))
}
/// Numeric fields select the whole text on keyboard focus, a completed focus click or a
/// double-click. A pointer press keeps its caret so dragging selects from the pressed position,
/// and a short drag the toolkit still counts as a click keeps it too (#1326): only a tap
@@ -435,9 +474,10 @@ fn select_all_on_focus(ui: &Ui, resp: &Response, text: &str) {
}
/// The recessed text box of the panel fields showing `shown`, `rows` lines tall (1: single line),
/// its text kept under `id` while it has focus. Returns the text (as edited), the text edit's
/// response and the box.
fn recessed_text(ui: &mut Ui, id: egui::Id, shown: &str, width: f32, rows: usize) -> (String, Response, Rect) {
/// its text kept under `id` while it has focus. Returns the text (as edited), whether the field
/// applies it in this frame (it lost the focus, or a single-line field took Shift+Enter and kept
/// the focus: [`shift_enter_keeps_focus`]), the text edit's response and the box.
fn recessed_text(ui: &mut Ui, id: egui::Id, shown: &str, width: f32, rows: usize) -> (String, bool, Response, Rect) {
let t = Tokens::get(ui.ctx());
let editing = ui.memory(|m| m.has_focus(id));
// A press outside the field ends its editing in this frame, so the text typed is committed
@@ -460,8 +500,14 @@ fn recessed_text(ui: &mut Ui, id: egui::Id, shown: &str, width: f32, rows: usize
// `lost_focus`: the text typed is still taken then, to be committed (#1637). Escape drops it.
let focus_id = id.with("had-focus");
let had_focus = ui.data_mut(|d| d.get_temp::<bool>(focus_id)).unwrap_or(false) && !ui.input(|i| i.key_pressed(egui::Key::Escape));
let typed = if (editing || had_focus) && followed { ui.data_mut(|d| d.get_temp::<String>(id)) } else { None };
// After Shift+Enter applied the text, the field shows the value as it is now, all selected.
let reshown = after_shift_enter(ui, id);
let typed = if (editing || had_focus) && followed && !reshown { ui.data_mut(|d| d.get_temp::<String>(id)) } else { None };
let mut buf: String = typed.unwrap_or_else(|| shown.to_string());
if reshown {
select_all(ui, id, &buf);
}
let kept = rows == 1 && shift_enter_keeps_focus(ui, id);
// The box is exactly as tall as the other controls of a row (26 points a line), its text
// centred in it: a frame around the text came out a point taller and stood out next to
// spinners and dropdowns (#900).
@@ -496,25 +542,26 @@ fn recessed_text(ui: &mut Ui, id: egui::Id, shown: &str, width: f32, rows: usize
d.insert_temp(id, buf.clone());
d.insert_temp(focus_id, focused);
});
(buf, resp, rect)
let commit = resp.lost_focus() || kept;
(buf, commit, resp, rect)
}
/// A recessed text field showing `value` (blank when `None`: the selection's values differ),
/// `rows` lines tall (1: single line, which Enter commits). Returns the new text, trimmed, when a
/// change is committed (Enter or focus loss).
/// `rows` lines tall (1: single line, which Enter and Shift+Enter commit). Returns the new text,
/// trimmed, when a change is committed (Enter, Shift+Enter or focus loss).
pub fn text_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: Option<&str>, width: f32, rows: usize) -> Option<String> {
let id = ui.id().with(id);
let shown = value.unwrap_or_default();
let (buf, resp, _) = recessed_text(ui, id, shown, width, rows);
(resp.lost_focus() && buf.trim() != shown).then(|| buf.trim().to_string())
let (buf, commit, _, _) = recessed_text(ui, id, shown, width, rows);
(commit && buf.trim() != shown).then(|| buf.trim().to_string())
}
/// A one-line [`text_field`] that keeps the text exactly as typed, spaces included (a tab
/// leader such as ". "). Returns it when a change is committed (Enter or focus loss).
/// leader such as ". "). Returns it when a change is committed (Enter, Shift+Enter or focus loss).
pub fn exact_text_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: &str, width: f32) -> Option<String> {
let id = ui.id().with(id);
let (buf, resp, _) = recessed_text(ui, id, value, width, 1);
(resp.lost_focus() && buf != value).then_some(buf)
let (buf, commit, _, _) = recessed_text(ui, id, value, width, 1);
(commit && buf != value).then_some(buf)
}
/// A form row: `label` in a column `label_width` wide, then what `add` draws.
@@ -572,14 +619,14 @@ pub fn mixed_field(
};
let shown = value.map(show).unwrap_or_default();
take_dialog_focus(ui, id, &shown);
let (mut buf, resp, rect) = recessed_text(ui, id, &shown, width, 1);
let (mut buf, commit, resp, rect) = recessed_text(ui, id, &shown, width, 1);
select_all_on_focus(ui, &resp, &buf);
// ↑/↓ and the wheel step at the field's precision (a count ignores Ctrl/Cmd's tenth); a drag on
// its label scrubs it.
let decimals = decimals.min(6) as i32;
let stepped = step_value(ui, &resp, &mut buf, read, show).map(|v| round_to(v, decimals));
let scrubbed = scrub::field(ui, id, rect, &buf, value, decimals);
if resp.lost_focus() && buf != shown { read(&buf) } else { stepped.or(scrubbed).filter(|&v| Some(v) != value) }
if commit && buf != shown { read(&buf) } else { stepped.or(scrubbed).filter(|&v| Some(v) != value) }
}
/// Draw a paint preview (swatch chip) into `rect`.
@@ -1672,7 +1719,8 @@ pub fn channel_slider_named(
}
/// A hex colour field (`E67828`, no `#`) after a `#` label. Returns the text on commit when it
/// was typed into the field and differs from `hex`.
/// was typed into the field and differs from `hex`. Enter, Shift+Enter (which keeps the focus
/// outside dialogs) and focus loss commit it.
pub fn hex_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, hex: &str) -> Option<String> {
let t = Tokens::get(ui.ctx());
ui.label(egui::RichText::new("#").size(15.0).color(t.text));
@@ -1685,6 +1733,10 @@ pub fn hex_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, hex: &
let edited_id = id.with("edited");
let edited = editing && ui.data_mut(|d| d.get_temp::<bool>(edited_id)).unwrap_or(false);
let mut buf: String = if edited { ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| hex.to_string()) } else { hex.to_string() };
if after_shift_enter(ui, id) {
select_all(ui, id, &buf);
}
let kept = shift_enter_keeps_focus(ui, id);
let resp = egui::Frame::NONE
.fill(t.input)
.stroke(Stroke::new(1.0, if editing { t.accent } else { t.input_border }))
@@ -1704,11 +1756,11 @@ pub fn hex_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, hex: &
)
})
.inner;
let commit = resp.lost_focus() && buf != hex;
let commit = (resp.lost_focus() || kept) && buf != hex;
let focused = ui.memory(|m| m.has_focus(id));
ui.data_mut(|d| {
d.insert_temp(id, buf.clone());
d.insert_temp(edited_id, (edited || resp.changed()) && focused);
d.insert_temp(edited_id, (edited || resp.changed()) && focused && !kept);
});
commit.then_some(buf)
}
@@ -1779,8 +1831,9 @@ pub fn menu_item_name(ui: &mut Ui, label: &str, enabled: bool, checked: bool) ->
}
/// A number field that may be empty (Stroke dash/gap): `value` (points) shows in `unit` without
/// its suffix, to fit narrow fields. Returns `Some(new)` (points) on commit, where `new` is `None`
/// when the field was cleared; typed units and arithmetic work as in [`num_field`].
/// its suffix, to fit narrow fields. Returns `Some(new)` (points) on commit (Enter, Shift+Enter or
/// focus loss, as in [`num_field`]), where `new` is `None` when the field was cleared; typed units
/// and arithmetic work as in [`num_field`].
pub fn opt_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: Option<f64>, unit: Unit, width: f32) -> Option<Option<f64>> {
let t = Tokens::get(ui.ctx());
let id = ui.id().with(id);
@@ -1793,9 +1846,11 @@ pub fn opt_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value:
let edited = editing && ui.data_mut(|d| d.get_temp::<bool>(edited_id)).unwrap_or(false);
let before = ui.data_mut(|d| d.get_temp::<String>(id));
let mut buf: String = if edited { before.clone().unwrap_or_else(|| shown.clone()) } else { shown.clone() };
if editing && !edited && before.is_some_and(|b| b != buf) {
let reshown = after_shift_enter(ui, id);
if reshown || editing && !edited && before.is_some_and(|b| b != buf) {
select_all(ui, id, &buf);
}
let kept = shift_enter_keeps_focus(ui, id);
let enabled = ui.is_enabled();
let framed = egui::Frame::NONE
.fill(t.input)
@@ -1820,9 +1875,9 @@ pub fn opt_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value:
let focused = ui.memory(|m| m.has_focus(id));
ui.data_mut(|d| {
d.insert_temp(id, buf.clone());
d.insert_temp(edited_id, (edited || resp.changed()) && focused);
d.insert_temp(edited_id, (edited || resp.changed()) && focused && !kept);
});
if resp.lost_focus() && buf != shown {
if (resp.lost_focus() || kept) && buf != shown {
let s = buf.trim();
if s.is_empty() { Some(None) } else { typed_unit(ui.ctx(), unit).parse(s).map(Some) }
} else {
File diff suppressed because one or more lines are too long