mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #907 from tylerjenningsw/enhance/toolbar-gradient-swatch
Shapes: the toolbar's gradient swatches open the Gradient Editor (#612) # Conflicts: # docs/gaps.md
This commit is contained in:
commit
e0b770a9ea
3 files changed
+171
-3
No files matched your search
@@ -12,7 +12,7 @@ use crate::widgets;
|
||||
use crate::{Dialog, EffectcraftApp};
|
||||
use effectcraft_color::BlendMode;
|
||||
use effectcraft_engine::commands::shape_tool::{PaintKind, ToolPaint};
|
||||
use effectcraft_engine::project::{LayerId, LayerSource};
|
||||
use effectcraft_engine::project::{LayerId, LayerSource, PropGroup};
|
||||
use effectcraft_engine::viewer as vw;
|
||||
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
@@ -329,6 +329,24 @@ fn tool_options(app: &mut EffectcraftApp, v: serde_json::Value) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Colors property of the first Gradient Fill (`gfill`) or Gradient Stroke (`gstroke`) on the
|
||||
/// shape `layers`, in contents order through nested groups: (layer, property uid). The toolbar's
|
||||
/// gradient swatch edits it in the Gradient Editor.
|
||||
fn gradient_colors(app: &EffectcraftApp, layers: &[u64], paint: &str) -> Option<(u64, u64)> {
|
||||
fn find(g: &PropGroup, paint: &str, depth: usize) -> Option<u64> {
|
||||
if depth >= 16 {
|
||||
return None;
|
||||
}
|
||||
g.groups().filter(|c| c.enabled).find_map(|c| match c.match_id.as_str() {
|
||||
m if m == paint => c.get("colors").map(|p| p.uid),
|
||||
"group" | "contents" => find(c, paint, depth + 1),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
let comp = app.session.active_comp()?;
|
||||
layers.iter().find_map(|&l| comp.layer(LayerId(l)).and_then(|layer| layer.props.sub("contents")).and_then(|c| find(c, paint, 0)).map(|u| (l, u)))
|
||||
}
|
||||
|
||||
/// The colour picker of the Fill or Stroke swatch.
|
||||
fn picker_id(key: &str) -> egui::Id {
|
||||
egui::Id::new(("tool-pick", key))
|
||||
@@ -381,8 +399,9 @@ fn fill_stroke_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Pa
|
||||
if wresp.clicked() {
|
||||
widgets::open_popup(ui, opts);
|
||||
}
|
||||
// The swatch picks a Solid Color's colour and opens the options for the other kinds;
|
||||
// Alt-click steps through the kinds (as in After Effects).
|
||||
// The swatch picks a Solid Color's colour, edits a Linear or Radial Gradient's colours in
|
||||
// the Gradient Editor (the selected shape layer's Gradient Fill / Gradient Stroke), and
|
||||
// opens the options otherwise; Alt-click steps through the kinds (as in After Effects).
|
||||
let sr = Rect::from_min_size(pos2(wr.max.x + 6.0, cy - 8.0), vec2(20.0, 16.0));
|
||||
let sresp = ui.interact(sr, egui::Id::new(("tool-paint-swatch", key)), Sense::click()).on_hover_text(format!("{title}: {}", tp.kind.label()));
|
||||
paint_swatch(p, sr, &tp, sresp.hovered(), &t);
|
||||
@@ -394,7 +413,16 @@ fn fill_stroke_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Pa
|
||||
paint_kind(app, shape_layers, key, next);
|
||||
} else if tp.kind == PaintKind::Solid {
|
||||
widgets::open_popup(ui, picker_id(key));
|
||||
} else if let Some((layer, prop)) = matches!(tp.kind, PaintKind::Linear | PaintKind::Radial)
|
||||
.then(|| gradient_colors(app, shape_layers, if key == "fill" { "gfill" } else { "gstroke" }))
|
||||
.flatten()
|
||||
{
|
||||
let ctx = ui.ctx().clone();
|
||||
if let Err(e) = crate::menus::invoke(app, &ctx, "prop.editGradient", json!({"layer": layer, "prop": prop})) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
} else {
|
||||
// No selected shape layer with that gradient: its options (new shapes' kind).
|
||||
widgets::open_popup(ui, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//! The toolbar's Fill / Stroke swatch with Linear or Radial Gradient (#612, reported on Discord):
|
||||
//! it opens the Gradient Editor on the selected shape layer's Gradient Fill / Gradient Stroke
|
||||
//! Colors, as in After Effects, instead of Fill / Stroke Options. The words still open the
|
||||
//! options, a Solid Color swatch the colour picker, Alt-click still steps through the kinds, and
|
||||
//! with no such gradient selected the swatch opens the options as before.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::keyframe::Value;
|
||||
use effectcraft_engine::project::{LayerId, PropGroup};
|
||||
use effectcraft_ui_egui::state::Tool;
|
||||
use effectcraft_ui_egui::{Dialog, EffectcraftApp};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::json;
|
||||
|
||||
/// The Colors uid of the first `paint` ("gfill" / "gstroke") group in `g`, through nested groups.
|
||||
fn colors(g: &PropGroup, paint: &str) -> Option<u64> {
|
||||
g.groups().find_map(|x| if x.match_id == paint { x.get("colors").map(|p| p.uid) } else { colors(x, paint) })
|
||||
}
|
||||
|
||||
/// A rectangle shape layer (selected unless `select` is false) with `items` added to its
|
||||
/// contents, and the toolbar set with `tool` (shape.toolOptions).
|
||||
fn setup(items: &[&str], tool: serde_json::Value, select: bool) -> (Harness<'static, EffectcraftApp>, u64) {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 640, "height": 360, "duration": 4})).unwrap();
|
||||
let l = s.execute("layer.newShape", json!({"kind": "rect"})).unwrap()["layer"].as_u64().unwrap();
|
||||
for kind in items {
|
||||
s.execute("layer.addShapeItem", json!({"layer": l, "kind": kind})).unwrap();
|
||||
}
|
||||
s.execute("shape.toolOptions", tool).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));
|
||||
// With no shape layer selected, Fill / Stroke show while a shape tool is active.
|
||||
if !select {
|
||||
h.state_mut().ui.tool = Tool::Rectangle;
|
||||
}
|
||||
h.run_steps(4);
|
||||
(h, l)
|
||||
}
|
||||
|
||||
fn center(h: &Harness<'_, EffectcraftApp>, id: &str) -> Pos2 {
|
||||
let r = h.state().auto.find(id).unwrap_or_else(|| panic!("no automation id {id}")).rect;
|
||||
pos2(r[0] + r[2] / 2.0, r[1] + r[3] / 2.0)
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str, modifiers: Modifiers) {
|
||||
let p = center(h, id);
|
||||
h.input_mut().events.push(Event::ModifiersChanged(modifiers));
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers });
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: false, modifiers });
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn layer_colors(h: &Harness<'_, EffectcraftApp>, l: u64, paint: &str) -> u64 {
|
||||
colors(&h.state().session.active_comp().unwrap().layer(LayerId(l)).unwrap().props, paint).expect("the gradient")
|
||||
}
|
||||
|
||||
/// The options popup is open (its kind buttons are in the automation tree).
|
||||
fn options_open(h: &Harness<'_, EffectcraftApp>, key: &str) -> bool {
|
||||
h.state().auto.find(&format!("header.{key}Options.solid")).is_some()
|
||||
}
|
||||
|
||||
/// The reported bug: a Linear Gradient Fill swatch opened Fill Options. It opens the Gradient
|
||||
/// Editor on the selected layer's Gradient Fill Colors, and OK sets them.
|
||||
#[test]
|
||||
fn a_gradient_fill_swatch_opens_the_gradient_editor() {
|
||||
let (mut h, l) = setup(&["gfill"], json!({"fillType": "linear"}), true);
|
||||
let uid = layer_colors(&h, l, "gfill");
|
||||
click(&mut h, "header.fill", Modifiers::NONE);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::GradientEditor), "the swatch opens the Gradient Editor, not Fill Options");
|
||||
assert!(!options_open(&h, "fill"), "and not the options");
|
||||
let target = h.state().gradient_draft().target.clone().expect("a target");
|
||||
assert_eq!((target.layer, target.prop), (l, uid), "on the layer's Gradient Fill Colors");
|
||||
// Edit and save it: the property really changes.
|
||||
let bar = h.state().auto.find("dialog.gradient.addColor").unwrap().rect;
|
||||
let at = pos2(bar[0] + bar[2] * 0.5, bar[1] + bar[3] / 2.0);
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
click(&mut h, "dialog.gradient.ok", Modifiers::NONE);
|
||||
let v = h.state().session.active_comp().unwrap().layer(LayerId(l)).unwrap().props.find(uid).unwrap().value.clone();
|
||||
let Value::Gradient(g) = v else { panic!("not a gradient: {v:?}") };
|
||||
assert_eq!(g.colors.len(), 3, "the added stop was saved: {g:?}");
|
||||
}
|
||||
|
||||
/// The Stroke swatch with Radial Gradient edits the Gradient Stroke's Colors (not the fill's).
|
||||
#[test]
|
||||
fn a_gradient_stroke_swatch_edits_the_gradient_stroke() {
|
||||
let (mut h, l) = setup(&["gfill", "gstroke"], json!({"strokeType": "radial"}), true);
|
||||
let uid = layer_colors(&h, l, "gstroke");
|
||||
click(&mut h, "header.stroke", Modifiers::NONE);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::GradientEditor));
|
||||
let target = h.state().gradient_draft().target.clone().expect("a target");
|
||||
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.
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
||||
/// The rest of the toolbar is unchanged: the words open the options, a Solid Color swatch opens the
|
||||
/// colour picker, and Alt-click steps to the next kind.
|
||||
#[test]
|
||||
fn words_solid_swatches_and_alt_click_are_unchanged() {
|
||||
let (mut h, _) = setup(&["gfill"], json!({"fillType": "linear"}), true);
|
||||
click(&mut h, "header.fillOptions", Modifiers::NONE);
|
||||
assert!(options_open(&h, "fill"), "the word opens Fill Options");
|
||||
assert_eq!(h.state().dialog, None);
|
||||
let (mut h, _) = setup(&["gfill"], json!({"fillType": "solid"}), true);
|
||||
click(&mut h, "header.fill", Modifiers::NONE);
|
||||
assert!(h.query_by_label("Color field").is_some(), "a Solid Color swatch opens the colour picker");
|
||||
assert_eq!(h.state().dialog, None);
|
||||
let (mut h, _) = setup(&["gfill"], json!({"fillType": "linear"}), true);
|
||||
click(&mut h, "header.fill", Modifiers::ALT);
|
||||
assert_eq!(h.state().dialog, None, "Alt-click doesn't open the editor");
|
||||
assert_eq!(h.state().session.state.shape_tool.fill.kind.id(), "radial", "it steps Linear → Radial");
|
||||
}
|
||||
@@ -145,6 +145,11 @@ The most important missing piece: it turns every other estimate here into a meas
|
||||
tests, checked headlessly on Windows, drag a selected Gradient Fill's start and end points in
|
||||
the Composition viewer: one undo step, the layer stays, a point inside a moved and scaled group
|
||||
converts to the group's space, and the handles show only for a selected gradient.
|
||||
- Regression evidence for the toolbar's gradient swatches (#612, reported on Discord): the
|
||||
`ui_toolbar_gradient` input tests, checked headlessly on Windows, click a Linear Fill swatch and
|
||||
a Radial Stroke swatch on a selected shape layer and get the Gradient Editor on its Gradient Fill
|
||||
/ Gradient Stroke Colors (OK saves); without such a gradient the swatch still opens the options,
|
||||
and the words, Solid swatches and Alt-click are unchanged.
|
||||
- Regression evidence for the reports of 6 October, checked headlessly on Windows: real pointer
|
||||
drags in `ui_drag_drop` (Project items into the Timeline between layers and at a time, and onto
|
||||
the viewer; an effect onto a layer in the viewer: #85, #88, #89), `ui_render_queue` (a template
|
||||
|
||||
Reference in new issue
Block a user