mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Merge pull request #1317
# Conflicts: # crates/ui-egui/src/panels/effects_presets.rs # crates/ui-egui/src/panels/timeline.rs
This commit is contained in:
@@ -1407,14 +1407,18 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
ui.set_clip_rect(panel_clip);
|
||||
let content_h = y + scroll.offset - body.min.y;
|
||||
scroll.end(ui, &mut app.auto, "effectControls.scroll", content_h, &t);
|
||||
// Drop effects here.
|
||||
// Drop effects and animation presets here.
|
||||
if let Some(payload) = egui::DragAndDrop::payload::<crate::panels::DragPayload>(&ctx)
|
||||
&& ui.rect_contains_pointer(rect)
|
||||
&& let crate::panels::DragPayload::Effect(e) = payload.as_ref()
|
||||
&& let Some((id, params)) = match payload.as_ref() {
|
||||
crate::panels::DragPayload::Effect(e) => Some(("effect.apply", json!({"effect": e, "layers": [layer.id.0]}))),
|
||||
crate::panels::DragPayload::Preset { preset, .. } => Some(preset.apply_to(layer.id.0)),
|
||||
_ => None,
|
||||
}
|
||||
{
|
||||
p.rect_stroke(rect, 0.0, Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
if ctx.input(|i| i.pointer.any_released()) {
|
||||
actions.push(("effect.apply".into(), json!({"effect": e, "layers": [layer.id.0]})));
|
||||
actions.push((id.into(), params));
|
||||
egui::DragAndDrop::clear_payload(&ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//! filters, animation presets on/off, categories or alphabetical, refresh), Favorites (star an
|
||||
//! effect), Recently Used, "* Animation Presets" (EffectCraft's own text animation and transition
|
||||
//! presets and the user's `.ecpreset` files) and the effect categories. Double-click applies to the
|
||||
//! selected layers; dragging an effect onto a layer (timeline, viewer, Effect Controls) applies
|
||||
//! it there.
|
||||
//! selected layers; dragging an effect or an animation preset onto a layer (timeline, viewer,
|
||||
//! Effect Controls) applies it there.
|
||||
|
||||
use crate::i18n::tr;
|
||||
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
|
||||
@@ -11,7 +11,7 @@ use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::panels::DragPayload;
|
||||
use crate::panels::{DragPayload, PresetRef};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{EffectcraftApp, widgets};
|
||||
|
||||
@@ -108,10 +108,8 @@ fn highlighted(p: &egui::Painter, pos: egui::Pos2, name: &str, query: &str, t: &
|
||||
|
||||
enum Apply {
|
||||
Effect(String),
|
||||
TextPreset(String),
|
||||
/// A built-in animation preset (Transitions…) by id.
|
||||
Builtin(String),
|
||||
PresetFile(String),
|
||||
/// A text, built-in (Transitions…) or user file animation preset.
|
||||
Preset(PresetRef),
|
||||
}
|
||||
|
||||
/// A folder row; returns whether it is open.
|
||||
@@ -216,7 +214,7 @@ fn effect_row(
|
||||
}
|
||||
}
|
||||
|
||||
/// A preset row (no drag; double-click applies).
|
||||
/// A preset row: double-click applies to the selected layers; drag it onto a layer to apply it there.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn preset_row(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -227,7 +225,7 @@ fn preset_row(
|
||||
name: &str,
|
||||
auto_id: &str,
|
||||
query: &str,
|
||||
on_apply: impl FnOnce() -> Apply,
|
||||
preset: PresetRef,
|
||||
apply: &mut Option<Apply>,
|
||||
) {
|
||||
let t = app.tokens;
|
||||
@@ -236,15 +234,18 @@ fn preset_row(
|
||||
if r.max.y < list.min.y || r.min.y > list.max.y {
|
||||
return;
|
||||
}
|
||||
let resp = ui.interact(r.intersect(list), egui::Id::new(("fxpreset", auto_id)), Sense::click());
|
||||
let resp = ui.interact(r.intersect(list), egui::Id::new(("fxpreset", auto_id)), Sense::click_and_drag());
|
||||
if resp.hovered() {
|
||||
lp.rect_filled(r, 0.0, t.hover);
|
||||
}
|
||||
icons::paint(lp, Rect::from_center_size(pos2(r.min.x + 58.0, r.center().y), vec2(12.0, 12.0)), Icon::Keyframe, t.text_dim);
|
||||
highlighted(lp, pos2(r.min.x + 70.0, r.center().y), name, query, &t);
|
||||
app.auto.add(auto_id, r, name);
|
||||
if resp.drag_started() {
|
||||
egui::DragAndDrop::set_payload(ui.ctx(), DragPayload::Preset { preset: preset.clone(), name: name.to_string() });
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
*apply = Some(on_apply());
|
||||
*apply = Some(Apply::Preset(preset));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +396,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
if any && folder_row(app, ui, &lp, list, &mut y, "presets", "* Animation Presets", 0.0, searching, None) {
|
||||
if !text.is_empty() && folder_row(app, ui, &lp, list, &mut y, "presets/Text", "Text", 14.0, searching, None) {
|
||||
for (id, name) in text {
|
||||
preset_row(app, ui, &lp, list, &mut y, &name, &format!("effects.preset.text.{id}"), &query, || Apply::TextPreset(id.clone()), &mut apply);
|
||||
preset_row(app, ui, &lp, list, &mut y, &name, &format!("effects.preset.text.{id}"), &query, PresetRef::Text(id.clone()), &mut apply);
|
||||
}
|
||||
}
|
||||
for cat in &categories {
|
||||
@@ -403,7 +404,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
continue;
|
||||
}
|
||||
for (id, name, _) in builtin.iter().filter(|(_, _, c)| c == cat) {
|
||||
preset_row(app, ui, &lp, list, &mut y, name, &format!("effects.preset.builtin.{id}"), &query, || Apply::Builtin(id.clone()), &mut apply);
|
||||
preset_row(app, ui, &lp, list, &mut y, name, &format!("effects.preset.builtin.{id}"), &query, PresetRef::Builtin(id.clone()), &mut apply);
|
||||
}
|
||||
}
|
||||
if !files.is_empty() && folder_row(app, ui, &lp, list, &mut y, "presets/User", "User Presets", 14.0, searching, None) {
|
||||
@@ -417,7 +418,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
&f.name,
|
||||
&format!("effects.preset.file.{}", f.name),
|
||||
&query,
|
||||
|| Apply::PresetFile(f.path.clone()),
|
||||
PresetRef::File(f.path.clone()),
|
||||
&mut apply,
|
||||
);
|
||||
}
|
||||
@@ -455,10 +456,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let content = y + scroll.offset - list.min.y;
|
||||
scroll.end(ui, &mut app.auto, "effects.scroll", content, &t);
|
||||
if let Some(DragPayload::Effect(id)) = egui::DragAndDrop::payload::<DragPayload>(&ctx).as_deref()
|
||||
&& let Some(pos) = ctx.pointer_hover_pos()
|
||||
if let Some(name) = egui::DragAndDrop::payload::<DragPayload>(&ctx).and_then(|p| match p.as_ref() {
|
||||
DragPayload::Effect(id) => Some(effectcraft_engine::effects::find(id).map(|e| crate::i18n::effects::name(e.id, e.name)).unwrap_or_default()),
|
||||
DragPayload::Preset { name, .. } => Some(name.clone()),
|
||||
_ => None,
|
||||
}) && let Some(pos) = ctx.pointer_hover_pos()
|
||||
{
|
||||
let name = effectcraft_engine::effects::find(id).map(|e| crate::i18n::effects::name(e.id, e.name)).unwrap_or_default();
|
||||
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("dnd-fx")));
|
||||
let g = painter.layout_no_wrap(name, Tokens::ui(12.0), t.text);
|
||||
let r = Rect::from_min_size(pos + vec2(12.0, 8.0), g.size() + vec2(16.0, 8.0));
|
||||
@@ -473,9 +476,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
let (cmd, params) = match &apply {
|
||||
Apply::Effect(id) => ("effect.apply", json!({"effect": id})),
|
||||
Apply::TextPreset(id) => ("layer.applyTextPreset", json!({"preset": id})),
|
||||
Apply::Builtin(id) => ("anim.applyBuiltinPreset", json!({"preset": id})),
|
||||
Apply::PresetFile(path) => ("anim.applyPreset", json!({"path": path})),
|
||||
Apply::Preset(preset) => preset.command(),
|
||||
};
|
||||
match app.session.execute(cmd, params) {
|
||||
Err(e) => app.ui.status = e.to_string(),
|
||||
|
||||
@@ -76,12 +76,49 @@ pub enum DragPayload {
|
||||
Item(u64),
|
||||
/// An effect id from Effects & Presets.
|
||||
Effect(String),
|
||||
/// An animation preset from Effects & Presets, with its display name for the drag ghost.
|
||||
Preset { preset: PresetRef, name: String },
|
||||
/// A property dragged from the timeline (to the Essential Graphics panel).
|
||||
Property { layer: u64, prop: u64 },
|
||||
/// Files from the Media Browser (import; into the timeline: import and add).
|
||||
Files(Vec<String>),
|
||||
}
|
||||
|
||||
/// An animation preset in Effects & Presets.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PresetRef {
|
||||
/// A text animation preset id (`layer.applyTextPreset`).
|
||||
Text(String),
|
||||
/// A built-in animation preset id, e.g. a transition (`anim.applyBuiltinPreset`).
|
||||
Builtin(String),
|
||||
/// A user `.ecpreset` file path (`anim.applyPreset`).
|
||||
File(String),
|
||||
}
|
||||
|
||||
impl PresetRef {
|
||||
/// The command and params that apply the preset to the selected layers.
|
||||
pub fn command(&self) -> (&'static str, serde_json::Value) {
|
||||
match self {
|
||||
PresetRef::Text(id) => ("layer.applyTextPreset", serde_json::json!({"preset": id})),
|
||||
PresetRef::Builtin(id) => ("anim.applyBuiltinPreset", serde_json::json!({"preset": id})),
|
||||
PresetRef::File(path) => ("anim.applyPreset", serde_json::json!({"path": path})),
|
||||
}
|
||||
}
|
||||
|
||||
/// The command and params that apply the preset to `layer` (a drop target).
|
||||
pub fn apply_to(&self, layer: u64) -> (&'static str, serde_json::Value) {
|
||||
let (id, mut params) = self.command();
|
||||
if let Some(obj) = params.as_object_mut() {
|
||||
// `layer.applyTextPreset` takes one `layer`; the animation preset commands take `layers`.
|
||||
match self {
|
||||
PresetRef::Text(_) => obj.insert("layer".into(), serde_json::json!(layer)),
|
||||
PresetRef::Builtin(_) | PresetRef::File(_) => obj.insert("layers".into(), serde_json::json!([layer])),
|
||||
};
|
||||
}
|
||||
(id, params)
|
||||
}
|
||||
}
|
||||
|
||||
/// Layers carried by a Project item drag. Dragging a selected item uses the
|
||||
/// complete selection, while dragging an unselected item uses only that item.
|
||||
/// Folders and data files are not composition layers.
|
||||
@@ -250,3 +287,18 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
|
||||
ui.ctx().data_mut(|d| d.remove::<(PanelKind, egui::Pos2)>(id));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod preset_drop_tests {
|
||||
use super::PresetRef;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn preset_drops_target_the_layer_with_each_commands_param() {
|
||||
assert_eq!(PresetRef::Text("typewriter".into()).apply_to(7), ("layer.applyTextPreset", json!({"preset": "typewriter", "layer": 7})));
|
||||
assert_eq!(PresetRef::Builtin("fadeIn".into()).apply_to(7), ("anim.applyBuiltinPreset", json!({"preset": "fadeIn", "layers": [7]})));
|
||||
assert_eq!(PresetRef::File("/p/a.ecpreset".into()).apply_to(7), ("anim.applyPreset", json!({"path": "/p/a.ecpreset", "layers": [7]})));
|
||||
// Double-click applies to the selection: no layer given.
|
||||
assert_eq!(PresetRef::Text("typewriter".into()).command(), ("layer.applyTextPreset", json!({"preset": "typewriter"})));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3346,7 +3346,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
// Drop targets: effects from Effects & Presets go on the layer under the pointer. Footage and
|
||||
// Drop targets: effects and animation presets from Effects & Presets go on the layer under the
|
||||
// pointer. Footage and
|
||||
// comps from the Project panel and files from the Media Browser go where they are dropped: a
|
||||
// line shows the place in the stack, and over the time graph a marker shows the In point too
|
||||
// (Shift: at the current time).
|
||||
@@ -3355,7 +3356,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
&& let Some(ptr) = ctx.input(|i| i.pointer.hover_pos()).filter(|_| ui.rect_contains_pointer(rows_rect))
|
||||
{
|
||||
let released = ctx.input(|i| i.pointer.any_released());
|
||||
if let crate::panels::DragPayload::Effect(e) = payload.as_ref() {
|
||||
if matches!(payload.as_ref(), crate::panels::DragPayload::Effect(_) | crate::panels::DragPayload::Preset { .. }) {
|
||||
let target = hit_rows.iter().find(|(r, _)| ptr.y >= r.min.y && ptr.y < r.max.y).map(|(_, row)| row.layer);
|
||||
// The layer the effect would land on (its row and its open properties) is the one
|
||||
// that lights up, not the whole timeline.
|
||||
@@ -3369,7 +3370,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
app.auto.add("timeline.dropTarget", lit, "Drop target layer");
|
||||
}
|
||||
if released && let Some(l) = target {
|
||||
actions.push(("effect.apply".into(), json!({"effect": e, "layers": [l.0]})));
|
||||
match payload.as_ref() {
|
||||
crate::panels::DragPayload::Effect(e) => actions.push(("effect.apply".into(), json!({"effect": e, "layers": [l.0]}))),
|
||||
crate::panels::DragPayload::Preset { preset, .. } => {
|
||||
let (id, params) = preset.apply_to(l.0);
|
||||
actions.push((id.into(), params));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let layer_rows: Vec<(Rect, LayerId)> = hit_rows.iter().filter(|(_, r)| matches!(r.kind, RowKind::Layer)).map(|(rr, r)| (*rr, r.layer)).collect();
|
||||
@@ -3405,7 +3413,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
crate::panels::DragPayload::Files(paths) => {
|
||||
actions.push(("mediaBrowser.import".into(), json!({"paths": paths, "addToComp": true, "index": index, "time": time})))
|
||||
}
|
||||
crate::panels::DragPayload::Effect(_) | crate::panels::DragPayload::Property { .. } => {}
|
||||
crate::panels::DragPayload::Effect(_) | crate::panels::DragPayload::Preset { .. } | crate::panels::DragPayload::Property { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Dropping onto the Composition viewer: footage, comps and solids from the
|
||||
//! Project panel and files from the Media Browser become layers centred where they are dropped
|
||||
//! (#85), and an effect from Effects & Presets goes on the layer under the pointer, outlined
|
||||
//! while it is dragged (#88). Agents do the same with `layer.addItem {position}`,
|
||||
//! `mediaBrowser.import {position}` and `effect.apply {layers}`.
|
||||
//! (#85), and an effect or animation preset from Effects & Presets goes on the layer under the
|
||||
//! pointer, outlined while it is dragged (#88). Agents do the same with `layer.addItem {position}`,
|
||||
//! `mediaBrowser.import {position}`, `effect.apply {layers}` and the preset commands.
|
||||
|
||||
use effectcraft_engine::project::{ItemId, ItemKind};
|
||||
use effectcraft_engine::render::EvalCtx;
|
||||
@@ -33,6 +33,15 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &egui::Ui, painter: &egui::Pain
|
||||
})
|
||||
.into_iter()
|
||||
.collect(),
|
||||
DragPayload::Preset { preset, .. } => layer_at(ectx, at)
|
||||
.map(|l| {
|
||||
if let Some((_, q, _)) = layer_quad(ectx, l) {
|
||||
painter.add(egui::Shape::closed_line(q.iter().map(|c| map.to_screen(*c)).collect(), stroke));
|
||||
}
|
||||
preset.apply_to(l.id.0)
|
||||
})
|
||||
.into_iter()
|
||||
.collect(),
|
||||
DragPayload::Item(item) => {
|
||||
// The layer's frame, centred on the pointer.
|
||||
if let Some([w, h]) = app.session.project.item(ItemId(*item)).and_then(|it| frame_size(&it.kind, ectx.comp.pixel_aspect)) {
|
||||
@@ -45,7 +54,9 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &egui::Ui, painter: &egui::Pain
|
||||
DragPayload::Files(paths) => vec![("mediaBrowser.import", json!({"paths": paths, "addToComp": true, "position": at}))],
|
||||
DragPayload::Property { .. } => vec![],
|
||||
};
|
||||
if actions.first().is_some_and(|(id, _)| *id != "effect.apply") {
|
||||
// Layer drops outline the target layer; the others outline the whole viewer.
|
||||
let layer_drop = matches!(payload.as_ref(), DragPayload::Effect(_) | DragPayload::Preset { .. });
|
||||
if !layer_drop && !actions.is_empty() {
|
||||
painter.rect_stroke(map.area, 0.0, stroke, StrokeKind::Inside);
|
||||
}
|
||||
if ctx.input(|i| i.pointer.any_released()) {
|
||||
|
||||
Reference in New Issue
Block a user