mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Merge pull request #2431 from dotCooCoo/feat/shift-enter-keeps-field
Fields: Shift+Enter applies a panel field's value and keeps the field active
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user