UI-1: every After Effects workspace

Adds Review, Learn, Essential Graphics, Color and Undocked Panels to the
built-in workspaces (Libraries, an Adobe service, is left out), lists them all
in Window > Workspace in After Effects' order, and gives each its own layout:
Review (large viewer, Markers and Project), Learn (Default plus the Home
screen with the community links), Essential Graphics (Properties panel full
height, type panels), Color (wide Effect Controls, Info), Undocked Panels
(Composition docked, the other panels floating). Tests check every AE
workspace exists, has its own layout, has each panel once and is in the menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-02 22:34:32 -04:00
parent 541c740f18
commit 6aca8fe85c
3 changed files with 139 additions and 7 deletions

View File

@ -17,7 +17,8 @@
//!
//! An entry's shortcut is the command's default shortcut unless the line gives one (needed for
//! entries with bound parameters). Adobe-service entries (Team Projects, Libraries, Bridge, Media
//! Encoder, Essential Graphics, Behance, Creative Cloud…) are intentionally absent.
//! Encoder, Behance, Creative Cloud…) are intentionally absent; the Essential Graphics workspace
//! uses the Properties panel.
use std::sync::OnceLock;
@ -909,16 +910,22 @@ View
Enter Full Screen | view.fullScreen
Window
Workspace
Default | window.workspace {"name":"Default"} | Shift+F10
Review | window.workspace {"name":"Review"}
Learn | window.workspace {"name":"Learn"}
Small Screen | window.workspace {"name":"Small Screen"} | Shift+F12
Standard | window.workspace {"name":"Standard"} | Shift+F11
---
All Panels | window.workspace {"name":"All Panels"}
Animation | window.workspace {"name":"Animation"}
Default | window.workspace {"name":"Default"} | Shift+F10
Color | window.workspace {"name":"Color"}
Effects | window.workspace {"name":"Effects"}
Essential Graphics | window.workspace {"name":"Essential Graphics"}
Minimal | window.workspace {"name":"Minimal"}
Motion Tracking | window.workspace {"name":"Motion Tracking"}
Paint | window.workspace {"name":"Paint"}
Small Screen | window.workspace {"name":"Small Screen"} | Shift+F12
Standard | window.workspace {"name":"Standard"} | Shift+F11
Text | window.workspace {"name":"Text"}
Undocked Panels | window.workspace {"name":"Undocked Panels"}
---
Reset to Saved Layout | window.resetWorkspace
Save Changes to this Workspace | window.saveWorkspace

View File

