From 7002c926323ff2b7e01c3b1af132da0eaaede906 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 6 Oct 2026 11:08:56 -0400 Subject: [PATCH] Timeline: selected effect rows highlight (#91, #97) Clicking an effect's name selected it (prop.select with the effect's group uid) but group rows never painted the selection, so there was no feedback. Group rows (effects, masks, shape groups...) now fill with row_selected when selected, exactly like selected property rows. Regression test: timeline_selected_effect_row_highlights. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 8b140428b39a24c3e1207ffde0430e759e316112) --- crates/ui-egui/src/panels/timeline.rs | 15 +++++++- crates/ui-egui/tests/ui_workflow_selection.rs | 38 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 281d3e9f..c8315c97 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -1601,7 +1601,20 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } } RowKind::Group { uid, name, open, has_children, fx, eye } => { - lp.rect_filled(left, 0.0, if ri % 2 == 0 { t.row } else { t.row_alt }); + // Selected groups (an effect, a mask, a shape group…) highlight like selected + // property rows. + let sel_group = app.session.state.selected_props.contains(&(layer.id, *uid)); + lp.rect_filled( + left, + 0.0, + if sel_group { + t.row_selected + } else if ri % 2 == 0 { + t.row + } else { + t.row_alt + }, + ); gp.rect_filled(Rect::from_min_max(pos2(graph_x0, r.min.y), r.max), 0.0, t.tl_bg); let indent = cw.name + 6.0 + 14.0 * row.depth as f32; if *has_children { diff --git a/crates/ui-egui/tests/ui_workflow_selection.rs b/crates/ui-egui/tests/ui_workflow_selection.rs index 4e849dca..1880bb86 100644 --- a/crates/ui-egui/tests/ui_workflow_selection.rs +++ b/crates/ui-egui/tests/ui_workflow_selection.rs @@ -372,3 +372,41 @@ fn workflow_snapshot() { h.run_steps(3); h.render().expect("offscreen UI render").save(out).unwrap(); } + +/// Filled rects painted this frame (flattening nested shape lists). +fn filled_rects(h: &Harness<'_, EffectcraftApp>) -> Vec<(Rect, egui::Color32)> { + fn walk(s: &egui::Shape, out: &mut Vec<(Rect, egui::Color32)>) { + match s { + egui::Shape::Rect(r) => out.push((r.rect, r.fill)), + egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, out)), + _ => {} + } + } + let mut out = vec![]; + for c in &h.output().shapes { + walk(&c.shape, &mut out); + } + out +} + +/// Issues #91 / #97: clicking an effect's name in the Timeline selects it, and its row then +/// highlights like a selected property row (it used to stay unhighlighted). +#[test] +fn timeline_selected_effect_row_highlights() { + let mut h = harness(); + let id = h.state().session.active_comp().unwrap().layers[0].id; + let fx = h.state_mut().session.execute("effect.apply", json!({"layers": [id.0], "effect": "Gaussian Blur"})).unwrap()["effects"][0].as_u64().unwrap(); + let fx_group = h.state().session.active_comp().unwrap().layer(id).unwrap().props.sub("effects").unwrap().uid; + h.state_mut().ui.timeline.open_layers.insert(id.0); + h.state_mut().ui.timeline.open_groups.insert(fx_group); + // `effect.apply` selects the new effect; start from no property selection. + h.state_mut().session.state.selected_props.clear(); + h.run_steps(3); + let row_selected = h.state().tokens.row_selected; + let name = rect(&h, &format!("timeline.group.{fx}.name")); + let highlighted = |h: &Harness<'_, EffectcraftApp>| filled_rects(h).iter().any(|(r, c)| *c == row_selected && r.contains(name.center())); + assert!(!highlighted(&h), "not highlighted before it is selected"); + click(&mut h, name.center(), Modifiers::NONE); + assert!(h.state().session.state.selected_props.contains(&(id, fx)), "the click selects the effect"); + assert!(highlighted(&h), "the selected effect's row is highlighted"); +}