Merge pull request #744 from vangel76/fix-732-toolbar-fill-type

#732: toolbar Fill/Stroke Options change the selected shape layers' kind
This commit is contained in:
Brandon Thomas
2026-10-12 03:03:20 -04:00
5 changed files with 294 additions and 31 deletions
+174 -17
View File
@@ -3,13 +3,14 @@
//! commands here set several of them in one undo step and add the groups to strokes saved before
//! they existed. Also the toolbar's Fill / Stroke options on selected shape layers.
use effectcraft_keyframe::Value as KV;
use effectcraft_keyframe::{Gradient, Value as KV};
use effectcraft_project::build::{self, EXTRA_DASHES, Ids};
use effectcraft_project::{ItemId, Layer, LayerId, LayerSource, Node, PropGroup, Uid};
use effectcraft_time::Tick;
use serde_json::{Value, json};
use super::layer::color_p;
use super::shape_tool::{self, PaintKind};
use super::{CommandSpec, bad, f_p, has_layers, layer_mut, layer_p, layers_p, merge_p};
use crate::{EngineError, Result, Session, cmd};
@@ -224,15 +225,157 @@ pub fn first_paint(layer: &Layer, lt: Tick) -> (Option<[f64; 4]>, Option<[f64; 4
(fill, stroke, width)
}
/// Paint one contents group and its sub-groups: Fills take `fill`, Strokes `stroke`, Strokes and
/// Gradient Strokes `width`. With `add_stroke`, a group with a path and no stroke gets one (before
/// its fills, as new shapes have it).
#[allow(clippy::too_many_arguments)]
fn paint(c: &mut PropGroup, ids: &mut Ids, lt: Tick, fill: Option<[f64; 4]>, stroke: Option<[f64; 4]>, width: Option<f64>, add_stroke: bool, depth: usize) {
/// What the toolbar's Fill and Stroke set on the selected shape layers.
#[derive(Clone, Copy, Debug, Default)]
struct PaintEdit {
fill: Option<[f64; 4]>,
stroke: Option<[f64; 4]>,
width: Option<f64>,
/// A group with a path and no stroke gets one.
add_stroke: bool,
fill_kind: Option<PaintKind>,
stroke_kind: Option<PaintKind>,
}
/// The kind of the first Fill and the first Stroke in `layer`'s contents at layer time `lt`: a
/// switched-off or missing one is None (what the toolbar shows for a selected shape layer).
pub fn first_paint_kinds(layer: &Layer, lt: Tick) -> (PaintKind, PaintKind) {
let (mut fill, mut stroke) = (None, None);
let kind = |g: &PropGroup| match g.match_id.as_str() {
_ if !g.enabled => PaintKind::None,
"fill" | "stroke" => PaintKind::Solid,
_ if g.get("type").is_some_and(|p| p.value_at(lt).as_enum() == 1) => PaintKind::Radial,
_ => PaintKind::Linear,
};
if let Some(c) = layer.props.sub("contents") {
walk_groups(c, &mut |g| match g.match_id.as_str() {
"fill" | "gfill" if fill.is_none() => fill = Some(kind(g)),
"stroke" | "gstroke" if stroke.is_none() => stroke = Some(kind(g)),
_ => {}
});
}
(fill.unwrap_or(PaintKind::None), stroke.unwrap_or(PaintKind::None))
}
/// The bounds (`[x, y, w, h]`) of the path items of one contents level at layer time `lt`, where
/// a new gradient's Start and End Points go; a 100 px square at the origin without paths.
fn path_bounds(c: &PropGroup, lt: Tick) -> [f64; 4] {
let mut acc: Option<[f64; 4]> = None;
for g in c.groups() {
let at = |id: &str| g.get(id).map(|p| p.value_at(lt));
let b = match g.match_id.as_str() {
"rect" | "ellipse" => {
let [w, h] = at("size").map(|v| v.as_vec2()).unwrap_or([100.0; 2]);
let [x, y] = at("position").map(|v| v.as_vec2()).unwrap_or([0.0; 2]);
Some([x - w.abs() / 2.0, y - h.abs() / 2.0, x + w.abs() / 2.0, y + h.abs() / 2.0])
}
"star" => {
let r = at("outerRadius").map(|v| v.as_f64().abs()).unwrap_or(100.0);
let [x, y] = at("position").map(|v| v.as_vec2()).unwrap_or([0.0; 2]);
Some([x - r, y - r, x + r, y + r])
}
"path" => {
let v = at("path");
let bez = v.as_ref().and_then(|v| v.as_path()).map(effectcraft_path::to_kurbo);
bez.and_then(|b| effectcraft_path::bounds(&[b])).map(|r| [r.x0, r.y0, r.x1, r.y1])
}
_ => None,
};
let Some(b) = b.filter(|b| b.iter().all(|v| v.is_finite())) else { continue };
acc = Some(match acc {
None => b,
Some(a) => [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])],
});
}
acc.map(|[x0, y0, x1, y1]| [x0, y0, x1 - x0, y1 - y0]).unwrap_or([-50.0, -50.0, 100.0, 100.0])
}
/// Fill Options / Stroke Options on one contents level (as in After Effects, with shape layers
/// selected they change the layers' paint): None switches its Fills and Gradient Fills (or
/// Strokes and Gradient Strokes) off; another kind switches them on, turns them into that kind
/// (keeping the properties both kinds have: blend mode, opacity, width, dashes…) and gives a
/// level with paths and no such item one.
fn set_kind(c: &mut PropGroup, ids: &mut Ids, lt: Tick, stroke: bool, kind: PaintKind, e: &PaintEdit) {
let (solid, grad) = if stroke { ("stroke", "gstroke") } else { ("fill", "gfill") };
let is_item = |g: &PropGroup| g.match_id == solid || g.match_id == grad;
if kind == PaintKind::None {
for n in &mut c.children {
if let Node::Group(g) = n
&& is_item(g)
{
g.enabled = false;
}
}
return;
}
let radial = kind == PaintKind::Radial;
let color = (if stroke { e.stroke } else { e.fill }).unwrap_or([1.0; 4]);
let width = e.width.filter(|w| *w > 0.0).unwrap_or(2.0);
let (start, end) = shape_tool::gradient_ends(radial, path_bounds(c, lt));
let fresh = |ids: &mut Ids| match (kind, stroke) {
(PaintKind::Solid, false) => build::shape_fill(ids, color),
(PaintKind::Solid, true) => build::shape_stroke(ids, color, width),
(_, false) => build::shape_gradient_fill(ids, radial, start, end, Gradient::default()),
(_, true) => build::shape_gradient_stroke(ids, radial, start, end, Gradient::default(), width),
};
let want = if kind == PaintKind::Solid { solid } else { grad };
let mut found = false;
for i in 0..c.children.len() {
let Some(Node::Group(g)) = c.children.get(i) else { continue };
if !is_item(g) {
continue;
}
found = true;
let replacement = (g.match_id != want).then(|| {
let mut new = fresh(ids);
new.uid = g.uid;
new.name = super::effect::unique_name(c, &new.name);
for n in &mut new.children {
if let Some(old) = g.children.iter().find(|o| o.match_id() == n.match_id()) {
*n = old.clone();
}
}
new
});
let Some(Node::Group(g)) = c.children.get_mut(i) else { continue };
match replacement {
Some(new) => *g = new,
None => {
g.enabled = true;
if let Some(t) = g.get_mut("type").filter(|_| want == grad) {
t.set_value_at(lt, KV::Enum(radial as u32));
}
}
}
}
if !found && c.groups().any(|g| PATHS.contains(&g.match_id.as_str())) {
let mut new = fresh(ids);
new.name = super::effect::unique_name(c, &new.name);
// Paths, strokes, then fills, as new shapes have them.
let at = if stroke {
c.children.iter().position(|n| matches!(n.match_id(), "fill" | "gfill")).unwrap_or(c.children.len())
} else {
c.children.iter().rposition(|n| PATHS.contains(&n.match_id()) || matches!(n.match_id(), "stroke" | "gstroke")).map_or(c.children.len(), |i| i + 1)
};
c.children.insert(at, new.into());
}
}
/// Paint one contents group and its sub-groups: the kinds first, then Fills take `fill`, Strokes
/// `stroke`, Strokes and Gradient Strokes `width`. With `add_stroke`, a group with a path and no
/// stroke gets one (before its fills, as new shapes have it).
fn paint(c: &mut PropGroup, ids: &mut Ids, lt: Tick, e: &PaintEdit, depth: usize) {
if depth > MAX_DEPTH {
return;
}
if add_stroke && c.groups().any(|g| PATHS.contains(&g.match_id.as_str())) && !c.groups().any(is_stroke) {
if let Some(k) = e.stroke_kind {
set_kind(c, ids, lt, true, k, e);
}
if let Some(k) = e.fill_kind {
set_kind(c, ids, lt, false, k, e);
}
let (fill, stroke, width) = (e.fill, e.stroke, e.width);
if e.add_stroke && c.groups().any(|g| PATHS.contains(&g.match_id.as_str())) && !c.groups().any(is_stroke) {
let at = c.children.iter().position(|n| matches!(n, Node::Group(g) if matches!(g.match_id.as_str(), "fill" | "gfill"))).unwrap_or(c.children.len());
c.children.insert(at, build::shape_stroke(ids, stroke.unwrap_or([1.0; 4]), width.unwrap_or(2.0)).into());
}
@@ -252,7 +395,7 @@ fn paint(c: &mut PropGroup, ids: &mut Ids, lt: Tick, fill: Option<[f64; 4]>, str
"gstroke" => set(g, "width", width.map(KV::Scalar)),
"group" => {
if let Some(sub) = g.sub_mut("contents") {
paint(sub, ids, lt, fill, stroke, width, add_stroke, depth + 1);
paint(sub, ids, lt, e, depth + 1);
}
}
_ => {}
@@ -260,10 +403,20 @@ fn paint(c: &mut PropGroup, ids: &mut Ids, lt: Tick, fill: Option<[f64; 4]>, str
}
}
/// `p`'s `fillType` / `strokeType` (none|solid|linear|radial).
fn kind_p(p: &Value, key: &str, c: &str) -> Result<Option<PaintKind>> {
match p.get(key) {
None | Some(Value::Null) => Ok(None),
Some(v) => v.as_str().and_then(PaintKind::from_name).map(Some).ok_or_else(|| bad(c, format!("`{key}`: none|solid|linear|radial"))),
}
}
/// The toolbar's Fill and Stroke options with shape layers selected (as in After Effects, they
/// change the layers' fills and strokes): `fill` sets every Fill's colour, `stroke` every Stroke's,
/// `strokeWidth` every stroke's width. A stroke colour or a width above 0 gives the paths that
/// have no stroke one. Without values, reports the first layer's paint.
/// change the layers' fills and strokes): `fillType` / `strokeType` switch every Fill / Stroke
/// off (none) or turn them into Solid Color, Linear or Radial Gradient ones (adding one where a
/// path has none), `fill` sets every Fill's colour, `stroke` every Stroke's, `strokeWidth` every
/// stroke's width. A stroke colour or a width above 0 gives the paths that have no stroke one.
/// Without values, reports the first layer's paint.
fn fill_stroke(s: &mut Session, p: &Value) -> Result<Value> {
let c = "shape.fillStroke";
let (cid, lids) = layers_p(s, p)?;
@@ -275,8 +428,10 @@ fn fill_stroke(s: &mut Session, p: &Value) -> Result<Value> {
let rgba = |k: &str| color_p(p, k).map(|c| [c[0] as f64, c[1] as f64, c[2] as f64, 1.0]);
let (fill, stroke) = (rgba("fill"), rgba("stroke"));
let width = f_p(p, "strokeWidth").filter(|w| w.is_finite()).map(|w| w.max(0.0));
if fill.is_some() || stroke.is_some() || width.is_some() {
let add_stroke = width.map_or(stroke.is_some(), |w| w > 0.0);
let (fill_kind, stroke_kind) = (kind_p(p, "fillType", c)?, kind_p(p, "strokeType", c)?);
let add_stroke = stroke_kind != Some(PaintKind::None) && width.map_or(stroke.is_some(), |w| w > 0.0);
let e = PaintEdit { fill, stroke, width, add_stroke, fill_kind, stroke_kind };
if fill.is_some() || stroke.is_some() || width.is_some() || fill_kind.is_some() || stroke_kind.is_some() {
let t = s.time();
s.edit("Fill and Stroke", merge_p(p), |proj, _| {
let mut next = proj.next_id;
@@ -284,7 +439,7 @@ fn fill_stroke(s: &mut Session, p: &Value) -> Result<Value> {
let l = layer_mut(proj, cid, *lid)?;
let lt = l.layer_time(t);
if let Some(contents) = l.props.sub_mut("contents") {
paint(contents, &mut Ids(&mut next), lt, fill, stroke, width, add_stroke, 0);
paint(contents, &mut Ids(&mut next), lt, &e, 0);
}
}
proj.next_id = next;
@@ -292,8 +447,10 @@ fn fill_stroke(s: &mut Session, p: &Value) -> Result<Value> {
})?;
}
let l = shapes.first().and_then(|id| s.project.comp(cid)?.layer(*id)).ok_or(EngineError::NoComp)?;
let (fill, stroke, width) = first_paint(l, l.layer_time(s.time()));
Ok(json!({"fill": fill, "stroke": stroke, "strokeWidth": width}))
let lt = l.layer_time(s.time());
let (fill, stroke, width) = first_paint(l, lt);
let (fill_kind, stroke_kind) = first_paint_kinds(l, lt);
Ok(json!({"fill": fill, "stroke": stroke, "strokeWidth": width, "fillType": fill_kind.id(), "strokeType": stroke_kind.id()}))
}
pub fn specs() -> Vec<CommandSpec> {
@@ -303,7 +460,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Fill and Stroke",
[],
None,
"{layers?, fill?: color, stroke?: color, strokeWidth?, merge?} → {fill, stroke, strokeWidth} (first layer)",
"{layers?, fillType?: none|solid|linear|radial, strokeType?, fill?: color, stroke?: color, strokeWidth?, merge?} → {fill, stroke, strokeWidth, fillType, strokeType} (first layer)",
has_layers,
fill_stroke
),
+9 -3
View File
@@ -139,12 +139,18 @@ fn blended(mut g: PropGroup, paint: &ToolPaint) -> PropGroup {
g
}
/// A gradient's Start and End Points across `bounds` (`[x, y, w, h]`): left to right, or out
/// from the centre.
pub(crate) fn gradient_ends(radial: bool, bounds: [f64; 4]) -> ([f64; 2], [f64; 2]) {
let [x, y, w, h] = bounds;
let mid = [x + w / 2.0, y + h / 2.0];
if radial { (mid, [mid[0] + w.max(h).max(2.0) / 2.0, mid[1]]) } else { ([x, mid[1]], [x + w.max(1.0), mid[1]]) }
}
/// The Stroke and Fill a new shape gets from `paint`; gradients run across `bounds` (`[x, y, w,
/// h]` in the group's space): left to right, or out from the centre.
pub(crate) fn paint_items(ids: &mut Ids, paint: &ShapeTool, bounds: [f64; 4]) -> Vec<PropGroup> {
let [x, y, w, h] = bounds;
let mid = [x + w / 2.0, y + h / 2.0];
let ends = |radial: bool| if radial { (mid, [mid[0] + w.max(h).max(2.0) / 2.0, mid[1]]) } else { ([x, mid[1]], [x + w.max(1.0), mid[1]]) };
let ends = |radial: bool| gradient_ends(radial, bounds);
let mut items = vec![];
let (s, width) = (&paint.stroke, paint.stroke_width);
let stroke = match s.kind {
+51
View File
@@ -280,6 +280,57 @@ fn toolbar_fill_and_stroke_paint_selected_shape_layers() {
let _ = s.render(cid, s.time(), Default::default());
}
/// The toolbar's Fill Options / Stroke Options change a selected shape layer's paint kind (#732):
/// None switches the fill off, a gradient turns the Fill into a Gradient Fill that keeps its
/// opacity, Solid Color turns it back, and a Stroke kind adds a stroke where there is none.
#[test]
fn toolbar_fill_and_stroke_kinds_change_selected_shape_layers() {
let mut s = Session::default();
s.execute("comp.new", json!({"width": 400, "height": 300, "duration": 2})).unwrap();
let sh = s.execute("layer.newShape", json!({"kind": "rect", "fill": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
let items = |s: &Session| -> Vec<(String, bool)> {
let l = s.active_comp().unwrap().layer(effectcraft_project::LayerId(sh)).unwrap();
l.props.group("contents/group/contents").unwrap().groups().map(|g| (g.match_id.clone(), g.enabled)).collect()
};
let kinds = |s: &mut Session| {
let p = s.execute("shape.fillStroke", json!({})).unwrap();
(p["fillType"].as_str().unwrap().to_string(), p["strokeType"].as_str().unwrap().to_string())
};
assert_eq!(kinds(&mut s), ("solid".into(), "none".into()));
let cid = s.active_comp_id().unwrap();
let centre = |s: &Session| s.render(cid, s.time(), Default::default()).get(200, 150);
let red = centre(&s);
// None switches the fill off: nothing is drawn, and the item stays.
s.execute("shape.fillStroke", json!({"fillType": "none"})).unwrap();
assert_eq!(items(&s), [("rect".into(), true), ("fill".into(), false)]);
assert_eq!(kinds(&mut s).0, "none");
assert_ne!(centre(&s), red, "the fill is off");
// A radial gradient: the Fill becomes a Gradient Fill (switched on) that keeps its opacity.
s.execute("shape.fillStroke", json!({"fillType": "solid", "fill": "#ff0000"})).unwrap();
assert_eq!(centre(&s), red);
let opacity = |s: &Session, m: &str| {
let l = s.active_comp().unwrap().layer(effectcraft_project::LayerId(sh)).unwrap();
l.props.group(&format!("contents/group/contents/{m}")).unwrap().get("opacity").unwrap().value.as_f64()
};
s.execute("prop.set", json!({"layer": sh, "path": "contents/group/contents/fill/opacity", "value": 40})).unwrap();
s.execute("shape.fillStroke", json!({"fillType": "radial"})).unwrap();
assert_eq!(items(&s), [("rect".into(), true), ("gfill".into(), true)]);
assert_eq!((kinds(&mut s).0.as_str(), opacity(&s, "gfill")), ("radial", 40.0));
s.execute("shape.fillStroke", json!({"fillType": "linear"})).unwrap();
assert_eq!(kinds(&mut s).0, "linear");
s.execute("shape.fillStroke", json!({"fillType": "solid", "fill": "#00ff00"})).unwrap();
assert_eq!(items(&s), [("rect".into(), true), ("fill".into(), true)]);
assert_eq!(opacity(&s, "fill"), 40.0);
// A Stroke kind adds a stroke (before the fill); None switches it off again; undo is per step.
s.execute("shape.fillStroke", json!({"strokeType": "linear"})).unwrap();
assert_eq!(items(&s), [("rect".into(), true), ("gstroke".into(), true), ("fill".into(), true)]);
s.execute("shape.fillStroke", json!({"strokeType": "none"})).unwrap();
assert_eq!(kinds(&mut s), ("solid".into(), "none".into()));
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(kinds(&mut s), ("solid".into(), "linear".into()));
assert!(s.execute("shape.fillStroke", json!({"fillType": "plaid"})).is_err());
}
/// Selecting a mask in the Timeline (its row or its Mask Path) selects all of its points, so the
/// viewer drags the whole mask (#203); a shape's Path item works the same way.
#[test]
+34 -11
View File
@@ -133,13 +133,15 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
app.session.state.selected_layers.iter().filter(|id| c.layer(**id).is_some_and(|l| matches!(l.source, LayerSource::Shape))).map(|id| id.0).collect()
})
.unwrap_or_default();
let layer_paint = shape_layers
.first()
.and_then(|id| app.session.active_comp()?.layer(LayerId(*id)))
.map(|l| effectcraft_engine::commands::shape_stroke::first_paint(l, l.layer_time(app.session.time())));
if let Some((fill, stroke, width)) = layer_paint {
let layer_paint = shape_layers.first().and_then(|id| app.session.active_comp()?.layer(LayerId(*id))).map(|l| {
let lt = l.layer_time(app.session.time());
(effectcraft_engine::commands::shape_stroke::first_paint(l, lt), effectcraft_engine::commands::shape_stroke::first_paint_kinds(l, lt))
});
if let Some(((fill, stroke, width), (fill_kind, stroke_kind))) = layer_paint {
let rgb = |c: [f64; 4]| [c[0] as f32, c[1] as f32, c[2] as f32];
let tool = &mut app.session.state.shape_tool;
tool.fill.kind = fill_kind;
tool.stroke.kind = stroke_kind;
if let Some(c) = fill {
tool.fill.color = rgb(c);
}
@@ -363,8 +365,7 @@ fn fill_stroke_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Pa
if ui.input(|i| i.modifiers.alt) {
let at = PaintKind::ALL.iter().position(|k| *k == tp.kind).unwrap_or(0);
let next = PaintKind::ALL.get((at + 1) % PaintKind::ALL.len()).copied().unwrap_or_default();
let k = format!("{key}Type");
tool_options(app, json!({k: next.id()}));
paint_kind(app, shape_layers, key, next);
} else if tp.kind == PaintKind::Solid {
widgets::open_popup(ui, picker_id(key));
} else {
@@ -376,7 +377,7 @@ fn fill_stroke_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Pa
tool_options(app, json!({key: c}));
paint(app, key, json!(c));
}
paint_options_popup(app, ui, key, &options, wr.left_bottom());
paint_options_popup(app, ui, shape_layers, key, &options, wr.left_bottom());
x = sr.max.x + 12.0;
}
let width = app.session.state.shape_tool.stroke_width;
@@ -389,9 +390,28 @@ fn fill_stroke_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Pa
r.max.x + 14.0
}
/// A Fill or Stroke kind: for new shapes, and (as in After Effects) for the selected shape
/// layers' fills or strokes, which None switches off (#732).
fn paint_kind(app: &mut EffectcraftApp, shape_layers: &[u64], key: &str, kind: PaintKind) {
let k_type = format!("{key}Type");
tool_options(app, json!({&k_type: kind.id()}));
if shape_layers.is_empty() {
return;
}
let mut v = json!({"layers": shape_layers, k_type: kind.id()});
// A Solid Color takes the swatch's colour.
let color = if key == "fill" { app.session.state.shape_tool.fill.color } else { app.session.state.shape_tool.stroke.color };
if let (PaintKind::Solid, Some(o)) = (kind, v.as_object_mut()) {
o.insert(key.to_string(), json!(color));
}
if let Err(e) = app.session.execute("shape.fillStroke", v) {
app.ui.status = e.to_string();
}
}
/// Fill Options / Stroke Options: None, Solid Color, Linear Gradient or Radial Gradient, and the
/// blend mode and opacity new shapes get (After Effects' dialogs, applied as they change).
fn paint_options_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, key: &str, title: &str, pos: egui::Pos2) {
fn paint_options_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, shape_layers: &[u64], key: &str, title: &str, pos: egui::Pos2) {
let id = egui::Id::new(("tool-paint-options", key));
if !widgets::popup_is_open(ui, id) {
return;
@@ -400,6 +420,7 @@ fn paint_options_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, key: &str, t
let tp = if key == "fill" { &app.session.state.shape_tool.fill } else { &app.session.state.shape_tool.stroke }.clone();
let blend_pop = id.with("blend");
let mut set = None;
let mut kind = None;
let mut blend_rect = Rect::NOTHING;
let area = egui::Area::new(id.with("area")).order(egui::Order::Foreground).fixed_pos(pos + vec2(0.0, 4.0)).show(ui.ctx(), |ui| {
egui::Frame::popup(ui.style()).show(ui, |ui| {
@@ -416,8 +437,7 @@ fn paint_options_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, key: &str, t
paint_swatch(&p, r.shrink(3.0), &ToolPaint { kind: k, ..tp.clone() }, resp.hovered(), &t);
app.auto.add(&format!("header.{key}Options.{}", k.id()), r, k.label());
if resp.clicked() {
let k_type = format!("{key}Type");
set = Some(json!({k_type: k.id()}));
kind = Some(k);
}
}
p.text(row(33.0) + vec2(134.0, 0.0), Align2::LEFT_CENTER, tp.kind.label(), Tokens::ui(11.5), t.text_dim);
@@ -451,6 +471,9 @@ fn paint_options_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, key: &str, t
if let Some(v) = set {
tool_options(app, v);
}
if let Some(k) = kind {
paint_kind(app, shape_layers, key, k);
}
}
/// A Fill or Stroke swatch: its colour, a gradient (white to black), or None's red slash.
+26
View File
@@ -601,6 +601,32 @@ fn fill_options_paint_the_next_shape() {
assert_eq!(gfill.get("opacity").unwrap().value.as_f64(), 40.0);
}
/// With a shape layer selected, Fill Options' None switches its fill off and Solid Color turns
/// it back on, and the Fill swatch shows the layer's kind (#732).
#[test]
fn fill_options_change_the_selected_shape_layer() {
let mut h = harness();
let l = h.state_mut().session.execute("layer.newShape", json!({"kind": "rect", "size": [100, 100], "position": [320, 180]})).unwrap()["layer"]
.as_u64()
.unwrap();
h.run_steps(2);
let fill_on = |h: &Harness<'_, EffectcraftApp>| {
let layer = h.state().session.active_comp().unwrap().layer(LayerId(l)).unwrap().clone();
layer.props.group("contents/group/contents/fill").unwrap().enabled
};
assert!(fill_on(&h));
for (kind, on) in [("none", false), ("solid", true)] {
let at = rect(&h, "header.fillOptions").center();
click(&mut h, at);
let at = rect(&h, &format!("header.fillOptions.{kind}")).center();
click(&mut h, at);
h.input_mut().events.push(Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() });
h.run_steps(3);
assert_eq!(fill_on(&h), on, "{kind}");
assert_eq!(h.state().session.state.shape_tool.fill.kind.id(), kind);
}
}
#[test]
fn shape_pen_draws_a_closed_shape_layer() {
let mut h = harness();