mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-26 02:59:17 -06:00
LR-BEHAV-TEXTFIELD: a slider's typed value is a shared text field
The value typed after clicking a slider's number gets the Cut / Copy / Paste / Select All menu (`sliderField:<control>`), and using the menu no longer closes the field: it used to close as soon as anything took the focus. Selecting the old value and Esc now come from the shared field; `TextFieldResponse::editing` says an edit is under way, the menu included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4efd92b38f
commit
7baf93ecdb
@@ -2005,6 +2005,28 @@ mod tests {
|
||||
assert_eq!(h.app.session.undo.len(), undo0 + 1);
|
||||
}
|
||||
|
||||
/// A slider's typed value is a shared text field: right-click ▸ Paste replaces the value
|
||||
/// (selected when the field opens) and Return applies it; the menu doesn't close the field.
|
||||
#[test]
|
||||
fn a_slider_value_pastes_from_its_menu() {
|
||||
let mut h = demo([1300.0, 900.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
h.request("ui.set", json!({"view": "detail", "right": "edit"}), t);
|
||||
h.settle(SETTLE);
|
||||
h.view.clipboard = "0.75".into();
|
||||
for (id, button) in
|
||||
[("sliderValue:light.exposure", "left"), ("sliderField:light.exposure", "right"), ("sliderField:light.exposure:paste", "left")]
|
||||
{
|
||||
let r = h.request("ui.clickWidget", json!({"id": id, "button": button}), t);
|
||||
assert_eq!(r["ok"], true, "{id}: {r}");
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
h.request("ui.key", json!({"key": "Enter"}), t);
|
||||
h.settle(SETTLE);
|
||||
let exposure = h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().light.exposure;
|
||||
assert!((exposure - 0.75).abs() < 1e-9, "{exposure}");
|
||||
}
|
||||
|
||||
/// The eye on a section header switches the section off and on again, one undo step each
|
||||
/// (issue #316).
|
||||
#[test]
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct TextFieldResponse {
|
||||
pub response: Response,
|
||||
/// Set on the frame an edit ends.
|
||||
pub ending: Option<Ending>,
|
||||
/// An edit is under way: the field has the focus, or its menu does.
|
||||
pub editing: bool,
|
||||
}
|
||||
|
||||
impl TextFieldResponse {
|
||||
@@ -221,8 +223,9 @@ impl<'a> TextField<'a> {
|
||||
if ending.is_some() {
|
||||
memo = Memo::default();
|
||||
}
|
||||
let editing = ending.is_none() && (response.has_focus() || memo.away || memo.menu_open || memo.action.is_some());
|
||||
ui.data_mut(|d| d.insert_temp(id.with("memo"), memo));
|
||||
TextFieldResponse { response, ending }
|
||||
TextFieldResponse { response, ending, editing }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +181,6 @@ pub fn slider(ui: &mut Ui, spec: &ControlSpec, value: f64, enabled: bool, label_
|
||||
let value_resp = ui.interact(value_rect, id.with("value"), if enabled { Sense::click() } else { Sense::hover() });
|
||||
register(ui.ctx(), format!("sliderValue:{}", spec.id), value_rect);
|
||||
let typing_id = id.with("typing");
|
||||
let field_id = id.with("typingField");
|
||||
// the text being typed and how many frames the field has been up: it takes the keyboard on its
|
||||
// first frames (not on the click's own, whose release would take the focus straight back)
|
||||
let mut typing: Option<(String, u8)> = ui.data(|m| m.get_temp(typing_id));
|
||||
@@ -194,28 +193,23 @@ pub fn slider(ui: &mut Ui, spec: &ControlSpec, value: f64, enabled: bool, label_
|
||||
let from_x = |x: f32| spec.min + ((x - track_rect.left()) / track_rect.width()).clamp(0.0, 1.0) as f64 * span;
|
||||
let mut v = value;
|
||||
if let Some((mut text, frames)) = typing.take() {
|
||||
let field = egui::TextEdit::singleline(&mut text)
|
||||
.id(field_id)
|
||||
.font(t.font(12.5))
|
||||
.horizontal_align(egui::Align::RIGHT)
|
||||
.desired_width(value_rect.width())
|
||||
.margin(egui::Margin::ZERO);
|
||||
// a child over the value, so the field never moves the rows around it
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(value_rect).layout(egui::Layout::right_to_left(egui::Align::Center)));
|
||||
let te = child.add(field);
|
||||
// the old value is selected when the field takes the focus, so typing replaces it
|
||||
let field = crate::text_field::TextField::singleline(&format!("sliderField:{}", spec.id), &mut text)
|
||||
.font(t.font(12.5))
|
||||
.horizontal_align(egui::Align::RIGHT)
|
||||
.width(value_rect.width())
|
||||
.margin(egui::Margin::ZERO)
|
||||
.select_on_focus(true)
|
||||
.show(&mut child);
|
||||
if frames < 2 {
|
||||
te.request_focus();
|
||||
// the old value is selected, so typing replaces it (set again once the field has the
|
||||
// focus, which places the cursor)
|
||||
let mut state = egui::text_edit::TextEditState::load(ui.ctx(), field_id).unwrap_or_default();
|
||||
let all = egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(text.chars().count()));
|
||||
state.cursor.set_char_range(Some(all));
|
||||
state.store(ui.ctx(), field_id);
|
||||
field.response.request_focus();
|
||||
typing = Some((text.clone(), frames + 1));
|
||||
ui.data_mut(|m| m.insert_temp(typing_id, (text, frames + 1)));
|
||||
} else if !te.has_focus() || !enabled {
|
||||
} else if field.ending.is_some() || !field.editing || !enabled {
|
||||
// Esc (or the slider turning off) keeps the old value; Return or clicking away applies
|
||||
let cancelled = !enabled || ui.input(|i| i.key_pressed(egui::Key::Escape));
|
||||
let cancelled = !enabled || field.cancelled();
|
||||
if let Some(nv) = typed_value(spec, &text).filter(|nv| !cancelled && (nv - value).abs() > 1e-12) {
|
||||
out.value = Some(nv);
|
||||
out.drag_started = true;
|
||||
|
||||
Reference in New Issue
Block a user