Project: opening a comp brings up its Composition panel (#90)

Opening a comp (Project double-click, Open Composition, a Timeline tab)
now reopens the Composition panel if it was closed and brings it (and
the Timeline) to the front, as in After Effects. A closed Composition
panel also reopens in the centre (with the Layer panel or other viewers,
else above the Timeline) instead of beside Effects & Presets, which also
fixes Window > Composition.

App::raise_panel opens/fronts a panel without moving the focus;
show_panel builds on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit adf2c6dac78ce426ea0bf6b4199b7238d8ad99dd)
This commit is contained in:
Brandon Thomas 2026-10-06 11:31:27 -04:00
parent a46ebf4dc7
commit aa31ffc2fb
3 changed files with 145 additions and 10 deletions

View File

@ -381,17 +381,41 @@ impl EffectcraftApp {
v
}
/// Show panel `p` (opening it in its usual place if it is closed), bring it to the front and
/// give it the focus: Window ▸ <panel>.
pub fn show_panel(&mut self, p: PanelKind) {
if let Some(f) = self.ui.floating.iter_mut().find(|f| f.panels.contains(&p)) {
f.active = f.panels.iter().position(|x| *x == p).unwrap_or(0);
self.ui.focused = p;
return;
}
if self.ui.maximized.is_some_and(|m| m != p) {
if !self.ui.floating.iter().any(|f| f.panels.contains(&p)) && self.ui.maximized.is_some_and(|m| m != p) {
self.ui.maximized = None;
}
self.raise_panel(p);
self.ui.focused = p;
}
/// Bring panel `p` to the front of its group, opening it in its usual place if it is closed,
/// without moving the focus (a comp opening brings up its viewer, an effect applied brings up
/// Effect Controls).
pub fn raise_panel(&mut self, p: PanelKind) {
if let Some(f) = self.ui.floating.iter_mut().find(|f| f.panels.contains(&p)) {
f.active = f.panels.iter().position(|x| *x == p).unwrap_or(0);
return;
}
if !self.ui.dock.contains(p) {
// The Composition panel lives in the centre, with the Layer panel and other viewers;
// with none of those left, above the Timeline.
let centre = |d: &dock::DockNode| {
let mut v = Vec::new();
d.panels(&mut v);
v.into_iter().find(|q| matches!(q, PanelKind::Layer | PanelKind::Viewer(_) | PanelKind::Flowchart | PanelKind::Footage))
};
let near = match p {
PanelKind::Composition => match centre(&self.ui.dock) {
Some(q) => q,
None if self.ui.dock.dock_panel(p, PanelKind::Timeline, dock::Zone::Top) => {
self.ui.dock.activate(p);
return;
}
None => PanelKind::EffectsPresets,
},
PanelKind::Layer | PanelKind::Flowchart | PanelKind::Viewer(_) => PanelKind::Composition,
PanelKind::RenderQueue => PanelKind::Timeline,
PanelKind::EffectControls | PanelKind::History => PanelKind::Project,
@ -400,7 +424,6 @@ impl EffectcraftApp {
self.ui.dock.open_near(p, near);
}
self.ui.dock.activate(p);
self.ui.focused = p;
}
pub fn render_source(&self) -> RenderSource {

View File

@ -192,10 +192,12 @@ pub fn on_open_comp(app: &mut EffectcraftApp, comp: ItemId) {
};
}
app.ui.viewers.insert(app.ui.active_viewer, Some(comp.0));
// The comp's viewer and its Timeline tab come to the front (unless locked).
// The comp's viewer and its Timeline tab come to the front (unless locked), reopened if they
// were closed (as in After Effects).
for p in [active_panel(app), PanelKind::Timeline] {
if !app.ui.locked_tabs.contains(&p.id()) {
app.ui.dock.activate(p);
let closed = !app.ui.dock.contains(p) && !app.ui.floating.iter().any(|f| f.panels.contains(&p));
if closed || !app.ui.locked_tabs.contains(&p.id()) {
app.raise_panel(p);
}
}
}

View File

@ -0,0 +1,110 @@
//! Panels that come up on their own (egui_kittest), as in After Effects: opening a comp brings up
//! its Composition panel even when it was closed (#90).
use effectcraft_engine::Session;
use effectcraft_engine::project::ItemId;
use effectcraft_ui_egui::EffectcraftApp;
use effectcraft_ui_egui::dock::{DockNode, PanelKind};
use egui::{Event, Pos2, pos2};
use egui_kittest::Harness;
use serde_json::json;
/// Comp "Main" (open) holding a solid, and comp "Other" (not open).
fn harness() -> (Harness<'static, EffectcraftApp>, u64, u64) {
let mut s = Session::default();
let other = s.execute("comp.new", json!({"name": "Other", "width": 320, "height": 180, "duration": 4})).unwrap()["comp"].as_u64().unwrap();
let main = s.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "duration": 4})).unwrap()["comp"].as_u64().unwrap();
s.execute("layer.newSolid", json!({"name": "Red", "color": [1.0, 0.0, 0.0, 1.0]})).unwrap();
s.state.open_comps = vec![ItemId(main)];
s.state.active_comp = Some(ItemId(main));
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
(h, other, main)
}
fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> egui::Rect {
let e = h.state().auto.previous.iter().chain(h.state().auto.elements.iter()).find(|e| e.id == id).cloned().unwrap_or_else(|| {
let have: Vec<&String> = h.state().auto.elements.iter().map(|e| &e.id).collect();
panic!("no element {id}; have {have:?}")
});
egui::Rect::from_min_size(pos2(e.rect[0], e.rect[1]), egui::vec2(e.rect[2], e.rect[3]))
}
fn click_n(h: &mut Harness<'_, EffectcraftApp>, p: Pos2, n: u32) {
h.event(Event::PointerMoved(p));
h.step();
for _ in 0..n {
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
}
h.step();
h.run_steps(2);
}
/// The tabs of the docked group holding `p`, in order.
fn group_of(n: &DockNode, p: PanelKind) -> Option<Vec<PanelKind>> {
match n {
DockNode::Split { a, b, .. } => group_of(a, p).or_else(|| group_of(b, p)),
DockNode::Tabs { panels, .. } => panels.contains(&p).then(|| panels.clone()),
DockNode::Stack { entries } => entries.iter().any(|e| e.panel == p).then(|| entries.iter().map(|e| e.panel).collect()),
}
}
fn invoke(h: &mut Harness<'_, EffectcraftApp>, id: &str, params: serde_json::Value) {
let ctx = h.ctx.clone();
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, id, params).unwrap();
h.run_steps(2);
}
/// Issue #90: with the Composition panel closed (the centre shows the Layer panel), opening a
/// comp (double-click in the Project panel, or Open Composition) brings the Composition panel
/// back, in front, where it normally lives (with the Layer panel), and so does Window ▸
/// Composition.
#[test]
fn opening_a_comp_brings_up_a_closed_composition_panel() {
let (mut h, other, main) = harness();
assert_eq!(group_of(&h.state().ui.dock, PanelKind::Composition), Some(vec![PanelKind::Composition, PanelKind::Layer]));
invoke(&mut h, "window.closePanel", json!({"panel": "Composition"}));
assert!(!h.state().ui.dock.contains(PanelKind::Composition));
assert!(h.state().ui.dock.is_visible(PanelKind::Layer), "the centre shows the Layer panel");
// Double-click "Other" in the Project panel.
let name = rect(&h, &format!("project.item.{other}.name"));
click_n(&mut h, name.center(), 2);
assert_eq!(h.state().session.active_comp_id(), Some(ItemId(other)));
let dock = &h.state().ui.dock;
assert!(dock.is_visible(PanelKind::Composition), "the Composition panel is back and in front");
assert_eq!(group_of(dock, PanelKind::Composition), Some(vec![PanelKind::Layer, PanelKind::Composition]), "back in the centre");
assert!(h.state().auto.elements.iter().any(|e| e.id == "panel.Composition"), "the Composition panel is drawn");
// Open Composition (the Project panel's context menu) with the panel closed again.
invoke(&mut h, "window.closePanel", json!({"panel": "Composition"}));
invoke(&mut h, "comp.open", json!({"comp": main}));
assert_eq!(h.state().session.active_comp_id(), Some(ItemId(main)));
assert!(h.state().ui.dock.is_visible(PanelKind::Composition));
assert_eq!(group_of(&h.state().ui.dock, PanelKind::Composition), Some(vec![PanelKind::Layer, PanelKind::Composition]));
// Window ▸ Composition reopens it in the centre too (not beside Effects & Presets).
invoke(&mut h, "window.closePanel", json!({"panel": "Composition"}));
invoke(&mut h, "window.panel", json!({"panel": "Composition"}));
assert_eq!(group_of(&h.state().ui.dock, PanelKind::Composition), Some(vec![PanelKind::Layer, PanelKind::Composition]));
assert!(h.state().ui.dock.is_visible(PanelKind::Composition));
}
/// Issue #90 in a workspace without a Layer panel: closing Composition leaves only the Timeline,
/// and opening a comp brings the viewer back above it.
#[test]
fn opening_a_comp_in_the_minimal_workspace_docks_the_viewer_above_the_timeline() {
let (mut h, other, _) = harness();
invoke(&mut h, "window.workspace", json!({"name": "Minimal"}));
invoke(&mut h, "window.closePanel", json!({"panel": "Composition"}));
assert!(!h.state().ui.dock.contains(PanelKind::Composition));
invoke(&mut h, "comp.open", json!({"comp": other}));
match &h.state().ui.dock {
DockNode::Split { vertical: true, a, b, .. } => {
assert_eq!(group_of(a, PanelKind::Composition), Some(vec![PanelKind::Composition]), "the viewer is on top");
assert!(b.contains(PanelKind::Timeline));
}
other => panic!("expected the viewer above the Timeline, got {other:?}"),
}
}