mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-15 03:37:04 -04:00
Merge pull request #1044 from pravdomil/claude/timeline-chevron-expand-tree-93231f
This commit is contained in:
commit
e248c172e8
6 files changed
+182
-20
No files matched your search
@@ -646,6 +646,27 @@ impl<'a> FxScope<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Twirl `g` closed (it was `open`) or open; with `all` (Alt/Cmd-click) every group under it follows.
|
||||
fn toggle_closed(app: &mut EffectcraftApp, g: &PropGroup, open: bool, all: bool) {
|
||||
fn walk(g: &PropGroup, out: &mut Vec<u64>) {
|
||||
for sg in g.groups() {
|
||||
out.push(sg.uid);
|
||||
walk(sg, out);
|
||||
}
|
||||
}
|
||||
let mut uids = vec![g.uid];
|
||||
if all {
|
||||
walk(g, &mut uids);
|
||||
}
|
||||
for u in uids {
|
||||
if open {
|
||||
app.ui.fx_closed.insert(u);
|
||||
} else {
|
||||
app.ui.fx_closed.remove(&u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn group_rows(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -705,11 +726,7 @@ fn group_rows(
|
||||
app.auto.add(&format!("effectControls.row.{}", sg.uid), r, &sg.name);
|
||||
app.auto.add(&format!("effectControls.group.{}.twirl", sg.uid), tw, &sg.name);
|
||||
if widgets::twirl(ui, tw, open, egui::Id::new(("ec-g", sg.uid)), &t).clicked() {
|
||||
if open {
|
||||
app.ui.fx_closed.insert(sg.uid);
|
||||
} else {
|
||||
app.ui.fx_closed.remove(&sg.uid);
|
||||
}
|
||||
toggle_closed(app, sg, open, ui.input(|i| i.modifiers.alt || i.modifiers.command));
|
||||
}
|
||||
let name = p.text(pos2(tw.max.x + 6.0, r.center().y), Align2::LEFT_CENTER, &sg.name, Tokens::ui(12.0), t.text);
|
||||
app.auto.add(&format!("effectControls.group.{}.name", sg.uid), name, &sg.name);
|
||||
@@ -1114,11 +1131,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
app.auto.add(&format!("effectControls.effect.{}.fx", g.uid), fxr, &g.name);
|
||||
let tw = Rect::from_center_size(pos2(r.min.x + 32.0, r.center().y), vec2(12.0, 12.0));
|
||||
if widgets::twirl(ui, tw, open, egui::Id::new(("ec-tw", g.uid)), &t).clicked() {
|
||||
if open {
|
||||
app.ui.fx_closed.insert(g.uid);
|
||||
} else {
|
||||
app.ui.fx_closed.remove(&g.uid);
|
||||
}
|
||||
toggle_closed(app, g, open, ui.input(|i| i.modifiers.alt || i.modifiers.command));
|
||||
}
|
||||
app.auto.add(&format!("effectControls.effect.{}.twirl", g.uid), tw, &g.name);
|
||||
let rename_id = egui::Id::new(("ec-rename", g.uid));
|
||||
|
||||
@@ -579,10 +579,21 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
egui::WidgetInfo::labeled(egui::WidgetType::Button, true, name)
|
||||
});
|
||||
if tresp.clicked() {
|
||||
if open {
|
||||
app.ui.project_open_folders.remove(&id.0);
|
||||
} else {
|
||||
app.ui.project_open_folders.insert(id.0);
|
||||
// Alt/Cmd-click twirls every folder inside it too.
|
||||
let mut ids = vec![*id];
|
||||
if ui.input(|i| i.modifiers.alt || i.modifiers.command) {
|
||||
let mut at = 0;
|
||||
while let Some(f) = ids.get(at).copied() {
|
||||
ids.extend(app.session.project.children(Some(f)).into_iter().filter(|c| c.is_folder()).map(|c| c.id));
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
for f in ids {
|
||||
if open {
|
||||
app.ui.project_open_folders.remove(&f.0);
|
||||
} else {
|
||||
app.ui.project_open_folders.insert(f.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
app.auto.add(&format!("project.item.{}.twirl", id.0), tw, &it.name);
|
||||
|
||||
@@ -2268,7 +2268,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
if *has_children {
|
||||
let tw = Rect::from_center_size(pos2(indent, cy), vec2(13.0, 13.0));
|
||||
if widgets::twirl(ui, tw, *open, egui::Id::new(("gtw", uid)), &t).clicked() {
|
||||
ui_actions.push(UiAct::ToggleGroup(*uid));
|
||||
// Alt/Cmd-click twirls the whole tree under the group (as on a layer).
|
||||
ui_actions.push(UiAct::ToggleGroup(layer.id.0, *uid, ui.input(|i| i.modifiers.alt || i.modifiers.command)));
|
||||
}
|
||||
app.auto.add(&format!("timeline.group.{uid}.twirl"), tw, name);
|
||||
}
|
||||
@@ -2337,7 +2338,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
actions.push(("prop.select".into(), json!({"layer": layer.id.0, "prop": uid, "selectKeys": false})));
|
||||
actions.push(("window.panel".into(), json!({"panel": "EffectControls"})));
|
||||
} else {
|
||||
ui_actions.push(UiAct::ToggleGroup(*uid));
|
||||
ui_actions.push(UiAct::ToggleGroup(layer.id.0, *uid, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3233,9 +3234,22 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
}
|
||||
}
|
||||
UiAct::ToggleGroup(uid) => {
|
||||
if !app.ui.timeline.open_groups.remove(&uid) {
|
||||
app.ui.timeline.open_groups.insert(uid);
|
||||
UiAct::ToggleGroup(layer, uid, all) => {
|
||||
let tl = &mut app.ui.timeline;
|
||||
let open = !tl.open_groups.remove(&uid);
|
||||
if open {
|
||||
tl.open_groups.insert(uid);
|
||||
}
|
||||
if all && let Some(g) = comp.layer(LayerId(layer)).and_then(|l| find_group(&l.props, uid)) {
|
||||
let mut uids = vec![];
|
||||
walk_groups(g, &mut uids);
|
||||
for u in uids {
|
||||
if open {
|
||||
tl.open_groups.insert(u);
|
||||
} else {
|
||||
tl.open_groups.remove(&u);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UiAct::SetTime(tt) => app.session.set_time(tt),
|
||||
@@ -3359,7 +3373,8 @@ fn layer_cells(
|
||||
|
||||
enum UiAct {
|
||||
ToggleLayer(u64, bool),
|
||||
ToggleGroup(u64),
|
||||
/// Layer, group uid, and whether the whole tree under the group follows.
|
||||
ToggleGroup(u64, u64, bool),
|
||||
SetTime(Tick),
|
||||
EndMerge,
|
||||
/// Composition Settings on its 3D Renderer tab.
|
||||
@@ -3367,12 +3382,21 @@ enum UiAct {
|
||||
}
|
||||
|
||||
fn walk_groups(g: &PropGroup, out: &mut Vec<u64>) {
|
||||
// Audio's Waveform row is not in the property tree; it twirls along.
|
||||
if g.match_id == "audio" {
|
||||
out.push(WAVE_BIT | g.uid);
|
||||
}
|
||||
for sg in g.groups() {
|
||||
out.push(sg.uid);
|
||||
walk_groups(sg, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// The group `uid` anywhere under `g`.
|
||||
fn find_group(g: &PropGroup, uid: u64) -> Option<&PropGroup> {
|
||||
g.groups().find_map(|sg| if sg.uid == uid { Some(sg) } else { find_group(sg, uid) })
|
||||
}
|
||||
|
||||
/// A stroke's Dashes "+" / "−" buttons (add or remove a Dash/Gap pair).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn dash_buttons(
|
||||
@@ -3940,6 +3964,21 @@ fn value_editor(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Alt/Cmd-click twirls Audio's Waveform row (not in the property tree) with the rest.
|
||||
#[test]
|
||||
fn the_waveform_row_twirls_with_its_tree() {
|
||||
let mut root = PropGroup::new(1, "", "");
|
||||
let mut audio = PropGroup::new(2, "audio", "Audio");
|
||||
audio.children.push(PropGroup::new(3, "x", "X").into());
|
||||
root.children.push(audio.into());
|
||||
let mut uids = vec![];
|
||||
walk_groups(&root, &mut uids);
|
||||
assert_eq!(uids, vec![2, WAVE_BIT | 2, 3]);
|
||||
let mut uids = vec![];
|
||||
walk_groups(find_group(&root, 2).unwrap(), &mut uids);
|
||||
assert_eq!(uids, vec![WAVE_BIT | 2, 3]);
|
||||
}
|
||||
|
||||
/// #710: with Time Display set to Frames the ruler labels are frame numbers (as under the
|
||||
/// CTI, from Start Numbering Frames At); otherwise seconds:frames.
|
||||
#[test]
|
||||
|
||||
@@ -220,6 +220,34 @@ fn keyer_eyedropper_picks_the_screen_from_the_effect_input() {
|
||||
assert!(img.get(160, 90)[3] < 0.01, "{:?}", img.get(160, 90));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_clicking_an_effect_twirl_closes_and_opens_its_groups() {
|
||||
let (app, x) = app();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app);
|
||||
let fx = h.state_mut().session.execute("effect.apply", json!({"layers": [x.small], "effect": "Lumetri Color"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
// Only this effect open, so its header is on screen.
|
||||
let l = layer(&h.state().session, x.small);
|
||||
let others: Vec<u64> = l.effects().unwrap().groups().map(|g| g.uid).filter(|u| *u != fx).collect();
|
||||
h.state_mut().ui.fx_closed.extend(others.iter().copied());
|
||||
h.run_steps(3);
|
||||
let inner: Vec<u64> = l.effects().unwrap().groups().find(|g| g.uid == fx).unwrap().groups().map(|g| g.uid).collect();
|
||||
assert!(!inner.is_empty(), "the effect has twirl-down groups");
|
||||
let cmd_click = |h: &mut Harness<'_, EffectcraftApp>| {
|
||||
let at = rect_of(h, &format!("effectControls.effect.{fx}.twirl")).center();
|
||||
h.event(egui::Event::ModifiersChanged(egui::Modifiers::COMMAND));
|
||||
click(h, at);
|
||||
h.event(egui::Event::ModifiersChanged(Default::default()));
|
||||
h.step();
|
||||
};
|
||||
cmd_click(&mut h);
|
||||
let closed = h.state().ui.fx_closed.clone();
|
||||
assert!(closed.contains(&fx) && inner.iter().all(|u| closed.contains(u)), "the effect and its groups twirl closed");
|
||||
cmd_click(&mut h);
|
||||
let closed = h.state().ui.fx_closed.clone();
|
||||
assert!(!closed.contains(&fx) && !inner.iter().any(|u| closed.contains(u)), "and open again together");
|
||||
assert!(others.iter().all(|u| closed.contains(u)), "other effects stay as they were");
|
||||
}
|
||||
|
||||
fn rect_of(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).unwrap_or_else(|| panic!("no {id}")).clone();
|
||||
egui::Rect::from_min_size(egui::pos2(e.rect[0], e.rect[1]), egui::vec2(e.rect[2], e.rect[3]))
|
||||
|
||||
@@ -59,6 +59,31 @@ fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
|
||||
click_at(h, p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_clicking_a_folder_twirl_opens_the_folders_inside_it() {
|
||||
let (mut h, folder, _, _) = harness();
|
||||
let s = &mut h.state_mut().session;
|
||||
let inner = s.execute("project.newFolder", json!({"name": "Inner"})).unwrap()["item"].as_u64().unwrap();
|
||||
let deep = s.execute("project.newFolder", json!({"name": "Deep"})).unwrap()["item"].as_u64().unwrap();
|
||||
s.execute("project.move", json!({"items": [inner], "folder": folder.0})).unwrap();
|
||||
s.execute("project.move", json!({"items": [deep], "folder": inner})).unwrap();
|
||||
s.execute("project.select", json!({"items": []})).unwrap();
|
||||
h.state_mut().ui.project_open_folders.clear();
|
||||
h.run_steps(3);
|
||||
let p = center(&h, &format!("project.item.{}.twirl", folder.0));
|
||||
let cmd_click = |h: &mut Harness<'_, EffectcraftApp>| {
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::COMMAND));
|
||||
click_at(h, p);
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
|
||||
h.step();
|
||||
};
|
||||
cmd_click(&mut h);
|
||||
let open = h.state().ui.project_open_folders.clone();
|
||||
assert!([folder.0, inner, deep].iter().all(|f| open.contains(f)), "the folder and every folder inside it open");
|
||||
cmd_click(&mut h);
|
||||
assert!(h.state().ui.project_open_folders.is_empty(), "and close again together");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folder_twirl_opens_and_closes_the_folder_on_one_click() {
|
||||
let (mut h, folder, comp, _) = harness();
|
||||
|
||||
@@ -1570,6 +1570,52 @@ fn timeline_twirl_arrows_open_layers_and_groups() {
|
||||
assert!(!shown(&h, prop_uid(&h, "Box", "transform/position")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_clicking_a_group_twirl_opens_its_whole_tree() {
|
||||
// Cmd-click (or Alt-click) the arrow of Effects, Contents, Masks…: every group under it
|
||||
// twirls open with it, and closed again with it.
|
||||
let mut h = harness();
|
||||
let id = layer_id(&h, "Box");
|
||||
h.state_mut().session.execute("effect.apply", json!({"effect": "Lumetri Color", "layers": [id]})).unwrap();
|
||||
fn walk(g: &effectcraft_engine::project::PropGroup, out: &mut Vec<u64>) {
|
||||
for sg in g.groups() {
|
||||
out.push(sg.uid);
|
||||
walk(sg, out);
|
||||
}
|
||||
}
|
||||
let (effects, inner, transform) = {
|
||||
let comp = h.state().session.active_comp().unwrap().clone();
|
||||
let props = &comp.layer(LayerId(id)).unwrap().props;
|
||||
let fx = props.sub("effects").unwrap();
|
||||
let mut inner = vec![];
|
||||
walk(fx, &mut inner);
|
||||
(fx.uid, inner, props.sub("transform").unwrap().uid)
|
||||
};
|
||||
assert!(inner.len() > 1, "the effect has twirl-down groups of its own");
|
||||
let at = rect(&h, &format!("timeline.layer.{id}.twirl")).center();
|
||||
click(&mut h, at);
|
||||
let at = rect(&h, &format!("timeline.group.{effects}.twirl")).center();
|
||||
let cmd_click = |h: &mut Harness<'_, EffectcraftApp>| {
|
||||
h.input_mut().events.push(Event::ModifiersChanged(egui::Modifiers::COMMAND));
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.step();
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::COMMAND });
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Default::default()));
|
||||
h.step();
|
||||
};
|
||||
cmd_click(&mut h);
|
||||
let open = h.state().ui.timeline.open_groups.clone();
|
||||
assert!(open.contains(&effects), "Effects twirls open");
|
||||
assert!(inner.iter().all(|u| open.contains(u)), "and so does everything under it");
|
||||
assert!(!open.contains(&transform), "other groups stay as they were");
|
||||
cmd_click(&mut h);
|
||||
let open = h.state().ui.timeline.open_groups.clone();
|
||||
assert!(!open.contains(&effects) && !inner.iter().any(|u| open.contains(u)), "Cmd-click again closes the tree");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeline_rename_commits_on_enter_and_on_click_away() {
|
||||
// The rename field (double-click a name, or Enter on a selected layer) used to grab the
|
||||
|
||||
Reference in new issue
Block a user