Timeline: every shape Contents item has an eye switch, as in After Effects (#959)

Groups, paths, fills, strokes and operators in a shape layer's Contents show the
Video eye in the A/V column (a group's Transform doesn't). Clicking it runs
prop.setGroupEnabled, which the shape renderer already honours; layer-style
eyes keep layer.style.toggle. Row building and search results share eye_of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-30 00:52:26 +07:00
co-authored by Claude Opus 5.5
parent 03f1a1b221
commit 9424c9eeb1
2 changed files with 46 additions and 9 deletions
+19 -9
View File
@@ -795,12 +795,11 @@ fn display_name(app: &EffectcraftApp, l: &Layer) -> String {
/// layer whose names contain `q` (lowercase), each with its enclosing groups. A matching group
/// brings its whole contents.
fn search_rows(l: &Layer, q: &str) -> Vec<Row> {
/// The row for a group in search results: depth, its effect's enabled flag, and the eye
/// switch for layer-style groups (not Blending Options).
/// The row for a group in search results: depth, its effect's enabled flag, and its eye
/// switch ([`eye_of`]).
fn group_row(l: &Layer, g: &PropGroup, depth: usize, parent: &PropGroup) -> Row {
let fx = matches!(g.kind, GroupKind::Effect { .. }).then_some(g.enabled);
let eye =
(parent.match_id == effectcraft_engine::project::styles::GROUP && g.match_id != effectcraft_engine::project::styles::BLENDING).then_some(g.enabled);
let eye = eye_of(parent, g);
Row { layer: l.id, depth, kind: RowKind::Group { uid: g.uid, name: g.name.clone(), open: true, has_children: !g.children.is_empty(), fx, eye } }
}
/// Every visible child of `g` at `depth`, groups recursed into whole — a matching group
@@ -1235,6 +1234,14 @@ fn collect_props(g: &PropGroup, out: &mut Vec<u64>, f: &dyn Fn(&Property) -> boo
/// in `open`, text animators list their selectors and properties directly (as in AE), and a
/// shape group's inner Contents (AE's elided `ADBE Vectors Group`) is skipped so its items
/// list directly under the group.
/// The eye switch of group `g` inside `parent`, as in After Effects: layer styles (not Blending
/// Options) and every item in a shape layer's Contents (#959), which the renderer skips when off.
fn eye_of(parent: &PropGroup, g: &PropGroup) -> Option<bool> {
let style = parent.match_id == effectcraft_engine::project::styles::GROUP && g.match_id != effectcraft_engine::project::styles::BLENDING;
let shape_item = parent.match_id == "contents" && g.match_id != "transform";
(style || shape_item).then_some(g.enabled)
}
fn push_group(rows: &mut Vec<Row>, l: &Layer, g: &PropGroup, depth: usize, open: &std::collections::BTreeSet<u64>) {
for c in &g.children {
match c {
@@ -1250,9 +1257,7 @@ fn push_group(rows: &mut Vec<Row>, l: &Layer, g: &PropGroup, depth: usize, open:
Node::Group(sg) => {
let o = open.contains(&sg.uid);
let fx = matches!(sg.kind, GroupKind::Effect { .. }).then_some(sg.enabled);
// Layer style groups have eye switches (not Blending Options).
let eye = (g.match_id == effectcraft_engine::project::styles::GROUP && sg.match_id != effectcraft_engine::project::styles::BLENDING)
.then_some(sg.enabled);
let eye = eye_of(g, sg);
rows.push(Row {
layer: l.id,
depth,
@@ -3388,11 +3393,16 @@ fn group_switches(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc: &
app.auto.add(&format!("timeline.group.{uid}.fx"), fr_, name);
}
if let Some(en) = eye {
// Layer style eye switch in the A/V column, like AE.
// A layer style's or shape item's eye switch in the A/V column, like AE.
let er = Rect::from_center_size(pos2(cw.av + 10.0, cy), vec2(15.0, 15.0));
lp.rect_stroke(er.shrink(1.5), 2.0, Stroke::new(1.0, t.separator), StrokeKind::Inside);
if widgets::icon_toggle(ui, er, Icon::Eye, *en, &t, egui::Id::unique(("geye", uid)), Sense::click()).clicked() {
actions.push(("layer.style.toggle".into(), json!({"layer": layer.id.0, "style": uid})));
let style = layer.props.sub(effectcraft_engine::project::styles::GROUP).is_some_and(|s| s.find_group(*uid).is_some());
actions.push(if style {
("layer.style.toggle".into(), json!({"layer": layer.id.0, "style": uid}))
} else {
("prop.setGroupEnabled".into(), json!({"layer": layer.id.0, "prop": uid, "value": !*en}))
});
}
app.auto.add(&format!("timeline.group.{uid}.eye"), er, name);
}
@@ -237,6 +237,33 @@ fn contents_names(h: &Harness<'_, EffectcraftApp>, l: u64) -> Vec<String> {
h.state().session.active_comp().unwrap().layer(LayerId(l)).unwrap().props.sub("contents").unwrap().groups().map(|g| g.name.clone()).collect()
}
/// #959: every item in a shape layer's Contents has an eye switch, as in After Effects; it turns
/// the item off (the renderer skips it), undoably. The group's Transform has none.
#[test]
fn shape_contents_items_have_eye_switches() {
let (mut h, a, _, rect) = two_shape_layers();
let enabled = |h: &Harness<'_, EffectcraftApp>| {
h.state().session.active_comp().unwrap().layer(LayerId(a)).unwrap().props.sub("contents").unwrap().find_group(rect).unwrap().enabled
};
let eye = format!("timeline.group.{rect}.eye");
assert!(enabled(&h));
click(&mut h, &eye);
assert!(!enabled(&h), "the eye turns Rectangle 1 off");
click(&mut h, &eye);
assert!(enabled(&h), "and on again");
click(&mut h, &eye);
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
assert!(enabled(&h), "one undo step");
// Inside the group: its path and fill have eyes, its Transform doesn't.
h.state_mut().ui.timeline.open_groups.insert(rect);
h.run_steps(3);
let group = h.state().session.active_comp().unwrap().layer(LayerId(a)).unwrap().props.find_group(rect).unwrap().clone();
let transform = group.sub("transform").expect("a shape group has a Transform").uid;
assert!(h.state().auto.find(&format!("timeline.group.{transform}.eye")).is_none(), "no eye on Transform");
let items = group.sub("contents").expect("the group's items").groups().map(|g| g.uid).collect::<Vec<_>>();
assert!(!items.is_empty() && items.iter().all(|u| h.state().auto.find(&format!("timeline.group.{u}.eye")).is_some()), "every item has an eye");
}
#[test]
fn ctrl_c_and_ctrl_v_copy_a_shape_group_into_another_shape_layer() {
let (mut h, a, b, rect) = two_shape_layers();