mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Fix tests and two behaviours after the community batch
- Renderer: no cover step for adjustment layers (#686); their buffer is the comp below. Restores a single CPU step in split adjustment stacks. - Effect Controls rename (#953): focus the name field the frame it exists. Focusing an id not yet drawn left AccessKit with a focus outside its tree (a panic in the consumer). The tests double-click at 60 fps, within egui's double-click time. - i18n: Russian, French and German entries for Copy Frame to Clipboard and the extrusion Front/Bevel/Side/Back Color submenus added by today's other PRs. - Tests: "fr" is now a valid language; the marker dialog's comment is focused on open (#922), so Enter-is-OK is tested with the focus elsewhere; the toolbar gradient tests give the selected layer its paint kind, since the toolbar mirrors it (#732). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bbc3a231c7
commit
61aaf83612
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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..."),
|
||||
|
||||
@@ -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::<bool>(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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user