diff --git a/crates/ui-egui/src/panels/effect_controls.rs b/crates/ui-egui/src/panels/effect_controls.rs index 6612187e..fb388845 100644 --- a/crates/ui-egui/src/panels/effect_controls.rs +++ b/crates/ui-egui/src/panels/effect_controls.rs @@ -186,8 +186,9 @@ fn key_navigator( } } -/// Toggle a viewer pick (crosshair / eyedropper) for a parameter. -fn toggle_pick(app: &mut EffectcraftApp, kind: &str, layer: &Layer, prop: &Property) { +/// Toggle a viewer pick (crosshair / eyedropper) for a parameter (also the Timeline's colour +/// eyedroppers). +pub(crate) fn toggle_pick(app: &mut EffectcraftApp, kind: &str, layer: &Layer, prop: &Property) { let pick = FxPick { kind: kind.into(), layer: layer.id.0, prop: prop.uid, name: prop.name.clone() }; if app.ui.fx_pick.as_ref() == Some(&pick) { app.ui.fx_pick = None; diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 086e46dc..b28c0d23 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -3591,6 +3591,17 @@ fn value_editor( if resp.clicked() { widgets::open_popup(ui, pop); } + // The eyedropper after the swatch samples the Composition panel (shape Fill and + // Stroke colours, text and effect colours), as in After Effects' Timeline (#633). + let er = Rect::from_center_size(pos2(r.max.x + 12.0, at.y), vec2(16.0, 16.0)); + let active = app.ui.fx_pick.as_ref().is_some_and(|k| k.prop == uid && k.kind == "color"); + if super::fx_widgets::eyedropper_button(ui, er, active, egui::Id::new(("tl-eye", uid)), &t) + .on_hover_text(crate::i18n::tr("Eyedropper: click, then click in the Composition panel")) + .clicked() + { + super::effect_controls::toggle_pick(app, "color", layer, prop); + } + app.auto.add(&format!("timeline.prop.{uid}.eyedropper"), er, &prop.name); let mut rgb = [c[0] as f32, c[1] as f32, c[2] as f32]; if crate::widgets::color_popup(ui, pop, r.left_bottom(), &mut rgb) { set(actions, json!([rgb[0], rgb[1], rgb[2], 1.0])); diff --git a/crates/ui-egui/tests/ui_timeline_shapes.rs b/crates/ui-egui/tests/ui_timeline_shapes.rs index 811b55c7..dd9232d7 100644 --- a/crates/ui-egui/tests/ui_timeline_shapes.rs +++ b/crates/ui-egui/tests/ui_timeline_shapes.rs @@ -197,3 +197,49 @@ fn toolbar_stroke_edits_the_selected_shape_layer() { assert_eq!(width, Some(4.0), "a stroke was added with the toolbar's width"); assert_eq!(h.state().session.state.shape_tool.stroke_width, 4.0); } + +/// Issue #633: a colour property in the Timeline (a shape's Fill Color) has an eyedropper after +/// its swatch, as in After Effects: click it, then click in the Composition panel to sample. +#[test] +fn timeline_colour_eyedropper_samples_the_comp() { + let mut s = session(); + s.execute("layer.newSolid", json!({"name": "Back", "color": "#20c040"})).unwrap(); + let l = s.execute("layer.newShape", json!({"kind": "rect", "fill": [1.0, 0.0, 0.0], "size": [100.0, 60.0]})).unwrap()["layer"].as_u64().unwrap(); + let layer = s.active_comp().unwrap().layer(LayerId(l)).unwrap().clone(); + let mut fill = None; + layer.props.walk("", &mut |_, p| { + if fill.is_none() && matches!(p.value, effectcraft_engine::keyframe::Value::Color(_)) { + fill = Some(p.uid); + } + }); + let fill = fill.expect("a fill colour"); + fn groups(g: &effectcraft_engine::project::PropGroup, out: &mut Vec) { + for c in g.groups() { + out.push(c.uid); + groups(c, out); + } + } + let mut open = vec![]; + groups(&layer.props, &mut open); + let mut h = harness(s); + h.state_mut().ui.timeline.open_groups.extend(open); + open_layer(&mut h, l); + h.run_steps(2); + click(&mut h, &format!("timeline.prop.{fill}.eyedropper")); + assert_eq!(h.state().ui.fx_pick.as_ref().map(|p| (p.prop, p.kind.as_str())), Some((fill, "color")), "the eyedropper waits for a viewer click"); + let pos = effectcraft_ui_egui::panels::viewer::comp_to_screen(&h.ctx, [6.0, 6.0]).unwrap(); + click_at(&mut h, pos); + assert!(h.state().ui.fx_pick.is_none(), "the pick is used up"); + let layer = h.state().session.active_comp().unwrap().layer(LayerId(l)).unwrap().clone(); + let mut got = None; + layer.props.walk("", &mut |_, p| { + if p.uid == fill { + got = Some(p.value.clone()); + } + }); + let Some(effectcraft_engine::keyframe::Value::Color(c)) = got else { panic!("{got:?}") }; + let want = [0x20 as f64 / 255.0, 0xc0 as f64 / 255.0, 0x40 as f64 / 255.0]; + for k in 0..3 { + assert!((c[k] - want[k]).abs() < 0.02, "{c:?} vs {want:?}"); + } +}