mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 11:07:54 -06:00
UI: dialogs open with their first text field focused and selected
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
67434e9038
commit
8e1d742a9d
@@ -177,6 +177,7 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tr("Composition Name:"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(300.0));
|
||||
crate::widgets::focus_on_open(ui.ctx(), &r, &d.name);
|
||||
auto.add("dialog.comp.name", r.rect, "Composition Name");
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
|
||||
@@ -353,6 +353,7 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tr("Name:"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut name).desired_width(320.0));
|
||||
crate::widgets::focus_on_open(ui.ctx(), &r, &name);
|
||||
auto.add("dialog.solid.name", r.rect, "Name");
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
@@ -628,6 +629,32 @@ mod tests {
|
||||
assert_eq!(app.session.project.comps().count(), comps + 1, "Single Composition is the default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dialog_opens_with_its_name_field_focused_and_selected() {
|
||||
let (mut app, ctx) = app();
|
||||
let selected = |ctx: &egui::Context| {
|
||||
let id = ctx.memory(|m| m.focused())?;
|
||||
egui::TextEdit::load_state(ctx, id)?.cursor.char_range().map(|r| r.as_sorted_char_range()).map(|r| r.start.0..r.end.0)
|
||||
};
|
||||
open_new_comp(&mut app);
|
||||
let name = app.dialog_state.comp.name.clone();
|
||||
frame(&mut app, &ctx);
|
||||
frame(&mut app, &ctx);
|
||||
assert_eq!(selected(&ctx), Some(0..name.chars().count()), "typing replaces “{name}”");
|
||||
// Afterwards the focus is the user's: it is not taken back.
|
||||
ctx.memory_mut(|m| m.stop_text_input());
|
||||
frame(&mut app, &ctx);
|
||||
assert!(ctx.memory(|m| m.focused()).is_none());
|
||||
// Closed and opened again: focused again.
|
||||
app.dialog = None;
|
||||
frame(&mut app, &ctx);
|
||||
frame(&mut app, &ctx);
|
||||
open_new_solid(&mut app).unwrap();
|
||||
frame(&mut app, &ctx);
|
||||
frame(&mut app, &ctx);
|
||||
assert_eq!(selected(&ctx), Some(0..app.dialog_state.solid_name.chars().count()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_settings_register_every_control_and_new_comp_remembers() {
|
||||
let (mut app, ctx) = app();
|
||||
|
||||
@@ -173,7 +173,8 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tr("Name"));
|
||||
ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(240.0));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(240.0));
|
||||
crate::widgets::focus_on_open(ui.ctx(), &r, &d.name);
|
||||
ui.end_row();
|
||||
ui.label(tr("Preset"));
|
||||
let focal = focal_for_zoom(w, d.zoom);
|
||||
@@ -258,7 +259,8 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
super::dialogs::modal(ctx, "Light Settings", vec2(480.0, 470.0), t, |ui| {
|
||||
egui::Grid::new("light-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
|
||||
ui.label(tr("Name"));
|
||||
ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(220.0));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(220.0));
|
||||
crate::widgets::focus_on_open(ui.ctx(), &r, &d.name);
|
||||
ui.end_row();
|
||||
ui.label(tr("Light Type"));
|
||||
egui::ComboBox::from_id_salt("light-type").selected_text(KINDS[d.kind.min(3)].label()).show_ui(ui, |ui| {
|
||||
|
||||
@@ -766,6 +766,7 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let h = 120.0 + rows;
|
||||
let mut regs: Vec<(String, egui::Rect, String)> = vec![];
|
||||
let mut browse: Option<usize> = None;
|
||||
let mut focused = false;
|
||||
// Wide enough for a label column and a 240 px popup without a horizontal scroll bar.
|
||||
super::dialogs::modal(ctx, &f.title.clone(), vec2(480.0, h), t, |ui| {
|
||||
egui::Grid::new("form-grid").num_columns(2).spacing([14.0, 10.0]).show(ui, |ui| {
|
||||
@@ -776,7 +777,14 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let dec = if *speed < 0.01 { 5 } else { 3 };
|
||||
ui.add(crate::widgets::drag_value(egui::DragValue::new(value).speed(*speed).max_decimals(dec))).rect
|
||||
}
|
||||
FieldKind::Text(s) => ui.add(egui::TextEdit::singleline(s).desired_width(220.0)).rect,
|
||||
FieldKind::Text(s) => {
|
||||
let r = ui.add(egui::TextEdit::singleline(s).desired_width(220.0));
|
||||
// The first text field takes the keyboard when the form opens.
|
||||
if !std::mem::replace(&mut focused, true) {
|
||||
crate::widgets::focus_on_open(ui.ctx(), &r, s);
|
||||
}
|
||||
r.rect
|
||||
}
|
||||
FieldKind::Path { value, .. } => {
|
||||
ui.horizontal(|ui| {
|
||||
let r = ui.add(egui::TextEdit::singleline(value).desired_width(150.0)).rect;
|
||||
|
||||
@@ -137,6 +137,7 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
ui.end_row();
|
||||
ui.label(tr("Comment:"));
|
||||
let r = ui.add(egui::TextEdit::multiline(&mut d.comment).desired_rows(3).desired_width(320.0));
|
||||
crate::widgets::focus_on_open(ui.ctx(), &r, &d.comment);
|
||||
app.auto.add("dialog.marker.comment", r.rect, "Comment");
|
||||
ui.end_row();
|
||||
ui.label(tr("Chapter:"));
|
||||
|
||||
@@ -115,6 +115,7 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tr("New composition name:"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(260.0));
|
||||
crate::widgets::focus_on_open(ui.ctx(), &r, &d.name);
|
||||
app.auto.add("dialog.precompose.name", r.rect, "New composition name");
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
|
||||
@@ -420,6 +420,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
// ---- search + command list + editor
|
||||
ui.horizontal(|ui| {
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut st.query).hint_text(tr("Search commands or shortcuts")).desired_width(360.0));
|
||||
crate::widgets::focus_on_open(ui.ctx(), &r, &st.query);
|
||||
app.auto.add("shortcuts.search", r.rect, "Search");
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
|
||||
@@ -187,6 +187,24 @@ pub fn keep_focus(ctx: &egui::Context, field: &Response, text: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Put the keyboard on a dialog's first text field the frame the dialog opens, with its whole
|
||||
/// `text` selected, so typing replaces the value without a click. Unlike [`keep_focus`] the
|
||||
/// focus is the user's afterwards.
|
||||
pub fn focus_on_open(ctx: &egui::Context, field: &Response, text: &str) {
|
||||
// The dialog is open in consecutive passes; a gap means it was closed and opened again.
|
||||
let shown = field.id.with("focus-on-open-pass");
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
let previous = ctx.data_mut(|d| {
|
||||
let previous = d.get_temp::<u64>(shown);
|
||||
d.insert_temp(shown, pass);
|
||||
previous
|
||||
});
|
||||
if previous.is_none_or(|p| p.saturating_add(1) < pass) {
|
||||
select_all(ctx, field.id, text);
|
||||
field.request_focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Select all of the text field `id`'s `text`, so typing replaces it.
|
||||
pub fn select_all(ctx: &egui::Context, id: egui::Id, text: &str) {
|
||||
let mut state = egui::TextEdit::load_state(ctx, id).unwrap_or_default();
|
||||
|
||||
Reference in New Issue
Block a user