diff --git a/crates/engine/src/tests_settings.rs b/crates/engine/src/tests_settings.rs index 3d5d127f..6ac160d1 100644 --- a/crates/engine/src/tests_settings.rs +++ b/crates/engine/src/tests_settings.rs @@ -649,7 +649,7 @@ fn interface_language_is_validated_persisted_and_backward_compatible() { assert_eq!(s.execute("prefs.get", json!({"key": "general.language"})).unwrap(), json!("pt-br")); s.execute("prefs.set", json!({"key": "general.language", "value": "ja"})).unwrap(); let saved = store.read(PREFS_FILE).unwrap(); - for bad in [json!("fr"), json!(""), json!(17), json!(null)] { + for bad in [json!("xx"), json!(""), json!(17), json!(null)] { assert!(s.execute("prefs.set", json!({"key": "general.language", "value": bad})).is_err()); assert_eq!(s.prefs.general.language, "ja"); assert_eq!(store.read(PREFS_FILE).unwrap(), saved); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 087e6dd1..cd56b0bc 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1122,8 +1122,10 @@ impl<'a> Renderer<'a> { // #659: an authored layer's buffer spans only its painted content while the stack works // on the layer's whole region (`layer_size`): give the buffer the room a sized source // already has, so tiling or displacement can place pixels outside the content instead - // of losing them at its edge. Only when something will actually run. - if fx.groups().any(|g| g.enabled && matches!(&g.kind, GroupKind::Effect { effect } if effectcraft_effects::find(effect).is_some_and(|s| !effectcraft_effects::audio_fx::is_audio_effect(s.id)))) { + // of losing them at its edge. Only when something will actually run, and not for an + // adjustment layer, whose buffer is the comp below and already spans it. + if !adjustment + && fx.groups().any(|g| g.enabled && matches!(&g.kind, GroupKind::Effect { effect } if effectcraft_effects::find(effect).is_some_and(|s| !effectcraft_effects::audio_fx::is_audio_effect(s.id)))) { target.cover(layer_size); } let mut mask_shapes = masks::shapes(ctx, layer); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 2b1217b6..611a4542 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -4009,6 +4009,12 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[ /// Russian (addition): the same (command, English label) keys as `JAPANESE`, one /// entry per fixed menu entry, in the same order. const RUSSIAN: &[(&str, &str, &str)] = &[ + ("comp.copyFrame", "Copy Frame to Clipboard", "Копировать кадр в буфер обмена"), + ("", "Front", "Передняя грань"), + ("", "Bevel", "Фаска"), + ("", "Side", "Боковая грань"), + ("", "Back", "Задняя грань"), + ("layer.addTextAnimator", "Color", "Цвет"), ("", "EffectCraft", "EffectCraft"), ("app.about", "About EffectCraft...", "О EffectCraft..."), ("", "Settings...", "Настройки..."), @@ -4632,6 +4638,12 @@ const RUSSIAN: &[(&str, &str, &str)] = &[ /// French (addition): the same (command, English label) keys as `JAPANESE`, one /// entry per fixed menu entry, in the same order. const FRENCH: &[(&str, &str, &str)] = &[ + ("comp.copyFrame", "Copy Frame to Clipboard", "Copier l'image dans le presse-papiers"), + ("", "Front", "Avant"), + ("", "Bevel", "Biseau"), + ("", "Side", "Côté"), + ("", "Back", "Arrière"), + ("layer.addTextAnimator", "Color", "Couleur"), ("", "EffectCraft", "EffectCraft"), ("app.about", "About EffectCraft...", "À propos d’EffectCraft..."), ("", "Settings...", "Préférences..."), @@ -5255,6 +5267,12 @@ const FRENCH: &[(&str, &str, &str)] = &[ /// German (addition): the same (command, English label) keys as `JAPANESE`, one /// entry per fixed menu entry, in the same order. const GERMAN: &[(&str, &str, &str)] = &[ + ("comp.copyFrame", "Copy Frame to Clipboard", "Frame in die Zwischenablage kopieren"), + ("", "Front", "Vorderseite"), + ("", "Bevel", "Abschrägung"), + ("", "Side", "Seite"), + ("", "Back", "Rückseite"), + ("layer.addTextAnimator", "Color", "Farbe"), ("", "EffectCraft", "EffectCraft"), ("app.about", "About EffectCraft...", "Über EffectCraft..."), ("", "Settings...", "Voreinstellungen..."), diff --git a/crates/ui-egui/src/panels/effect_controls.rs b/crates/ui-egui/src/panels/effect_controls.rs index dab3f8a7..d8de61d8 100644 --- a/crates/ui-egui/src/panels/effect_controls.rs +++ b/crates/ui-egui/src/panels/effect_controls.rs @@ -1127,6 +1127,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { if let Some(mut value) = editing.as_ref().cloned() { let name_rect = Rect::from_min_max(pos2(tw.max.x + 6.0, r.min.y + 2.0), pos2(reset.min.x - 6.0, r.max.y - 2.0)); let response = ui.put(name_rect, egui::TextEdit::singleline(&mut value).id(rename_id.with("field")).font(Tokens::semibold(12.0))); + // Focus the field the frame it first exists: focusing an id not yet on screen leaves + // the accessibility tree with a focus it doesn't contain. + if ctx.data_mut(|d| d.remove_temp::(rename_id.with("start"))).is_some() { + crate::widgets::select_all(&ctx, response.id, &value); + response.request_focus(); + } app.auto.add(&format!("effectControls.effect.{}.nameEdit", g.uid), name_rect, "Effect name"); // Escape cancels. egui drops the field's focus on Escape in the same frame, so the // key is checked on the focus being lost as well as held. @@ -1170,7 +1176,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { ); if hresp.double_clicked() { ctx.data_mut(|d| d.insert_temp(rename_id, g.name.clone())); - ctx.memory_mut(|m| m.request_focus(rename_id.with("field"))); + ctx.data_mut(|d| d.insert_temp(rename_id.with("start"), true)); } hresp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, active, &g.name)); app.auto.add(&format!("effectControls.effect.{}", g.uid), r, &g.name); @@ -1202,7 +1208,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { item(ui, "Duplicate", true, "effect.duplicate", json!({"layer": lid, "effect": g.uid})); if ui.button(crate::i18n::tr("Rename")).clicked() { ctx.data_mut(|d| d.insert_temp(rename_id, g.name.clone())); - ctx.memory_mut(|m| m.request_focus(rename_id.with("field"))); + ctx.data_mut(|d| d.insert_temp(rename_id.with("start"), true)); ui.close(); } ui.separator(); diff --git a/crates/ui-egui/src/panels/markers_ui.rs b/crates/ui-egui/src/panels/markers_ui.rs index 7eef7a3b..edf46f4e 100644 --- a/crates/ui-egui/src/panels/markers_ui.rs +++ b/crates/ui-egui/src/panels/markers_ui.rs @@ -504,6 +504,14 @@ mod tests { // Enter is OK and Cmd+Backspace is Delete. let ctx = egui::Context::default(); crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app)); + // The dialog opens with Comment focused (#922), a multi-line field where Enter types a + // new line; with the focus elsewhere, Enter is OK. + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { + app.auto.begin_frame(); + crate::panels::dialogs::show(&mut app, ui.ctx()); + }); + out.textures_delta.clear(); + ctx.memory_mut(|m| m.stop_text_input()); app.dialog_state.marker.chapter = "Entered".into(); crate::panels::dialogs::press_key(&mut app, &ctx, egui::Key::Enter, egui::Modifiers::NONE); assert!(app.dialog.is_none()); diff --git a/crates/ui-egui/tests/ui_effect_controls.rs b/crates/ui-egui/tests/ui_effect_controls.rs index 0bd5f526..3251aed0 100644 --- a/crates/ui-egui/tests/ui_effect_controls.rs +++ b/crates/ui-egui/tests/ui_effect_controls.rs @@ -528,7 +528,7 @@ fn radial_center_stays_draggable_when_its_property_is_selected() { #[test] fn clicking_an_effect_parameter_activates_its_header_and_handles() { let (app, x) = app(); - let mut h = Harness::builder().with_size(egui::vec2(1700.0, 1100.0)).build_eframe(|_| app); + let mut h = Harness::builder().with_size(egui::vec2(1700.0, 1100.0)).with_step_dt(1.0 / 60.0).build_eframe(|_| app); settle(&mut h); let name = rect_of(&h, &format!("effectControls.prop.{}.name", x.point)).center(); click(&mut h, name); @@ -660,12 +660,11 @@ fn file_and_text_parameters_have_widgets() { #[test] fn effect_control_name_can_be_renamed_from_its_header() { let (app, x) = app(); - let mut h = Harness::builder().with_size(egui::vec2(1700.0, 1100.0)).build_eframe(|_| app); + let mut h = Harness::builder().with_size(egui::vec2(1700.0, 1100.0)).with_step_dt(1.0 / 60.0).build_eframe(|_| app); settle(&mut h); let name = format!("effectControls.effect.{}.name", x.point_fx); let point = rect_of(&h, &name).center(); - click(&mut h, point); - click(&mut h, point); + double_click(&mut h, point); h.run_steps(3); assert!(ids(&h).contains(&format!("effectControls.effect.{}.nameEdit", x.point_fx))); h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); @@ -683,11 +682,10 @@ fn effect_control_name_can_be_renamed_from_its_header() { #[test] fn effect_rename_escape_keeps_the_name() { let (app, x) = app(); - let mut h = Harness::builder().with_size(egui::vec2(1700.0, 1100.0)).build_eframe(|_| app); + let mut h = Harness::builder().with_size(egui::vec2(1700.0, 1100.0)).with_step_dt(1.0 / 60.0).build_eframe(|_| app); settle(&mut h); let point = rect_of(&h, &format!("effectControls.effect.{}.name", x.point_fx)).center(); - click(&mut h, point); - click(&mut h, point); + double_click(&mut h, point); h.run_steps(3); assert!(ids(&h).contains(&format!("effectControls.effect.{}.nameEdit", x.point_fx))); h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); @@ -699,3 +697,14 @@ fn effect_rename_escape_keeps_the_name() { assert_eq!(name, "Point Control"); assert!(!ids(&h).contains(&format!("effectControls.effect.{}.nameEdit", x.point_fx))); } + +/// Two presses within egui's double-click time (one frame apart). +fn double_click(h: &mut Harness<'_, EffectcraftApp>, pos: egui::Pos2) { + h.event(egui::Event::PointerMoved(pos)); + h.step(); + for pressed in [true, false, true, false] { + h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }); + h.step(); + } + h.run_steps(2); +} diff --git a/crates/ui-egui/tests/ui_toolbar_gradient.rs b/crates/ui-egui/tests/ui_toolbar_gradient.rs index 575a4b3e..b5682ffa 100644 --- a/crates/ui-egui/tests/ui_toolbar_gradient.rs +++ b/crates/ui-egui/tests/ui_toolbar_gradient.rs @@ -28,7 +28,14 @@ fn setup(items: &[&str], tool: serde_json::Value, select: bool) -> (Harness<'sta for kind in items { s.execute("layer.addShapeItem", json!({"layer": l, "kind": kind})).unwrap(); } - s.execute("shape.toolOptions", tool).unwrap(); + s.execute("shape.toolOptions", tool.clone()).unwrap(); + // The toolbar shows a selected shape layer's own Fill / Stroke kind (#732), so the layer gets + // the kind too. + if select && let Some(o) = tool.as_object() { + let mut p = o.clone(); + p.insert("layers".into(), json!([l])); + s.execute("shape.fillStroke", serde_json::Value::Object(p)).unwrap(); + } s.state.selected_layers = if select { vec![LayerId(l)] } else { vec![] }; s.state.selected_props.clear(); let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s)); @@ -104,16 +111,14 @@ fn a_gradient_stroke_swatch_edits_the_gradient_stroke() { assert_eq!((target.layer, target.prop), (l, uid), "on the Gradient Stroke's Colors"); } -/// With no shape layer selected, or one without that gradient, the swatch opens the options (the -/// kind new shapes get) as before. +/// With no shape layer selected the swatch opens the options (the kind new shapes get) as before. +/// (A selected shape layer shows its own kind, #732, so a gradient kind always has its gradient.) #[test] fn without_a_selected_gradient_the_swatch_opens_the_options() { - for (items, select) in [(vec![], true), (vec!["gfill"], false)] { - let (mut h, _) = setup(&items, json!({"fillType": "linear"}), select); - click(&mut h, "header.fill", Modifiers::NONE); - assert_eq!(h.state().dialog, None, "{items:?} selected={select}: no editor"); - assert!(options_open(&h, "fill"), "{items:?} selected={select}: Fill Options"); - } + let (mut h, _) = setup(&["gfill"], json!({"fillType": "linear"}), false); + click(&mut h, "header.fill", Modifiers::NONE); + assert_eq!(h.state().dialog, None, "no editor"); + assert!(options_open(&h, "fill"), "Fill Options"); } /// The rest of the toolbar is unchanged: the words open the options, a Solid Color swatch opens the