@ -172,7 +172,43 @@ fn vsplit(size: SplitSize, a: DockNode, b: DockNode) -> DockNode {
DockNode::Split { vertical: true, size, a: Box::new(a), b: Box::new(b) }
}
pub const WORKSPACES: [&str; 10] = ["Default", "Standard", "Small Screen", "Animation", "Effects", "Motion Tracking", "Paint", "Text", "Minimal", "All Panels"];
/// The built-in workspaces: After Effects 2026's (Window ▸ Workspace), less Libraries (an Adobe
/// service). Learn shows the Home screen (community links instead of Adobe's tutorials);
/// Essential Graphics uses the Properties panel; Undocked Panels floats the panels over the
/// Composition panel.
pub const WORKSPACES: &[&str] = &[
"Default",
"Standard",
"Small Screen",
"Animation",
"Effects",
"Motion Tracking",
"Paint",
"Text",
"Minimal",
"All Panels",
"Review",
"Learn",
"Essential Graphics",
"Color",
"Undocked Panels",
];
/// The floating panels a workspace starts with (Undocked Panels), in points for a window of
/// about 1680×1020 (they are kept inside the window when drawn).
pub fn workspace_floating(name: &str) -> Vec<Floating> {
use PanelKind::*;
let f = |panels: &[PanelKind], rect: [f32; 4]| Floating { panels: panels.to_vec(), active: 0, rect };
match name {
"Undocked Panels" => vec![
f(&[Project, EffectControls], [24.0, 110.0, 320.0, 420.0]),
f(&[Preview, Info, Audio], [1330.0, 110.0, 320.0, 300.0]),
f(&[EffectsPresets, Properties, Character, Paragraph], [1330.0, 430.0, 320.0, 360.0]),
f(&[Timeline, RenderQueue], [24.0, 640.0, 1280.0, 340.0]),
],
_ => vec![],
}
}
/// The default layout of a named workspace.
pub fn workspace(name: &str) -> DockNode {
@ -226,6 +262,40 @@ pub fn workspace(name: &str) -> DockNode {
tabs(&[Timeline], 0),
),
"Minimal" => vsplit(Ratio(0.6), tabs(&[Composition], 0), tabs(&[Timeline], 0)),
// Review: a large viewer with the markers and the project beside it.
"Review" => vsplit(
Ratio(0.66),
hsplit(FixedA(280.0), tabs(&[Project, Markers], 0), hsplit(FixedB(260.0), tabs(&[Composition, Layer], 0), right(&[Preview, Info], &[Audio]))),
tabs(&[Timeline, RenderQueue], 0),
),
// Essential Graphics: the Properties panel (After Effects' successor to the Essential
// Graphics panel) full height on the right, type panels beside the viewer.
"Essential Graphics" => hsplit(
FixedB(320.0),
vsplit(
Ratio(0.58),
hsplit(
FixedA(280.0),
tabs(&[Project, EffectControls], 0),
hsplit(FixedB(260.0), tabs(&[Composition, Layer], 0), right(&[Character, Paragraph], &[Align, Preview])),
),
tabs(&[Timeline], 0),
),
stack(&[(Properties, true, None), (EffectsPresets, false, None)]),
),
// Color: wide Effect Controls for grading effects, Info for colour readouts.
"Color" => vsplit(
Ratio(0.6),
hsplit(
FixedA(360.0),
tabs(&[EffectControls, Project], 0),
hsplit(FixedB(300.0), tabs(&[Composition, Layer], 0), right(&[Info, Preview], &[EffectsPresets])),
),
tabs(&[Timeline], 0),
),
// Undocked Panels: the Composition panel docked, the others floating
// ([`workspace_floating`]).
"Undocked Panels" => tabs(&[Composition, Layer, Flowchart], 0),
"Small Screen" => vsplit(
Ratio(0.55),
hsplit(
@ -814,8 +884,59 @@ mod tests {
fn workspaces_contain_core_panels() {
for w in WORKSPACES {
let d = workspace(w);
assert!(d.contains(PanelKind::Timeline), "{w}");
let floating: Vec<PanelKind> = workspace_floating(w).into_iter().flat_map(|f| f.panels).collect();
assert!(d.contains(PanelKind::Timeline) || floating.contains(&PanelKind::Timeline), "{w}");
assert!(d.contains(PanelKind::Composition) || d.contains(PanelKind::Layer), "{w}");
// A panel is in one place only.
let mut all = vec![];
d.panels(&mut all);
all.extend(floating);
let n = all.len();
all.sort_by_key(|p| p.id());
all.dedup();
assert_eq!(all.len(), n, "{w} has a panel twice");
}
}
#[test]
fn after_effects_workspaces_exist() {
// Window ▸ Workspace in After Effects 2026, less Libraries (an Adobe service).
for w in [
"Default",
"Review",
"Learn",
"Small Screen",
"Standard",
"All Panels",
"Animation",
"Essential Graphics",
"Color",
"Effects",
"Minimal",
"Motion Tracking",
"Paint",
"Text",
"Undocked Panels",
] {
assert!(WORKSPACES.contains(&w), "{w}");
}
assert!(!WORKSPACES.contains(&"Libraries"));
// Layouts differ from Default (except Learn, which is Default plus the Home screen).
let def = workspace("Default");
for w in WORKSPACES.iter().filter(|w| !matches!(**w, "Default" | "Learn")) {
assert_ne!(workspace(w), def, "{w} falls back to Default");
}
assert!(workspace("Essential Graphics").contains(PanelKind::Properties));
assert!(workspace("Color").contains(PanelKind::EffectControls));
assert!(!workspace_floating("Undocked Panels").is_empty());
// Every built-in workspace is in Window ▸ Workspace.
let menu: Vec<String> = effectcraft_engine::menus::entries()
.into_iter()
.filter(|(_, e)| e.command == "window.workspace")
.filter_map(|(_, e)| e.params.get("name").and_then(|v| v.as_str()).map(str::to_string))
.collect();
for w in WORKSPACES {
assert!(menu.iter().any(|m| m == w), "{w} missing from Window > Workspace");
}
}

View File

@ -279,8 +279,12 @@ impl EffectcraftApp {
pub fn set_workspace(&mut self, name: &str) {
self.ui.workspace = name.to_string();
self.ui.dock = self.ui.saved_workspaces.get(name).cloned().unwrap_or_else(|| dock::workspace(name));
self.ui.floating = self.ui.saved_floating.get(name).cloned().unwrap_or_default();
self.ui.floating = self.ui.saved_floating.get(name).cloned().unwrap_or_else(|| dock::workspace_floating(name));
self.ui.maximized = None;
// Learn: the Home screen (community links) in the Composition panel.
if name == "Learn" {
self.ui.start_screen = true;
}
}
/// Built-in workspaces followed by the saved ones (Window ▸ Workspace ▸ Save as New Workspace).