mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 21:48:20 -06:00
M-egui037: document migration behavior and guard atomic batch rollback
This commit is contained in:
@@ -86,6 +86,8 @@ pub struct WebApp {
|
||||
}
|
||||
|
||||
impl eframe::App for WebApp {
|
||||
/// Take dropped files out of `raw_input` before eframe can replay them on a hidden tab, read them asynchronously
|
||||
/// through `open_or_import`, then run the shared app's input hook.
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
|
||||
// Files dropped on the page are input: read them here, where the browser's drop event is
|
||||
// still the current pass's, not from a later (possibly logic-only) tick.
|
||||
@@ -101,6 +103,9 @@ impl eframe::App for WebApp {
|
||||
self.app.raw_input_hook(ctx, raw_input);
|
||||
}
|
||||
|
||||
/// The browser host's logic tick: run queued UI actions, tick the shared app, poll API replies, flush Render
|
||||
/// Queue downloads, persist the session snapshot when enabled, publish GPU/frame-worker/disk-cache info and close
|
||||
/// the IME pass.
|
||||
fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
let actions = ACTIONS.with(|a| std::mem::take(&mut *a.borrow_mut()));
|
||||
for f in actions {
|
||||
|
||||
@@ -138,6 +138,10 @@ fn install_file_dialogs(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
let _ = (app, ctx);
|
||||
}
|
||||
|
||||
/// Run the desktop app: parse arguments (`--version` exits before any file is written), install logging and the panic
|
||||
/// hook, attach the session log, scan fonts, honor the launch guard's backend choice and open the eframe window.
|
||||
/// Builds the session, loads settings, plugins, the requested project/media and the optional control server, then
|
||||
/// reports startup failure through `startup_failed`. Returns the event loop's result.
|
||||
fn main() -> eframe::Result {
|
||||
// First, so the panic hook and every start-up warning are recorded. Records go to stderr
|
||||
// and the session log; Help ▸ Enable Logging writes through this logger too, and warnings
|
||||
@@ -432,6 +436,7 @@ struct Desktop {
|
||||
}
|
||||
|
||||
impl eframe::App for Desktop {
|
||||
/// The desktop host's logic tick: update the macOS menu bar, run the shared app tick and close the IME pass.
|
||||
fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
#[cfg(target_os = "macos")]
|
||||
self.menu.update(&mut self.app, ctx);
|
||||
|
||||
@@ -143,7 +143,7 @@ pub fn write(prefs: &Prefs, project: Option<&str>, fallback: Option<&Path>, last
|
||||
write_in(&StdFiles, prefs, project, fallback, last_slot, json)
|
||||
}
|
||||
|
||||
/// [`write`] through `fs`.
|
||||
/// [`write()`] through `fs`.
|
||||
pub fn write_in(
|
||||
fs: &dyn FileOps,
|
||||
prefs: &Prefs,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! camera"). When only the solve settings changed (Shot Type, Angle of View, Solve Method,
|
||||
//! deleted points) the stored tracks are re-solved without re-tracking. Results are written into
|
||||
//! the effect's hidden parameters as one undo step; edits that change the layer's frames clear
|
||||
//! them and queue the effect again ([`invalidate`], run after every edit), like After Effects.
|
||||
//! them and queue the effect again (`invalidate`, run after every edit), like After Effects.
|
||||
//!
|
||||
//! Composition space: the solve's canonical frame maps to comp world by
|
||||
//! [`CameraSolve::world`] with the zoom of the first solved frame (focal length × the layer's
|
||||
|
||||
@@ -354,7 +354,7 @@ fn assign_view_shortcut(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
reassign(s, view_command(cur), &slot, "view.assign3dShortcut")
|
||||
}
|
||||
|
||||
/// Window ▸ Assign Shortcut to "<workspace>" Workspace: Shift+F10, Shift+F11 or Shift+F12.
|
||||
/// Window ▸ Assign Shortcut to `"<workspace>"` Workspace: Shift+F10, Shift+F11 or Shift+F12.
|
||||
/// The frontend fills in the current workspace.
|
||||
fn assign_workspace_shortcut(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let slot = slot_p(p, "window.assignWorkspaceShortcut", true)?;
|
||||
@@ -668,6 +668,9 @@ fn extract_cubemap(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The specs for the commands in this module: history steps, recent footage and presets, Save
|
||||
/// a Copy As XML, footage replacement and Execute File, 3D-view and workspace shortcuts,
|
||||
/// logging and the system report, VR environment/cubemap creation and System Information.
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!("edit.history", "History", [], None, "{steps?: n (undo n steps), redo?: n}", has_history, history),
|
||||
|
||||
@@ -51,6 +51,17 @@ fn substitute(v: &Value, results: &[Value]) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// `engine.batch`: runs `steps` (`[{command|id, params?}]`) as engine commands, substituting
|
||||
/// `$N` / `$N.key.0` strings in each step's params with earlier results, and returns
|
||||
/// `{steps, results}`.
|
||||
///
|
||||
/// `engine.batch`, `file.runScript` and `script.run` are refused inside a batch. Successful
|
||||
/// steps are folded into one undo step named `label` (default "Batch"). With the default
|
||||
/// `atomic: true`, a failing step restores the project, history and editor state, drops the
|
||||
/// events the batch published, sanitizes the editor against the failing intermediate state (so
|
||||
/// no pre-batch IME token is revived) and keeps the higher process IME revision; with
|
||||
/// `atomic: false` the steps before the failure stay applied. Either way the error names the
|
||||
/// failing step and command.
|
||||
fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let steps = p.get("steps").and_then(Value::as_array).ok_or_else(|| bad("engine.batch", "missing `steps`: [{command, params?}, …]"))?.clone();
|
||||
let label = str_p(p, "label").unwrap_or("Batch").to_string();
|
||||
@@ -138,6 +149,32 @@ mod tests {
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// Failed atomic steps discard their UI events while preserving events queued beforehand.
|
||||
#[test]
|
||||
fn atomic_batch_event_rollback_preserves_prior_events() {
|
||||
for atomic in [true, false] {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "First", "width": 100, "height": 100})).unwrap();
|
||||
let first = s.active_comp_id().unwrap();
|
||||
s.execute("comp.new", json!({"name": "Second", "width": 100, "height": 100})).unwrap();
|
||||
let second = s.active_comp_id().unwrap();
|
||||
s.drain_events();
|
||||
s.execute("comp.open", json!({"comp": first.0})).unwrap();
|
||||
let result = s.execute_checked(
|
||||
"engine.batch",
|
||||
json!({"atomic": atomic, "steps": [
|
||||
{"command": "comp.open", "params": {"comp": second.0}},
|
||||
{"command": "no.suchCommand", "params": {}}
|
||||
]}),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(s.active_comp_id(), Some(if atomic { first } else { second }));
|
||||
let opened: Vec<_> =
|
||||
s.drain_events().into_iter().filter_map(|event| if let crate::Event::OpenComp(id) = event { Some(id) } else { None }).collect();
|
||||
assert_eq!(opened, if atomic { vec![first] } else { vec![first, second] });
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_refs_one_undo_step_and_rollback() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -149,6 +149,16 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!(new.iter().map(|l| l.0).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
/// `edit.clear`: delegates to `text.delete` while editing text (deleting the selection or
|
||||
/// preceding character/word), then handles selected Time Remap (turning remapping off), keyframes, loose mask
|
||||
/// vertices, puppet pins, effects, masks/shape items/text animators/trackers, and finally the
|
||||
/// selected (or given) unlocked layers. Layer and selected-property-group paths return the number
|
||||
/// removed; text, Time Remap, keyframe, loose-vertex, puppet-pin and effect paths return their
|
||||
/// delegated command's result.
|
||||
///
|
||||
/// Layer removal also unparents surviving children in place, clears track mattes that pointed
|
||||
/// at a removed layer and rewrites layer-reference values and keys to `None` in the same undo
|
||||
/// step; the text-edit session is then sanitized so a removed layer's editor is not reused.
|
||||
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if s.state.text_edit.is_some() && p.get("layers").is_none() {
|
||||
return s.execute("text.delete", json!({}));
|
||||
|
||||
@@ -162,6 +162,15 @@ fn text_range_p(p: &Value) -> Option<std::ops::Range<usize>> {
|
||||
Some(x.min(y)..x.max(y))
|
||||
}
|
||||
|
||||
/// `layer.newText`: adds a Text layer to the comp at the CTI and returns `{layer}`.
|
||||
///
|
||||
/// The document starts as centre-justified "Text", with the optional text parameters applied.
|
||||
/// With `box` `[x, y, w, h]`, `(x, y)` is the paragraph box's top-left in comp space. Its width
|
||||
/// and height are `w.abs().max(1.0)` and `h.abs().max(1.0)`; the layer position is the box centre.
|
||||
/// It is left justified unless `justify` is given. The layer is named by `name`, else the first 40
|
||||
/// characters of the first text line, else "Text". `edit: true` also opens a new editing
|
||||
/// session with the caret after the text and `created: true`, so leaving it empty removes the
|
||||
/// layer.
|
||||
fn new_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cid = comp_id(s, p)?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?.clone();
|
||||
@@ -1454,6 +1463,15 @@ fn shape_item_target(proj: &mut Project, cid: ItemId, lid: LayerId, group_uid: O
|
||||
})
|
||||
}
|
||||
|
||||
/// `layer.setText`: sets a text layer's Source Text and/or character attributes at the CTI.
|
||||
///
|
||||
/// A `font` name is also added to the recent fonts and the preferences saved. With an empty
|
||||
/// `range` and no `text`, while that layer is being edited and its document is non-empty,
|
||||
/// recognised character attributes are also staged in the pending insertion style without
|
||||
/// changing existing characters. Paragraph/document attributes still apply through the edit;
|
||||
/// the session is bumped whenever this branch runs. Document changes use [`Session::edit`],
|
||||
/// merged by `merge_p`. With `text` given, a layer whose name is empty or starts with `Text`
|
||||
/// takes the new first line (up to 40 characters) when that line is non-empty. Replies `null`.
|
||||
fn set_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, lid) = layer_p(s, p, "layer.setText")?;
|
||||
// The Character panel's recent fonts (Settings ▸ Type ▸ Number of Recent Fonts to Display).
|
||||
|
||||
@@ -234,7 +234,7 @@ fn uninstall(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if removed { Ok(json!({"removed": name})) } else { Err(bad(c, format!("`{name}` is not installed"))) }
|
||||
}
|
||||
|
||||
/// Run an installed or bundled script by name (File ▸ Scripts ▸ <name>). A ScriptUI panel run
|
||||
/// Run an installed or bundled script by name (File ▸ Scripts ▸ `<name>`). A ScriptUI panel run
|
||||
/// this way gets `this` = a floating palette-like panel window.
|
||||
pub(crate) fn run_named(s: &mut Session, name: &str) -> Result<Value> {
|
||||
let (panel, code) = script_code(s, name, None).ok_or_else(|| bad("file.runScript", format!("no installed or bundled script `{name}`")))?;
|
||||
@@ -308,7 +308,7 @@ fn restore_panels(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
Ok(json!({"opened": opened, "failed": failed}))
|
||||
}
|
||||
|
||||
/// Window ▸ <ScriptUI panel>: run the panel script (or bring its panel forward).
|
||||
/// Window ▸ `<ScriptUI panel>`: run the panel script (or bring its panel forward).
|
||||
fn open_panel(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = "window.scriptPanel";
|
||||
let name = str_p(p, "name").ok_or_else(|| bad(c, "missing `name` (a script in the ScriptUI Panels folder)"))?.to_string();
|
||||
|
||||
@@ -14,6 +14,8 @@ use serde_json::{Value, json};
|
||||
/// never reuses a live IME context). Exhaustion wraps; it cannot panic.
|
||||
static NEXT_EDIT_SESSION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
/// Takes the next session token: returns the counter's current value and increments it, so
|
||||
/// successive tokens differ until the process-wide `u64` counter wraps.
|
||||
pub(super) fn next_edit_session() -> u64 {
|
||||
NEXT_EDIT_SESSION.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
@@ -27,10 +29,14 @@ pub(super) fn next_edit_session() -> u64 {
|
||||
/// receipt rather than claiming a mutation with a reused revision.
|
||||
static NEXT_IME_REVISION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// Allocates a fresh process-wide IME revision, or `None` once the counter has nowhere left
|
||||
/// to advance; callers keep their existing stamp rather than reuse one.
|
||||
pub(crate) fn next_ime_revision() -> Option<u64> {
|
||||
allocate_ime_revision(&NEXT_IME_REVISION)
|
||||
}
|
||||
|
||||
/// Takes the next value of `counter` with a compare-exchange loop: `Some(current + 1)` after
|
||||
/// storing it, or `None` without storing when the counter is already `u64::MAX`.
|
||||
fn allocate_ime_revision(counter: &std::sync::atomic::AtomicU64) -> Option<u64> {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
let mut current = counter.load(Relaxed);
|
||||
@@ -85,6 +91,8 @@ pub struct TextEdit {
|
||||
}
|
||||
|
||||
impl Default for TextEdit {
|
||||
/// An empty, uncreated editing session on layer `0` with no selection, pending style or goal
|
||||
/// column, and a fresh process-wide session token.
|
||||
fn default() -> Self {
|
||||
Self { layer: LayerId(0), anchor: 0, caret: 0, pending: None, created: false, goal_x: None, session: next_edit_session() }
|
||||
}
|
||||
@@ -909,6 +917,8 @@ fn editing(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.state.text_edit.is_some() { Ok(()) } else { Err("not editing text".into()) }
|
||||
}
|
||||
|
||||
/// The command specs of this module: the `text.*` editing, selection and caret commands and
|
||||
/// the two `edit.pasteText*` commands, each with its parameter shape and enablement predicate.
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! document's folder, and only local files, or files on the network share the document itself is
|
||||
//! on, are followed (see `craft_docpath`).
|
||||
//!
|
||||
//! Loading a document resolves its footage paths once ([`resolve_footage`]): followed paths are
|
||||
//! Loading a document resolves its footage paths once (`resolve_footage`): followed paths are
|
||||
//! replaced by the resolved path, and an item with a refused path is marked missing, so the media
|
||||
//! layer never reads it. The session then remembers which paths came from which document
|
||||
//! ([`StoredPaths`]); the footage check, the changed-on-disk scan, Reload Footage, Relink, Edit
|
||||
|
||||
@@ -314,6 +314,9 @@ impl Session {
|
||||
}
|
||||
// Every state off the new line goes into branches: a chain per first child, the other
|
||||
// children starting branches of their own.
|
||||
/// Adds to `out` one [`Branch`] for the off-line subtree at `start`: it follows the first
|
||||
/// child that is not on the new history line, and records every other such child as a further
|
||||
/// branch from the state it forked at (`parent`).
|
||||
fn chain(a: &Arena, start: usize, parent: usize, in_new: &[bool], out: &mut Vec<Branch>) {
|
||||
let mut steps = vec![];
|
||||
let mut n = start;
|
||||
|
||||
@@ -564,6 +564,10 @@ pub type ScriptRunner = fn(&mut Session, &ScriptRequest) -> Value;
|
||||
pub type PluginLoader = fn(&[u8], &str) -> std::result::Result<Value, String>;
|
||||
|
||||
impl Default for Session {
|
||||
/// A new unsaved session: a default project that is both current and saved, no path, no
|
||||
/// active jobs, the plain `FsServices`/`NoFootage` defaults, default preferences and keymaps,
|
||||
/// empty history, events and journal, and a fresh process-wide IME stamp (`u64::MAX` when the
|
||||
/// allocator is exhausted).
|
||||
fn default() -> Self {
|
||||
let project = Arc::new(Project::default());
|
||||
Session {
|
||||
@@ -692,6 +696,8 @@ impl Session {
|
||||
self.script_calls.0 > 0
|
||||
}
|
||||
|
||||
/// Whether `id` names a known command whose `enabled` predicate returns `Ok` for this session;
|
||||
/// an unknown id or a predicate error reads as disabled.
|
||||
pub fn is_enabled(&self, id: &str) -> bool {
|
||||
commands::find(id).is_some_and(|c| (c.enabled)(self).is_ok())
|
||||
}
|
||||
@@ -715,6 +721,15 @@ impl Session {
|
||||
self.edit_inner(label, merge, f)
|
||||
}
|
||||
|
||||
/// The shared body of [`Session::edit`] and [`Session::edit_text_doc`].
|
||||
///
|
||||
/// Runs `f` on a clone of the project and editor state, then synchronizes derived data (global
|
||||
/// light and Essential Graphics), queues invalidated Warp Stabilizer/camera-tracker work and
|
||||
/// Roto Brush instances needing re-propagation in the `*_pending` lists. If the clone has the same
|
||||
/// content the project and history are left as they were (only editor-state changes from `f`
|
||||
/// are kept) and no revision is bumped; otherwise the previous project is recorded as an
|
||||
/// undo step under `label`, unless it merges with the current merge key (then the redo branch
|
||||
/// is dropped). An `Err` from `f` propagates with the session unchanged.
|
||||
fn edit_inner<T>(&mut self, label: &str, merge: Option<&str>, f: impl FnOnce(&mut Project, &mut EditorState) -> Result<T>) -> Result<T> {
|
||||
let before = self.project.clone();
|
||||
let mut p = (*self.project).clone();
|
||||
@@ -845,6 +860,10 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Undoes one step: the current project goes onto the redo stack, per-comp Resolution is kept
|
||||
/// as it was, the merge key is cleared, selections are sanitized for the replaced state (the
|
||||
/// on-canvas editor gets a new IME epoch only when its own document changed) and the session is
|
||||
/// bumped. Returns `false`, changing nothing, when there is nothing to undo.
|
||||
pub fn undo(&mut self) -> bool {
|
||||
let Some((label, p)) = self.history.undo.pop() else { return false };
|
||||
let edited = commands::text_edit::active_editor_snapshot(self);
|
||||
@@ -857,6 +876,10 @@ impl Session {
|
||||
true
|
||||
}
|
||||
|
||||
/// Redoes one step: the current project goes back onto the undo stack, per-comp Resolution is
|
||||
/// kept, the merge key is cleared, selections are sanitized for the replaced state (a changed
|
||||
/// text document starts a new IME epoch) and the session is bumped. Returns `false`, changing
|
||||
/// nothing, when there is nothing to redo.
|
||||
pub fn redo(&mut self) -> bool {
|
||||
let Some((label, p)) = self.history.redo.pop() else { return false };
|
||||
let edited = commands::text_edit::active_editor_snapshot(self);
|
||||
@@ -996,6 +1019,10 @@ impl Session {
|
||||
commands::text_edit::after_external_change(self, edited.as_ref());
|
||||
}
|
||||
|
||||
/// Sets the time of every comp related to `comp` through precomp layers: down into the comps
|
||||
/// it nests (through each layer's timing) and up into the comps nesting it. A seen-set keeps
|
||||
/// cycles and diamonds finite; each found time is clamped to its comp's last frame, snapped to
|
||||
/// the nearest frame and stored in `state.times`. The caller owns the text-editor boundary.
|
||||
fn sync_related_times_inner(&mut self, comp: ItemId, t: Tick) {
|
||||
let mut seen = std::collections::BTreeSet::from([comp]);
|
||||
let mut todo = vec![(comp, t)];
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! added, changed or removed; arrays element-wise when their length is unchanged, else
|
||||
//! replaced). Lossless (unlike JSON Merge Patch, `null` values survive).
|
||||
//! - [`Mirror`] (page side): what the worker holds; turns the next project revision into a
|
||||
//! [`FrameMsg::Sync`] (a full project the first time, a diff afterwards).
|
||||
//! [`FrameMsg::Project`] the first time and [`FrameMsg::Patch`] for subsequent diffs.
|
||||
//! - [`FrameServer`] (worker side): applies syncs, relayed Roto Brush segmentations and render
|
||||
//! requests, and renders frames as premultiplied RGBA8 ([`rgba8_premultiplied`]).
|
||||
//!
|
||||
@@ -301,7 +301,7 @@ impl FrameServer {
|
||||
}
|
||||
|
||||
/// Identity of virtual footage files, used when native metadata cannot describe the path.
|
||||
/// Called from [`FrameServer::load`], not from the UI thread's cache lookup.
|
||||
/// Called from `FrameServer::load`, not from the UI thread's cache lookup.
|
||||
pub fn set_virtual_id(&mut self, id: Option<Arc<dyn Fn(&str) -> Option<(u64, u64)> + Send + Sync>>) {
|
||||
self.virtual_id = id;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! `wait`; in the browser the background is a Web Worker whose segmentations stream back,
|
||||
//! [`crate::offload`]), frame by frame outward from the base frame, rendering the effect's input
|
||||
//! (`Renderer::layer_input`, source → masks → the effects above it). `roto.freeze` runs the same
|
||||
//! pass and stores the final mattes in the effect (one undo step). [`sync`], run after every
|
||||
//! pass and stores the final mattes in the effect (one undo step). `sync`, run after every
|
||||
//! edit, keeps each instance's hidden Input Key equal to a signature of what its frames are made
|
||||
//! of (the layer's source and timing, its masks and the effects above it) and drops frozen
|
||||
//! mattes made from other frames.
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::{Importer, Services};
|
||||
/// How an import treats numbered stills.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SequenceOptions {
|
||||
/// The "<format> Sequence" checkbox: picked numbered stills of one run import as a sequence.
|
||||
/// The `"<format> Sequence"` checkbox: picked numbered stills of one run import as a sequence.
|
||||
pub sequence: bool,
|
||||
/// The numbered stills inside a picked or dropped folder import as sequences.
|
||||
pub folders: bool,
|
||||
@@ -44,7 +44,7 @@ impl SequenceOptions {
|
||||
|
||||
/// `file.import`'s options. Dropped files (`drag: true`) import as stills, as in After
|
||||
/// Effects, while a folder's numbered stills import as sequences unless `sequence: false`
|
||||
/// (Alt-drag of a folder). The Import dialog passes its "<format> Sequence" checkbox (off
|
||||
/// (Alt-drag of a folder). The Import dialog passes its `"<format> Sequence"` checkbox (off
|
||||
/// unless ticked) as `sequence`. Without `sequence` or `drag` (scripts, agents) a numbered
|
||||
/// still brings its run, as it always has.
|
||||
pub fn for_import(p: &serde_json::Value) -> SequenceOptions {
|
||||
|
||||
@@ -160,7 +160,7 @@ fn clipped_items(ids: &mut Ids, s: &effectcraft_svg::Shape, m: Affine, clips: &[
|
||||
/// Blend Mode). Top of the paint order first.
|
||||
///
|
||||
/// Clipping groups (PDF / Illustrator clipping masks): shapes inside them are intersected with
|
||||
/// the clip paths through Merge Paths ▸ Intersect (see [`clipped_items`]). Clips enclosing the
|
||||
/// the clip paths through Merge Paths ▸ Intersect (see `clipped_items`). Clips enclosing the
|
||||
/// whole document are layer masks instead ([`vector_clip_masks`], used by
|
||||
/// [`shapes_from_vector`]). Images are skipped (Layer ▸ Create makes them footage layers, see
|
||||
/// [`split_at_images`]); soft masks are not converted.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! stabilization plan once to validate the result. When the job finishes, the analysis is
|
||||
//! written into the effect's hidden Analysis parameter as one undo step, together with a key of
|
||||
//! what it was made from (the layer's source, In/Out, start, stretch and Time Remap). Edits that
|
||||
//! change those clear the analysis ([`invalidate`], run after every edit) and queue the effect
|
||||
//! change those clear the analysis (`invalidate`, run after every edit) and queue the effect
|
||||
//! for re-analysis, which frontends start with [`Session::poll_warp`] (After Effects re-analyses
|
||||
//! automatically too).
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ pub fn color_popup(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, c: &mut [f32; 3])
|
||||
out.changed
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the pass number in which a colour picker last stayed open, which
|
||||
/// [`open_recently`] uses to keep Escape for the picker rather than the dialog under it.
|
||||
fn open_pass_id() -> egui::Id {
|
||||
egui::Id::unique("color-picker-open-pass")
|
||||
}
|
||||
@@ -175,6 +177,12 @@ impl Pick {
|
||||
const SQUARE: f32 = 200.0;
|
||||
const SLIDER_W: f32 = 16.0;
|
||||
|
||||
/// The colour picker popup's contents: saturation/value square, channel slider, number fields for
|
||||
/// H/S/B and R/G/B, a hex field and an original-colour swatch, plus OK and Cancel. `*c` is updated
|
||||
/// as the UI changes and `original` is what Cancel (or the swatch) restores. The chosen channel
|
||||
/// and the HSV/RGB pair are remembered in egui temp data so reopening keeps the hue when
|
||||
/// saturation or value reaches zero. Returns what the picker did this frame; it never runs a
|
||||
/// command itself.
|
||||
fn color_picker(ui: &mut Ui, id: egui::Id, c: &mut [f32; 3], original: [f32; 3]) -> Outcome {
|
||||
let state_id = id.with("pick");
|
||||
let mut pick = match ui.data(|d| d.get_temp::<([f32; 3], [f32; 3])>(state_id)) {
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct ControlRequest {
|
||||
}
|
||||
|
||||
impl ControlRequest {
|
||||
/// Creates a request for `method` with `params`, returning it with the channel that will
|
||||
/// receive the UI thread's reply.
|
||||
pub fn new(method: impl Into<String>, params: Value) -> (Self, std::sync::mpsc::Receiver<Value>) {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
(Self { method: method.into(), params, reply: tx }, rx)
|
||||
@@ -170,6 +172,10 @@ fn point(app: &EffectcraftApp, p: &Value) -> Result<egui::Pos2, String> {
|
||||
Ok(egui::pos2(x as f32, y as f32))
|
||||
}
|
||||
|
||||
/// Dispatches one control request on the UI thread. Replies are `{"ok": true, "result": …}`
|
||||
/// or `{"ok": false, "error": …}`; an unknown method is an error. Some methods answer with
|
||||
/// [`Outcome::Retry`], [`Outcome::AfterInput`], [`Outcome::Screenshot`] or
|
||||
/// [`Outcome::AwaitJobs`] instead of replying in this call.
|
||||
pub fn handle(app: &mut EffectcraftApp, ctx: &egui::Context, req: &ControlRequest, now: f64) -> Outcome {
|
||||
let p = &req.params;
|
||||
match req.method.as_str() {
|
||||
@@ -198,6 +204,10 @@ pub fn handle(app: &mut EffectcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `engine.execute` or `ui.menu.invoke`. `engine.execute` with a known engine command id
|
||||
/// goes through [`execute_engine`] (so a `wait: true` job can be offloaded); every other id,
|
||||
/// and every `ui.menu.invoke`, goes through the menu dispatcher. A failure is copied to the
|
||||
/// status line.
|
||||
fn execute_command(app: &mut EffectcraftApp, ctx: &egui::Context, method: &str, p: &Value) -> Outcome {
|
||||
let s = |k: &str| p.get(k).and_then(Value::as_str);
|
||||
let Some(id) = s("command").or(s("id")) else { return err("missing `command`") };
|
||||
@@ -219,6 +229,7 @@ fn execute_command(app: &mut EffectcraftApp, ctx: &egui::Context, method: &str,
|
||||
}
|
||||
}
|
||||
|
||||
/// Replies with `command.list`'s result, forwarding `filter` and `enabledOnly`.
|
||||
fn engine_commands(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
match app.session.execute("command.list", json!({"filter": p.get("filter"), "enabledOnly": p.get("enabledOnly")})) {
|
||||
Ok(v) => ok(v),
|
||||
@@ -226,6 +237,8 @@ fn engine_commands(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replies with the automation elements whose id starts with `prefix` (all when empty), as
|
||||
/// `{id, label, rect}` records.
|
||||
fn elements(app: &EffectcraftApp, p: &Value) -> Outcome {
|
||||
let prefix = p.get("prefix").and_then(Value::as_str).unwrap_or("");
|
||||
ok(serde_json::to_value(app.auto.query(prefix)).unwrap_or_default())
|
||||
@@ -237,6 +250,9 @@ fn focus(ctx: &egui::Context) -> Outcome {
|
||||
ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Applies `ui.set` in order: tool, workspace, theme, focused panel, viewer settings, then the
|
||||
/// layout fields ([`set_ui_layout`]). An unknown tool, workspace or theme name, or an invalid
|
||||
/// viewer value, replies with an error after the earlier fields were already applied.
|
||||
fn set_ui(app: &mut EffectcraftApp, ctx: &egui::Context, p: &Value) -> Outcome {
|
||||
let s = |k: &str| p.get(k).and_then(Value::as_str);
|
||||
if let Some(t) = s("tool") {
|
||||
@@ -297,6 +313,9 @@ fn set_ui_layout(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Applies viewer `zoom`, the active comp's `res` (stored in the project), `pan` and the
|
||||
/// `grid`, `rulers`, `safeMargins` and `transparencyGrid` toggles. An unknown resolution name
|
||||
/// is an error; a resolution change without an active comp is ignored.
|
||||
fn apply_viewer(app: &mut EffectcraftApp, v: &Value) -> Result<(), String> {
|
||||
if let Some(z) = v.get("zoom") {
|
||||
app.ui.viewer.zoom = z.as_f64().map(|z| z as f32);
|
||||
@@ -328,6 +347,8 @@ fn apply_viewer(app: &mut EffectcraftApp, v: &Value) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies timeline `pps`, `start`, `graphEditor`, `showModes`, `openLayers` and `openGroups`
|
||||
/// to the UI state, leaving absent fields unchanged.
|
||||
fn apply_timeline(app: &mut EffectcraftApp, tl: &Value) {
|
||||
if let Some(v) = tl.get("pps") {
|
||||
app.ui.timeline.pps = v.as_f64();
|
||||
@@ -349,6 +370,8 @@ fn apply_timeline(app: &mut EffectcraftApp, tl: &Value) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the Project panel's optional columns with the names in `a`, in the canonical
|
||||
/// `COLUMNS` order; unknown names are kept, sorted after the known ones.
|
||||
fn apply_project_columns(app: &mut EffectcraftApp, a: &[Value]) {
|
||||
let mut cols = vec![];
|
||||
for k in a.iter().filter_map(Value::as_str) {
|
||||
@@ -357,6 +380,8 @@ fn apply_project_columns(app: &mut EffectcraftApp, a: &[Value]) {
|
||||
app.ui.project_columns = cols;
|
||||
}
|
||||
|
||||
/// Merges `m` over the current Wiggler/Smoother/Motion Sketch settings by round-tripping them
|
||||
/// through JSON. A value of the wrong type is an error and leaves the settings unchanged.
|
||||
fn apply_anim_tools(app: &mut EffectcraftApp, m: &serde_json::Map<String, Value>) -> Result<(), String> {
|
||||
let mut cur = serde_json::to_value(&app.ui.anim_tools).unwrap_or_default();
|
||||
for (k, v) in m {
|
||||
@@ -371,6 +396,8 @@ fn apply_anim_tools(app: &mut EffectcraftApp, m: &serde_json::Map<String, Value>
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows (`ui.panel.show`) or closes (`ui.panel.close`) the named panel; an unknown panel name
|
||||
/// is an error.
|
||||
fn panel(app: &mut EffectcraftApp, method: &str, p: &Value) -> Outcome {
|
||||
let s = |k: &str| p.get(k).and_then(Value::as_str);
|
||||
let Some(panel) = s("panel").and_then(PanelKind::from_name) else { return err("unknown `panel`") };
|
||||
@@ -382,6 +409,8 @@ fn panel(app: &mut EffectcraftApp, method: &str, p: &Value) -> Outcome {
|
||||
ok(Value::Null)
|
||||
}
|
||||
|
||||
/// The number of synthetic events a gesture queues: `2 * count + 3` for `ui.click` (`count`
|
||||
/// 1..=512), or 3 for `ui.move`. An out-of-range `count` or an overflow is an error.
|
||||
fn click_gesture_events(method: &str, p: &Value) -> Result<usize, String> {
|
||||
if method != "ui.click" {
|
||||
return Ok(3);
|
||||
@@ -394,6 +423,10 @@ fn click_gesture_events(method: &str, p: &Value) -> Result<usize, String> {
|
||||
usize::try_from(count).ok().and_then(|n| n.checked_mul(2)).and_then(|n| n.checked_add(3)).ok_or_else(|| "click event count overflow".to_string())
|
||||
}
|
||||
|
||||
/// Queues a click or a plain move at `{id}`'s centre (`fx`/`fy` within its rect, default 0.5)
|
||||
/// or at `{x, y}`, holding the request's modifiers across the whole gesture. An element that is
|
||||
/// not on screen returns [`Outcome::Retry`]; the gesture is size-checked against the 4096-event
|
||||
/// queue limit before anything is pushed. Replies [`Outcome::AfterInput`].
|
||||
fn click_or_move(app: &mut EffectcraftApp, method: &str, p: &Value) -> Outcome {
|
||||
let s = |k: &str| p.get(k).and_then(Value::as_str);
|
||||
let pos = match point(app, p) {
|
||||
@@ -433,6 +466,10 @@ fn click_or_move(app: &mut EffectcraftApp, method: &str, p: &Value) -> Outcome {
|
||||
Outcome::AfterInput
|
||||
}
|
||||
|
||||
/// Queues a primary-button drag from `from` to `to` over `steps` interpolated moves (default
|
||||
/// 12, at least 2, at most 2048), with the request's modifiers. Both endpoints are resolved
|
||||
/// first (an unseen element returns [`Outcome::Retry`]) and the event count is checked against
|
||||
/// the 4096-event queue limit. Replies [`Outcome::AfterInput`].
|
||||
fn drag(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
let (Some(from), Some(to)) = (p.get("from"), p.get("to")) else { return err("need `from` and `to`") };
|
||||
let (a, b) = match (point(app, from), point(app, to)) {
|
||||
@@ -463,6 +500,8 @@ fn drag(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
Outcome::AfterInput
|
||||
}
|
||||
|
||||
/// Queues a move to the target point, a wheel event of `dx`, `dy` points with the request's
|
||||
/// modifiers, and a released Extra1 button. Replies [`Outcome::AfterInput`].
|
||||
fn scroll(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
let pos = match point(app, p) {
|
||||
Ok(p) => p,
|
||||
@@ -487,6 +526,9 @@ fn scroll(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
Outcome::AfterInput
|
||||
}
|
||||
|
||||
/// Queues a press and release of `key`, whose name may carry a shortcut (`Cmd+Shift+Z`); the
|
||||
/// separate `modifiers` flags are added. A `Physical+` prefix makes the key positional. An
|
||||
/// unknown key name is an error.
|
||||
fn key(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
let s = |k: &str| p.get(k).and_then(Value::as_str);
|
||||
let Some(name) = s("key") else { return err("missing `key`") };
|
||||
@@ -499,6 +541,8 @@ fn key(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
Outcome::AfterInput
|
||||
}
|
||||
|
||||
/// Queues a text event with `text`, followed by a released F35 key. Missing `text` is an
|
||||
/// error.
|
||||
fn type_text(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
let Some(text) = p.get("text").and_then(Value::as_str) else { return err("missing `text`") };
|
||||
app.synthetic.push(egui::Event::Text(text.to_string()));
|
||||
@@ -506,6 +550,8 @@ fn type_text(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
Outcome::AfterInput
|
||||
}
|
||||
|
||||
/// Converts comp pixel `x`, `y` to a screen point with the viewer's current transform, or
|
||||
/// returns [`Outcome::Retry`] while the viewer is not visible.
|
||||
fn viewer_locate(ctx: &egui::Context, p: &Value) -> Outcome {
|
||||
let (Some(x), Some(y)) = (p.get("x").and_then(Value::as_f64), p.get("y").and_then(Value::as_f64)) else {
|
||||
return err("need `x`, `y` (comp pixels)");
|
||||
@@ -516,6 +562,8 @@ fn viewer_locate(ctx: &egui::Context, p: &Value) -> Outcome {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a point (`{id}`, or `{x, y}`) to comp pixels; replies with an error when the
|
||||
/// viewer is not visible.
|
||||
fn viewer_hit(app: &mut EffectcraftApp, ctx: &egui::Context, p: &Value) -> Outcome {
|
||||
let pos = match point(app, p) {
|
||||
Ok(p) => p,
|
||||
@@ -527,6 +575,9 @@ fn viewer_hit(app: &mut EffectcraftApp, ctx: &egui::Context, p: &Value) -> Outco
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the screen point at `time` seconds on the ruler row of layer `layer`'s bar;
|
||||
/// without a numeric `time`, returns the bar's centre. Returns [`Outcome::Retry`] when the bar
|
||||
/// is unavailable, or a numeric `time` is supplied but the ruler is unavailable or malformed.
|
||||
fn timeline_locate(app: &EffectcraftApp, p: &Value) -> Outcome {
|
||||
let Some(l) = p.get("layer").and_then(Value::as_u64) else { return err("need `layer` (id)") };
|
||||
match crate::panels::timeline::locate(app, l, p.get("time").and_then(Value::as_f64)) {
|
||||
@@ -535,6 +586,9 @@ fn timeline_locate(app: &EffectcraftApp, p: &Value) -> Outcome {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies `action` (`play`, `stop`, `status`, else toggle) at `now` and replies with the
|
||||
/// playing state, current time in seconds, whether audio is present and the meter's level and
|
||||
/// peak in dB.
|
||||
fn playback(app: &mut EffectcraftApp, p: &Value, now: f64) -> Outcome {
|
||||
let action = p.get("action").and_then(Value::as_str).unwrap_or("toggle");
|
||||
match action {
|
||||
@@ -548,6 +602,11 @@ fn playback(app: &mut EffectcraftApp, p: &Value, now: f64) -> Outcome {
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolves the capture target — a named panel, an element id, or the whole window — and
|
||||
/// returns [`Outcome::Retry`] while a requested panel or element is not registered, so a
|
||||
/// missing target is never silently captured as the window. Requests a repaint without raising
|
||||
/// or focusing the window and returns [`Outcome::Screenshot`]; the target is resolved again
|
||||
/// when the capture is issued.
|
||||
fn screenshot(app: &mut EffectcraftApp, ctx: &egui::Context, p: &Value) -> Outcome {
|
||||
let s = |k: &str| p.get(k).and_then(Value::as_str);
|
||||
// The panel/element is an identity, not a rectangle: it is resolved when the capture
|
||||
@@ -574,11 +633,12 @@ fn screenshot(app: &mut EffectcraftApp, ctx: &egui::Context, p: &Value) -> Outco
|
||||
crate::ShotTarget::Window | crate::ShotTarget::Rect(_) => "the element is not on screen".into(),
|
||||
});
|
||||
}
|
||||
// Bring the window on screen so a frame is presented, without taking keyboard focus.
|
||||
// Request a repaint without raising the window or taking keyboard focus.
|
||||
app.raise_for_control(ctx);
|
||||
Outcome::Screenshot { path: s("path").map(str::to_string), target }
|
||||
}
|
||||
|
||||
/// Asks the viewport for an inner size of `width`×`height` pixels (default 1600×1000).
|
||||
fn resize(ctx: &egui::Context, p: &Value) -> Outcome {
|
||||
let w = p.get("width").and_then(Value::as_f64).unwrap_or(1600.0) as f32;
|
||||
let h = p.get("height").and_then(Value::as_f64).unwrap_or(1000.0) as f32;
|
||||
@@ -586,6 +646,8 @@ fn resize(ctx: &egui::Context, p: &Value) -> Outcome {
|
||||
ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Sends the viewport close request. `force: true` marks the unsaved-changes state as
|
||||
/// quitting, so a modified project closes without the save prompt.
|
||||
fn quit(app: &mut EffectcraftApp, ctx: &egui::Context, p: &Value) -> Outcome {
|
||||
// A modified project asks to save first unless `force: true`.
|
||||
if p.get("force").and_then(Value::as_bool).unwrap_or(false) {
|
||||
@@ -653,6 +715,10 @@ pub fn base64(data: &[u8]) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
/// Builds the `ui.inspect` reply: window size and pixels per point, fps, UI state with the
|
||||
/// active comp's viewer resolution, playback and time, active comp, selected layer/key counts,
|
||||
/// registered element count, keyboard focus (egui id, rect, automation ids and rect
|
||||
/// candidates), fresh window focus, workspace storage error, open dialog and last frame time.
|
||||
pub fn inspect(app: &EffectcraftApp, ctx: &egui::Context) -> Value {
|
||||
// A logic-only tick refreshes the viewport information but not `InputState`'s screen rect.
|
||||
let size = ctx.input(|i| i.viewport().inner_rect).map_or_else(|| ctx.content_rect().size(), |r| r.size());
|
||||
|
||||
@@ -1053,6 +1053,8 @@ fn gutter(ui: &mut egui::Ui, gap: Rect, vertical: bool, path: &str, t: &Tokens,
|
||||
(response.delta != 0.0).then_some(response.delta)
|
||||
}
|
||||
|
||||
/// Build the split-divider handle for `gap`: `vertical` selects the axis, `path` gives its stable id, and the theme
|
||||
/// supplies hover/drag/focus colours and the localized size label. Returns the handle for the caller to show.
|
||||
fn resize_handle(gap: Rect, vertical: bool, path: &str, t: &Tokens) -> ResizeHandle<'static> {
|
||||
let axis = if vertical { SplitAxis::Vertical } else { SplitAxis::Horizontal };
|
||||
let style = ResizeHandleStyle { hovered: t.focus.gamma_multiply(0.4), dragged: t.focus.gamma_multiply(0.9), focused: t.focus };
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::dock::{self, DockAction, DockNode, Group, Layout, PanelKind, PanelWin
|
||||
use crate::{EffectcraftApp, panels};
|
||||
use craft_ui::docking::{Action, DropTarget, resolve_drop};
|
||||
|
||||
/// The `egui::Id` under which the panel whose tab is being dragged is stored.
|
||||
fn drag_id() -> egui::Id {
|
||||
egui::Id::unique("dock-tab-drag")
|
||||
}
|
||||
@@ -161,6 +162,8 @@ impl EffectcraftApp {
|
||||
pos.and_then(|p| self.dock_rects.iter().rev().find(|(_, r)| r.contains(p)).map(|(k, _)| *k)).unwrap_or(self.ui.focused)
|
||||
}
|
||||
|
||||
/// Draw panel `p`'s body in `rect`: register its automation region, put the Composition Navigator above an active
|
||||
/// viewer when a nested-comp flow exists, then call the panel's `show` and (for a viewer) the sketch overlay.
|
||||
fn panel_body(&mut self, ui: &mut egui::Ui, p: PanelKind, rect: Rect) {
|
||||
self.auto.add(&format!("panel.{}", p.id()), rect, p.title());
|
||||
let mut content = rect.shrink(1.0);
|
||||
@@ -228,6 +231,9 @@ impl EffectcraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lay out and draw the workspace in `body`: the maximized panel or the dock tree, group chrome, active bodies
|
||||
/// and focus outline; then floating groups, their native panel windows, layout/action commands, tab drag-and-drop
|
||||
/// and the panel menu. Draws the Home screen instead when it is showing.
|
||||
pub(crate) fn dock_area(&mut self, ui: &mut egui::Ui, body: Rect) {
|
||||
let t = self.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
@@ -15,6 +15,10 @@ use effectcraft_engine::commands::shape_tool::{PaintKind, ToolPaint};
|
||||
use effectcraft_engine::project::{LayerId, LayerSource, PropGroup};
|
||||
use effectcraft_engine::viewer as vw;
|
||||
|
||||
/// Draws the Tools bar into `rect`: brand (About), Home, the tool slots with their fly-out
|
||||
/// menus and press-and-hold picks, options for the active tool (shape paint, puppet, roto),
|
||||
/// Snapping, then Discord, GitHub, website, appearance and workspace controls. Clicks apply
|
||||
/// tool, dialog, theme and workspace changes to `app` or run their commands.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().clone();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
//! the id is stable, so a translation never becomes an orphan.
|
||||
//!
|
||||
//! Each row repeats the registry's English name so that a rename upstream fails
|
||||
//! [`tests::every_row_names_a_registry_effect`] instead of silently pointing a translation at
|
||||
//! `tests::every_row_names_a_registry_effect` instead of silently pointing a translation at
|
||||
//! another effect. An effect with no row draws the registry's English name; the
|
||||
//! `missing_translations_report` test (run by `cargo xtask ci`) lists those per language.
|
||||
//!
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! inventing an id, and a missing row falls back to English instead of to an empty label.
|
||||
//!
|
||||
//! The language in force is set once per frame, from the same resolved language the fonts are
|
||||
//! installed for ([`super::language`], [`set_current`]), so a panel does not have to thread it
|
||||
//! installed for (`crate::i18n::language`, [`set_current`]), so a panel does not have to thread it
|
||||
//! through every function. It is thread-local: the render threads never draw interface text.
|
||||
//!
|
||||
//! A row that carries values keeps one `{}` per value and is read with [`tr_args`]; the tests below
|
||||
|
||||
@@ -111,6 +111,7 @@ impl AppClock {
|
||||
}
|
||||
|
||||
impl Default for AppClock {
|
||||
/// The real monotonic clock (`Self::real()`).
|
||||
fn default() -> Self {
|
||||
Self::real()
|
||||
}
|
||||
@@ -555,6 +556,10 @@ struct PlaybackPass<'a> {
|
||||
}
|
||||
|
||||
impl EffectcraftApp {
|
||||
/// Build the app around `session`.
|
||||
/// Loads and applies the workspaces saved in the session's config directory (a failed load is reported in
|
||||
/// `ui.status`), initializes the default UI, playback and render state, and registers the frontend's commands as
|
||||
/// shortcutable `UiCommand`s on the session.
|
||||
pub fn new(session: Session) -> Self {
|
||||
let mut ui = UiState::default();
|
||||
let workspace_store = workspace_state::WorkspaceStore::load(session.config.as_deref(), &mut ui);
|
||||
@@ -848,7 +853,7 @@ impl EffectcraftApp {
|
||||
}
|
||||
|
||||
/// 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>.
|
||||
/// give it the focus: Window ▸ `<panel>`.
|
||||
pub fn show_panel(&mut self, p: PanelKind) {
|
||||
if !self.ui.floating.iter().any(|f| f.panels.contains(&p)) && self.ui.maximized.is_some_and(|m| m != p) {
|
||||
self.ui.maximized = None;
|
||||
@@ -1570,6 +1575,8 @@ impl EffectcraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the audio or scrub output and feed its latest peaks to the level meter; an idle scrub is closed after
|
||||
/// one second. With no source but an active meter, drive the meter to silence and request a repaint.
|
||||
fn pump_meters(&mut self, ctx: &egui::Context, now: f64) {
|
||||
if let Some(a) = &mut self.audio {
|
||||
a.pump();
|
||||
@@ -1794,6 +1801,10 @@ impl EffectcraftApp {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// Handle the control channel for this tick: drain queued requests plus previously deferred ones, run each
|
||||
/// through `control::handle`, answer `Done` immediately, retry `Retry` until its three-second deadline, hold
|
||||
/// input/job/screenshot replies until their event happens and finish job waiters. Retry timeouts and refused
|
||||
/// screenshots are answered as `{"ok": false, "error": ...}`; the receiver is kept for the next tick.
|
||||
fn drain_control(&mut self, ctx: &egui::Context, now: f64) {
|
||||
let Some(rx) = self.control_rx.take() else { return };
|
||||
let mut reqs: Vec<(ControlRequest, f64)> = std::mem::take(&mut self.deferred);
|
||||
@@ -2149,6 +2160,8 @@ impl EffectcraftApp {
|
||||
|
||||
// ---------------------------------------------------------------- frame
|
||||
|
||||
/// Apply the engine events drained this tick: open a comp, show a toast, open a URL, run a frontend command,
|
||||
/// purge frame caches or copy an image; command and clipboard failures go to `app.ui.status`.
|
||||
fn handle_events(&mut self, ctx: &egui::Context) {
|
||||
for ev in self.session.drain_events() {
|
||||
match ev {
|
||||
@@ -2359,6 +2372,10 @@ impl EffectcraftApp {
|
||||
panels::viewer_text::finish_logic(self, ctx);
|
||||
}
|
||||
|
||||
/// One UI pass after the IME begin hook: close on an applied GPU failure, run `ui_setup`, skip drawing until
|
||||
/// fonts are ready, draw the frame, finish pointer wrapping, pump queued wasm frames, record input activity,
|
||||
/// publish the completed pass's geometry and pass sequence, acknowledge applied synthetic input and answer
|
||||
/// `AfterInput` replies only after a non-discarded pass. Requests repaints while frames are in flight.
|
||||
fn draw_inner(&mut self, ui: &mut egui::Ui, frame: &eframe::Frame) {
|
||||
let ctx = ui.ctx().clone();
|
||||
if self.gpu_failure_applied == Some(true) {
|
||||
@@ -2619,6 +2636,9 @@ impl eframe::App for EffectcraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed pending synthetic input into `raw_input` only once fonts are ready (so a non-drawing pass never consumes
|
||||
/// a batch), then prepare pointer wrapping; ending a numeric scrub clears the history merge key so the next edit
|
||||
/// starts a new undo step.
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
|
||||
let injected = self.synthetic_in_flight || !self.synthetic.is_empty();
|
||||
// Only a pass that can draw may receive synthetic input: `Context::run_ui` applies
|
||||
@@ -2633,6 +2653,8 @@ impl eframe::App for EffectcraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// The logic-only tick: run `tick` and close the IME pass with `finish_ime_logic`. It does not read pass-time
|
||||
/// input state (events, pointer and time are stale here), only the viewport information eframe refreshed.
|
||||
fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
// Never `ctx.input(|i| i.time)` here: `Context::run_logic` leaves events, pointer state and
|
||||
// time at the previous full pass's values (only the viewport information is refreshed).
|
||||
@@ -2640,6 +2662,7 @@ impl eframe::App for EffectcraftApp {
|
||||
self.finish_ime_logic(ctx);
|
||||
}
|
||||
|
||||
/// Draw one UI pass through `draw`.
|
||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
self.draw(ui, frame);
|
||||
}
|
||||
|
||||
@@ -60,15 +60,24 @@ struct KeyOpened {
|
||||
pointer: Option<Pos2>,
|
||||
}
|
||||
|
||||
/// The temp-data id of the [`FirstEntry`] depth to highlight after a keyboard-opened menu is
|
||||
/// first drawn.
|
||||
fn pending_id() -> Id {
|
||||
Id::unique("menu-keys-first")
|
||||
}
|
||||
|
||||
/// The temp-data id of the [`KeyOpened`] record: submenus opened from the keyboard and the
|
||||
/// pointer position they were opened at.
|
||||
fn opened_id() -> Id {
|
||||
Id::unique("menu-keys-opened")
|
||||
}
|
||||
|
||||
/// The temp-data id of the flag saying the menu bar currently holds the keyboard.
|
||||
fn active_id() -> Id {
|
||||
Id::unique("menu-keys-active")
|
||||
}
|
||||
|
||||
/// The temp-data id of the `(armed, held)` Alt-key state used to detect an Alt tap.
|
||||
fn alt_id() -> Id {
|
||||
Id::unique("menu-keys-alt")
|
||||
}
|
||||
|
||||
@@ -1580,7 +1580,7 @@ pub(crate) fn panel_shown(app: &EffectcraftApp, p: PanelKind) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The command a menu entry runs when chosen from a menu: Window ▸ <panel> closes a panel whose
|
||||
/// The command a menu entry runs when chosen from a menu: Window ▸ `<panel>` closes a panel whose
|
||||
/// entry is checked, as in After Effects (#1630). Shortcuts and agents calling `window.panel`
|
||||
/// still only show the panel.
|
||||
pub(crate) fn menu_choice(app: &EffectcraftApp, id: String, params: Value) -> (String, Value) {
|
||||
@@ -1823,6 +1823,8 @@ fn system_paste_id() -> egui::Id {
|
||||
egui::Id::unique("menus-system-paste")
|
||||
}
|
||||
|
||||
/// The temp-data id of the flag set by a plain Space press and taken on its release, so a tap
|
||||
/// runs the Space shortcut while holding Space stays the Hand tool.
|
||||
fn space_tap_id() -> egui::Id {
|
||||
egui::Id::unique("spacebar-tap")
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ fn choice(app: &mut EffectcraftApp, ui: &mut egui::Ui, r: Rect, auto: &str, opti
|
||||
widgets::popup_menu(ui, id, r.left_bottom(), &opts, Some(cur)).filter(|i| *i != cur)
|
||||
}
|
||||
|
||||
/// A drag-to-change number field for an anim-tool setting: `v` clamped to `range`, `speed` points
|
||||
/// per pixel, `dec` decimals and a `suffix`; returns the new value, or `v` when it did not change.
|
||||
fn number(app: &mut EffectcraftApp, ui: &mut egui::Ui, at: egui::Pos2, auto: &str, v: f64, speed: f64, range: (f64, f64), dec: usize, suffix: &str) -> f64 {
|
||||
let t = app.tokens;
|
||||
let (r, nv, _) = widgets::hot_number_at(ui, at, egui::Id::unique(("animtool-n", auto)), v, speed, range, dec, suffix, &t);
|
||||
@@ -38,6 +40,8 @@ fn number(app: &mut EffectcraftApp, ui: &mut egui::Ui, at: egui::Pos2, auto: &st
|
||||
nv.unwrap_or(v)
|
||||
}
|
||||
|
||||
/// A checkbox at `at` with its `text` label drawn to its right; returns the state to store, i.e.
|
||||
/// `!on` when it was clicked and `on` otherwise.
|
||||
fn checkbox(app: &mut EffectcraftApp, ui: &mut egui::Ui, at: egui::Pos2, auto: &str, text: &str, on: bool) -> bool {
|
||||
let t = app.tokens;
|
||||
let r = Rect::from_min_size(at, vec2(16.0, 16.0));
|
||||
@@ -218,6 +222,8 @@ struct Capture {
|
||||
points: Vec<[f64; 3]>,
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the Motion Sketch capture in progress ([`Capture`]): its start time and the
|
||||
/// recorded points.
|
||||
fn capture_id() -> egui::Id {
|
||||
egui::Id::unique("motion-sketch-capture")
|
||||
}
|
||||
|
||||
@@ -181,6 +181,8 @@ fn timecode_field(ui: &mut egui::Ui, buf: &mut Option<String>, secs: &mut f64, r
|
||||
r
|
||||
}
|
||||
|
||||
/// The temp-data id of the one-shot flag that makes the Save Preset name field take focus when
|
||||
/// it is next drawn.
|
||||
fn name_focus_id() -> egui::Id {
|
||||
egui::Id::unique("comp-preset-name-focus")
|
||||
}
|
||||
|
||||
@@ -273,6 +273,10 @@ const ABOUT_TABS: [(&str, &str); 3] = [("About", "about"), ("Contributors", "con
|
||||
/// Height of the Contributors / Models tab bodies (they scroll inside it).
|
||||
const CREDITS_HEIGHT: f32 = 380.0;
|
||||
|
||||
/// The About dialog: tabs for the name/version/blurb, the contributors and the models (the last
|
||||
/// two scroll in a fixed-height body), the licence and bundled-font line, and Close. The tab is
|
||||
/// kept in egui temp data. A command the About body asked for is run once the modal has been
|
||||
/// built; Close clears `app.dialog`.
|
||||
fn about(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let mut close = false;
|
||||
let mut cmd: Option<&str> = None;
|
||||
|
||||
@@ -670,6 +670,11 @@ fn toggle_closed(app: &mut EffectcraftApp, g: &PropGroup, open: bool, all: bool)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lays out one property group's rows below `*y` (which it advances), recursing into open
|
||||
/// subgroups: one row per visible property with its controls, plus the dial or slider row when its
|
||||
/// slider is open, and an inline editor for groups that have one. Rows scrolled out of `rect` are
|
||||
/// skipped without being drawn or registered, though their height is still counted. Pick-whip
|
||||
/// drops are registered for each visible property; edits go on `actions`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn group_rows(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -1121,6 +1126,12 @@ pub fn start_rename(ctx: &egui::Context, uid: u64, name: &str) {
|
||||
ctx.memory_mut(|m| m.request_focus(id.with("field")));
|
||||
}
|
||||
|
||||
/// The Effect Controls panel for the selected layer: a header with the comp and layer names, then
|
||||
/// one header per effect (Fx toggle, twirl, rename, Reset, About, label bar, reorder drag and its
|
||||
/// right-click menu) followed by that effect's custom editor and its property rows. Dragging
|
||||
/// effects or presets onto the panel applies them; a right-click on empty space opens the Effect
|
||||
/// menu. The rows read from a snapshot of the project, and every edit is queued and run through
|
||||
/// [`crate::menus::invoke`], with failures reported in `app.ui.status`.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
|
||||
@@ -264,6 +264,13 @@ fn user_presets(ctx: &egui::Context, force: bool, scan: impl FnOnce() -> Vec<Pre
|
||||
v
|
||||
}
|
||||
|
||||
/// The Effects & Presets panel: search, a contents menu (colour-depth and GPU filters, whether
|
||||
/// presets show, categories or one alphabetical list, refresh and clear of the recently used
|
||||
/// list), then Favourites, Recently Used, Animation Presets and the effects by category or
|
||||
/// alphabetically, filtered by the query. Picking an effect or preset applies it to the selection
|
||||
/// (`effect.apply` or the preset's own command); applying an effect also records it as recently
|
||||
/// used and opens Effect Controls. Without a selected layer it only reports that in
|
||||
/// `app.ui.status`.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
@@ -56,6 +56,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Essential Graphics panel's contents for the Primary (else active) comp: its name and Solo
|
||||
/// toggle, the template name, the controls list with its drop target for Timeline properties, the
|
||||
/// Add Formatting / Add Selected As / Media Replacement / Export Template buttons and the selected
|
||||
/// instance's properties. Returns the `essential.*` commands to run directly and the ones that go
|
||||
/// through the menu/frontend layer; with no comp it shows a hint and returns empty lists.
|
||||
fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
@@ -23,6 +23,8 @@ struct Cache {
|
||||
current: usize,
|
||||
}
|
||||
|
||||
/// The `egui::Id` under which the error bar's cache for composition `cid` is kept: this pass's
|
||||
/// failing expressions and which one is shown.
|
||||
fn cache_id(cid: ItemId) -> egui::Id {
|
||||
egui::Id::unique(("expr-error-bar", cid.0))
|
||||
}
|
||||
@@ -46,6 +48,11 @@ pub fn height(app: &mut EffectcraftApp, ctx: &egui::Context, cid: ItemId) -> f32
|
||||
if errors(app, ctx, cid).errors.is_empty() { 0.0 } else { HEIGHT }
|
||||
}
|
||||
|
||||
/// Draws the expression error strip for composition `cid` in `rect`: a warning, the shown error's
|
||||
/// description and ◀ n/m ▶ buttons to step through the failing expressions. Clicking the message
|
||||
/// selects that layer and property, reveals it in the Timeline and opens its expression. Does
|
||||
/// nothing when every expression evaluates. Stepping and clicking only change UI state (the
|
||||
/// cached index, selection and Timeline state), never the document.
|
||||
pub fn draw(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, cid: ItemId) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
@@ -13,6 +13,11 @@ use super::panel_kit as kit;
|
||||
use crate::EffectcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// The Footage panel for the item opened into it: the frame at the panel's current time (cached
|
||||
/// per item, time and session revision), a ruler with the in/out span and CTI that scrubs on click
|
||||
/// or drag, Play/Pause, set In/Out, and Ripple Insert / Overlay Edit into the active comp. A solid
|
||||
/// shows its colour, footage without video says so, and an empty panel explains how to open one.
|
||||
/// Playback and time changes run `footage.*` commands, so the panel and agents share the state.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
@@ -152,7 +152,7 @@ fn is_pdf(p: &Value) -> bool {
|
||||
}
|
||||
|
||||
/// File ▸ Import ▸ File… picked numbered stills that import as image sequences: ask, as After
|
||||
/// Effects' "<format> Sequence" checkbox in its Import dialog does (rfd's native dialog can't
|
||||
/// Effects' `<format> Sequence` checkbox in its Import dialog does (rfd's native dialog can't
|
||||
/// carry a checkbox), with Force Alphabetical Order and the frame rate. `p`: the import's
|
||||
/// parameters. Returns whether the dialog opened. The checkbox starts off, as in After Effects:
|
||||
/// unticked, the picked files import as stills. Dropped files and agents' `file.import` don't
|
||||
|
||||
@@ -111,6 +111,8 @@ fn tabs(
|
||||
out
|
||||
}
|
||||
|
||||
/// A small "Reset" link centred in `r`, registered under the automation id `id`; returns whether
|
||||
/// it was clicked.
|
||||
fn reset_link(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, r: Rect, id: &str) -> bool {
|
||||
let t = app.tokens;
|
||||
let resp = ui.interact(r, egui::Id::unique(id), Sense::click());
|
||||
@@ -162,6 +164,10 @@ pub fn inline_editor(
|
||||
|
||||
const RGB_CURVES: [(&str, &str); 4] = [("master", "Master"), ("red", "Red"), ("green", "Green"), ("blue", "Blue")];
|
||||
|
||||
/// The Lumetri RGB Curves editor for effect group `g`: channel tabs (Master, Red, Green, Blue),
|
||||
/// the selected channel's curve graph with its Reset, and the point positions registered for
|
||||
/// automation. A curve edit or Reset is queued on `actions` as a `prop.set` of that channel's
|
||||
/// curve string; the visible channel is remembered in `app.ui.fx_curve_channel` per group.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn rgb_curves(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -269,6 +275,11 @@ fn hue_color(h: f32) -> Color32 {
|
||||
Color32::from(c)
|
||||
}
|
||||
|
||||
/// The Lumetri Hue/Saturation Curves editor for effect group `g`: tabs for the Hue vs Sat,
|
||||
/// Hue vs Hue, Hue vs Luma, Luma vs Sat and Sat vs Sat curves, drawn as a neutral-relative offset
|
||||
/// curve over the axis' colour band, with point drag/add/remove gestures and a Reset. Changes are
|
||||
/// queued on `actions` as a `prop.set` of the selected curve's point list; the visible tab is kept
|
||||
/// in `app.ui.fx_curve_channel` per group.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn huesat_curves(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -441,6 +452,12 @@ fn wheel_point(c: Pos2, radius: f32, phase: f32) -> Pos2 {
|
||||
pos2(c.x + radius * a.sin(), c.y - radius * a.cos())
|
||||
}
|
||||
|
||||
/// Colorama's Output Cycle wheel for effect group `g`: the palette drawn as a ring with its stops.
|
||||
/// Clicking the ring adds a stop with the colour there, dragging moves one, and Alt-clicking or
|
||||
/// dragging one off removes it while another stop remains; the selected stop's colour can be
|
||||
/// edited below. A change queues
|
||||
/// the rewritten palette on `actions`, and for a preset palette also switches the effect to the
|
||||
/// custom palette in the same undo step.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn colorama_wheel(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -657,6 +674,10 @@ pub fn join_map(m: &[fw::CurvePoints; 3]) -> String {
|
||||
m.iter().map(|c| c.format()).collect::<Vec<_>>().join(" | ")
|
||||
}
|
||||
|
||||
/// Glow's Colour Map for effect group `g`: a strip showing the gradient the glow uses (A/B colours,
|
||||
/// or the arbitrary map sampled through the loop settings). In arbitrary-map mode it also draws the
|
||||
/// RGB curve tabs and the selected channel's graph with a Reset; edits queue a `prop.set` of
|
||||
/// `arbitraryMap` on `actions`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn glow_map(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, layer: &Layer, g: &PropGroup, ectx: &EvalCtx, r: Rect, actions: &mut Actions) {
|
||||
let t = app.tokens;
|
||||
@@ -769,6 +790,10 @@ pub fn add_pair(v: &[(f64, f64)]) -> (f64, f64) {
|
||||
((a.0 + ds * 0.5).rem_euclid(1.0), (a.1 + gap * 0.5).rem_euclid(1.0))
|
||||
}
|
||||
|
||||
/// Reshape's correspondence buttons: Add Point appends a pair at the widest destination-fraction
|
||||
/// gap (or `(0, 0)` for an empty list), Remove Point drops the last stored pair, and Clear removes
|
||||
/// all pairs. A status line says whether points are automatic. Each change
|
||||
/// queues a `prop.set` of the `correspondencePoints` string on `actions`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn reshape_buttons(
|
||||
app: &mut EffectcraftApp,
|
||||
|
||||
@@ -246,6 +246,12 @@ fn to_color32(c: [f32; 4]) -> Color32 {
|
||||
Color32::from_rgba_unmultiplied(b(c[0]), b(c[1]), b(c[2]), b(c[3]))
|
||||
}
|
||||
|
||||
/// The Gradient Editor dialog for the gradient in `app.dialog_state.gradient`: a stop track for
|
||||
/// the opacity and colour tracks over a checkerboard preview, the selected stop's location,
|
||||
/// opacity or colour and a Delete button (when its track has more than one stop), the
|
||||
/// mismatched-stop-count warning, and OK/Cancel. Dragging moves stops, Delete/Backspace removes the
|
||||
/// selected one unless a widget wants keyboard input. OK runs the commit path; Cancel closes the dialog and drops the
|
||||
/// draft. With no gradient target it closes the dialog.
|
||||
pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
if app.dialog_state.gradient.target.is_none() {
|
||||
app.dialog = None;
|
||||
|
||||
@@ -189,6 +189,9 @@ const MORE_POPUP: &str = "graph-more";
|
||||
impl ButtonBar {
|
||||
const MORE_W: f32 = 24.0;
|
||||
|
||||
/// A bar starting 8 points into `bar`, stopping short of its right end by the "»" button's
|
||||
/// width. It takes the id picked from the "»" menu in the previous frame, so the button whose
|
||||
/// id that was reports a click on the frame after the pick.
|
||||
fn new(ctx: &egui::Context, bar: Rect) -> Self {
|
||||
let chosen = ctx.data_mut(|d| d.remove_temp::<String>(egui::Id::unique(MORE_POPUP).with("chosen")));
|
||||
ButtonBar { x: bar.min.x + 8.0, limit: bar.max.x - 8.0 - Self::MORE_W - 4.0, hidden: Vec::new(), chosen }
|
||||
@@ -233,6 +236,10 @@ impl ButtonBar {
|
||||
}
|
||||
}
|
||||
|
||||
/// One text button of the Graph Editor's button bar at the bar's current `x`, in the toolbar row
|
||||
/// at `y`: draws it, registers it for automation and advances the bar. Once a button does not fit
|
||||
/// (or a previous one overflowed), the button goes to the "»" menu instead and only the id the
|
||||
/// menu picked reports a click. Returns whether this button was chosen.
|
||||
fn text_button(
|
||||
app: &mut EffectcraftApp,
|
||||
ui: &mut egui::Ui,
|
||||
@@ -627,6 +634,8 @@ fn fit_all(app: &mut EffectcraftApp) {
|
||||
app.ui.timeline.pps = None;
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the one-shot flag [`track_open`] sets when the Graph Editor is switched on,
|
||||
/// so [`show`] frames all graphs on its next pass, once the plot rectangle is known.
|
||||
fn fit_pending_id() -> egui::Id {
|
||||
egui::Id::unique("graph-fit-pending")
|
||||
}
|
||||
@@ -739,6 +748,8 @@ struct KeyGrab {
|
||||
t: f64,
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the key drag in progress ([`KeyGrab`]): where the button went down and the
|
||||
/// grabbed key's comp time when it did.
|
||||
fn key_grab_id() -> egui::Id {
|
||||
egui::Id::unique("graph-key-grab")
|
||||
}
|
||||
@@ -786,6 +797,8 @@ impl HandleGrab {
|
||||
/// A handle drag: where the button went down, and the handles that move.
|
||||
type HandleDrag = (Pos2, Vec<HandleGrab>);
|
||||
|
||||
/// The `egui::Id` of the Bezier handle drag in progress ([`HandleDrag`]): the press position and
|
||||
/// the handles the drag moves.
|
||||
fn handle_drag_id() -> egui::Id {
|
||||
egui::Id::unique("graph-handle-drag")
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ struct BoxDrag {
|
||||
last: [f64; 2],
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the transform-box drag in progress ([`BoxDrag`]).
|
||||
fn box_id() -> egui::Id {
|
||||
egui::Id::unique("graph-transform-box")
|
||||
}
|
||||
@@ -254,6 +255,7 @@ struct AltScale {
|
||||
span: Option<f64>,
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the Timeline Alt-drag group scale in progress ([`AltScale`]).
|
||||
fn alt_id() -> egui::Id {
|
||||
egui::Id::unique("timeline-alt-scale")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ use egui::{Align2, Color32, Rect, pos2, vec2};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{EffectcraftApp, widgets};
|
||||
|
||||
/// The Info panel: the colour and alpha under the pointer (sampled from the shown viewer frame,
|
||||
/// read back for GPU frames) with its position and a swatch, then the selected layers, the current
|
||||
/// time, the duration/In/Out of a single selected layer, the achieved preview frame rate, the
|
||||
/// render time and queue state (when the preference asks for it) and the selected keyframe's
|
||||
/// property, time and value. Read-only: it draws and scrolls but changes no document state.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
|
||||
@@ -40,10 +40,14 @@ struct Tex {
|
||||
rect: [f64; 4],
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the cached render of the layer shown in this panel ([`Tex`]), with the key
|
||||
/// the image was made from.
|
||||
fn tex_id() -> egui::Id {
|
||||
egui::Id::unique("layer-panel-tex")
|
||||
}
|
||||
|
||||
/// The `egui::Id` under which the panel's last Fit magnification is kept, so View ▸ Zoom In/Out
|
||||
/// can start from it (see [`last_fit`]).
|
||||
fn fit_id() -> egui::Id {
|
||||
egui::Id::unique("layer-panel-fit")
|
||||
}
|
||||
@@ -83,6 +87,13 @@ fn default_view(layer: &Layer, tool: Tool) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Layer panel: one layer in its own space with its masks and effects up to the View menu's
|
||||
/// choice, rendered synchronously at a zoom/pan the panel keeps (the last Fit magnification is
|
||||
/// published for the View menu). Wheel or pinch zooms about the pointer, the Hand tool or
|
||||
/// Space-drag pans, and the Zoom tool clicks zoom. The paint tools record a stroke's layer-space
|
||||
/// points while dragging and run `paint.stroke` on release (or on a click); Alt-click with the
|
||||
/// Clone Stamp sets its source. With a Roto Brush effect it hands the canvas to
|
||||
/// [`super::roto_tool`]. Without a current layer it shows how to open one.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn open(app: &mut EffectcraftApp, params: &Value) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Menu routing: choosing one of the Layer ▸ Layer Styles ▸ <style> items (no parameters) adds
|
||||
/// Menu routing: choosing one of the Layer ▸ Layer Styles ▸ `<style>` items (no parameters) adds
|
||||
/// the style as usual and opens the dialog on it.
|
||||
pub fn route(app: &mut EffectcraftApp, id: &str, params: &Value) -> Result<bool, String> {
|
||||
let Some(style) = id.strip_prefix("layer.style.").filter(|s| STYLES.iter().any(|x| x.0 == *s)) else { return Ok(false) };
|
||||
|
||||
@@ -243,6 +243,8 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
|
||||
// ---------------------------------------------------------------- timeline drawing + gestures
|
||||
|
||||
/// The `egui::Id` of a marker drag in progress: the dragged marker's layer and index (updated as
|
||||
/// the marker moves) and whether Alt was held when the drag started.
|
||||
fn drag_state_id() -> egui::Id {
|
||||
egui::Id::unique("marker-drag")
|
||||
}
|
||||
|
||||
@@ -63,6 +63,12 @@ fn thumbnail(app: &EffectcraftApp, ctx: &egui::Context, path: &str, budget: &mut
|
||||
thumb
|
||||
}
|
||||
|
||||
/// The Media Browser panel: a toolbar (up, home, favourite, "Media only" filter and the browser's
|
||||
/// own actions), the current path, the favourite places and a tile grid of the folder's entries
|
||||
/// with thumbnails for images and videos. Double-click opens a folder or imports a file; files
|
||||
/// drag out as [`DragPayload::Files`] and a context menu copies paths, imports (optionally into the
|
||||
/// comp) or adds a favourite. The listing is cached per folder and filter for a second, and every
|
||||
/// change runs a `mediaBrowser.*` command.
|
||||
pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
@@ -362,6 +362,10 @@ pub fn audio(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The History panel: every state of the session's branching history, indented under the state it
|
||||
/// grew from, with the current state highlighted and future/abandoned branches dimmed. Only the
|
||||
/// rows in view are drawn; the current state is scrolled into view when it changes. Clicking a
|
||||
/// state runs `edit.history.goto`, reporting a failure in `app.ui.status`.
|
||||
pub fn history(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
@@ -419,6 +423,9 @@ pub fn history(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Markers panel: the active comp's markers in time order, each with its timecode and comment;
|
||||
/// clicking a row moves the current time to that marker. Shows a hint when the comp has none and
|
||||
/// draws nothing without an active comp.
|
||||
pub fn markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
|
||||
@@ -41,6 +41,9 @@ fn number(
|
||||
nv.filter(|_| enabled)
|
||||
}
|
||||
|
||||
/// A label at `x`, `y` and, to its right, a dropdown over `options` with `cur` selected. The
|
||||
/// dropdown only opens while `enabled`; otherwise only its label is dimmed. Returns the index
|
||||
/// picked from the open menu, if any.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn popup_row(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -66,6 +69,7 @@ fn popup_row(
|
||||
widgets::popup_menu(ui, pop, r.left_bottom(), options, Some(cur))
|
||||
}
|
||||
|
||||
/// The RGB components of an RGBA paint colour as `f32`, for the swatches and the colour picker.
|
||||
fn rgb(c: [f64; 4]) -> [f32; 3] {
|
||||
[c[0] as f32, c[1] as f32, c[2] as f32]
|
||||
}
|
||||
@@ -79,6 +83,10 @@ fn paint_swap_icon(p: &egui::Painter, rect: Rect, color: Color32) {
|
||||
p.arrow(pos2(right, bottom), vec2(left - right, 0.0), stroke);
|
||||
}
|
||||
|
||||
/// The Paint panel: opacity and flow, foreground/background colours with swap and reset, mode,
|
||||
/// channels, duration, erase mode and the Clone Stamp options. Every change is sent as a
|
||||
/// `paint.options` command. Mode and Channels are disabled for the Eraser; Erase is enabled
|
||||
/// only for it. The Clone Options heading is dimmed for other tools, but its controls stay enabled.
|
||||
pub fn paint(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
@@ -286,6 +294,9 @@ fn tip_preview(p: &egui::Painter, r: Rect, diameter: f64, angle: f64, roundness:
|
||||
}
|
||||
}
|
||||
|
||||
/// The Brushes panel: a grid of built-in brush presets (each runs `paint.brushPreset`), a tip
|
||||
/// preview, diameter/angle/roundness/hardness/spacing fields and the size/opacity/flow pressure
|
||||
/// options. Values are sent as `paint.options` commands and failures land in `app.ui.status`.
|
||||
pub fn brushes(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
|
||||
@@ -191,6 +191,8 @@ fn fit(r: Rect, w: f32, h: f32) -> Rect {
|
||||
|
||||
/// An inline text edit in progress: (item, field `name`|`comment`, text).
|
||||
type Editing = (u64, String, String);
|
||||
/// The `egui::Id` of the Project panel's inline edit in progress ([`Editing`]): the item, which
|
||||
/// field (`name` or `comment`) and the text being typed.
|
||||
fn edit_id() -> egui::Id {
|
||||
egui::Id::unique("proj-inline-edit")
|
||||
}
|
||||
@@ -379,6 +381,11 @@ pub(crate) fn reveal(app: &mut EffectcraftApp, ctx: &egui::Context, items: &[Ite
|
||||
app.show_panel(crate::dock::PanelKind::Project);
|
||||
}
|
||||
|
||||
/// The Project panel: the selected item's thumbnail and details (with the comps that use it), the
|
||||
/// search field, sortable column headers, and the item rows (folders, comps, footage and solids)
|
||||
/// with selection, Shift/Cmd ranges, keyboard navigation, inline rename, drag-and-drop move,
|
||||
/// marquee selection and context menus. Only the rows in view are drawn. File commands are queued
|
||||
/// and run through [`crate::menus::invoke`] at the end, with failures shown in `app.ui.status`.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().clone();
|
||||
|
||||
@@ -51,6 +51,12 @@ fn decimals(v: f64) -> usize {
|
||||
if (v - v.round()).abs() < 1e-6 { 0 } else { 1 }
|
||||
}
|
||||
|
||||
/// The Properties panel for the first selected layer: "N Selected Objects" when several are
|
||||
/// selected, the Layer Transform section (with Reset) and, for a text layer, the text, paragraph
|
||||
/// and Text Animation sections that edit the range being edited in the viewer. Rows are evaluated
|
||||
/// against a snapshot of the project; every row edit is queued and run through
|
||||
/// [`crate::menus::invoke`] at the end (range-scoped for `layer.setText`), with failures reported
|
||||
/// in `app.ui.status`. Without a selected layer it shows a hint.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
|
||||
@@ -415,6 +415,11 @@ fn pick_output(app: &EffectcraftApp, it: &RenderQueueItem) -> Option<Option<Stri
|
||||
app.hooks.save_dialog(&app.session.resolve_output(it).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// The Render Queue panel: the Current Render strip (elapsed and remaining time, Render/Stop and
|
||||
/// Notify, plus the progress bar), the column header, the queued items with their output template,
|
||||
/// status and timings, and the footer totals. Comps dragged from the Project are queued on
|
||||
/// release. The open items and the selected item are kept in egui temp data; output-template forms
|
||||
/// open through a small form, and every change runs a `renderQueue.*` command.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
|
||||
@@ -21,6 +21,8 @@ struct State {
|
||||
new_name: String,
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the render-settings / output-module template dialog's state ([`State`]):
|
||||
/// which kind it edits, the selected template and the name being typed.
|
||||
fn state_id() -> egui::Id {
|
||||
egui::Id::unique("rq-templates-dialog")
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ struct Cached {
|
||||
tex: Option<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
/// The Scopes panel: the scope kind, colour standard, 8-bit/Float scale and Clamp Signal controls,
|
||||
/// then the chosen scope for the active comp's frame at the current time, cached per revision,
|
||||
/// comp, time, settings and plot size (drawn as a texture, or bars for the histogram) over its
|
||||
/// graticule. The controls update `app.ui.scopes`; the scope itself is only computed and drawn.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
|
||||
@@ -74,6 +74,9 @@ pub fn run(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
con.input.clear();
|
||||
}
|
||||
|
||||
/// The Script Console panel: the output log (newest lines at the bottom, coloured by kind), the
|
||||
/// multi-line script input and Run/Clear. Run — the button or ⌘/Ctrl+Enter while typing — hands the
|
||||
/// input to the script runner; Clear only empties the log. The input text and log are UI state.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let input_h = 86.0;
|
||||
|
||||
@@ -81,6 +81,11 @@ fn draw_children(app: &mut EffectcraftApp, ui: &mut egui::Ui, origin: egui::Pos2
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws ScriptUI widget `w` of window `win` at its bounds offset by `origin`, registering it for
|
||||
/// automation, and recurses into containers. Widgets with an `onDraw` handler are painted by the
|
||||
/// script (replacing the default look, or on top of it when it called `drawOSControl()`) while
|
||||
/// buttons, checkboxes and radios still take clicks. Edits and clicks are appended to `acts` for
|
||||
/// [`perform`]; while a field or slider is in use its live value lives in egui temp data.
|
||||
fn draw_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, origin: egui::Pos2, win: u32, w: &Widget, acts: &mut Vec<Act>) {
|
||||
let t = app.tokens;
|
||||
let services = app.session.services.clone();
|
||||
@@ -471,6 +476,9 @@ fn snug(r: Rect, text: &str) -> Rect {
|
||||
Rect::from_min_size(r.min, vec2(w, r.height()))
|
||||
}
|
||||
|
||||
/// Runs the actions collected while drawing a ScriptUI window as `scriptui.*` commands, in order.
|
||||
/// A reply carrying an `error` becomes a toast with the script's message; otherwise a non-empty
|
||||
/// `output` becomes the status line (its last line). Command failures also go to `app.ui.status`.
|
||||
fn perform(app: &mut EffectcraftApp, ctx: &egui::Context, acts: Vec<Act>) {
|
||||
for a in acts {
|
||||
let (cmd, p) = match a {
|
||||
@@ -546,7 +554,7 @@ pub fn show_windows(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
perform(app, ctx, acts);
|
||||
}
|
||||
|
||||
/// A dockable ScriptUI panel's content (Window ▸ <panel>.jsx).
|
||||
/// A dockable ScriptUI panel's content (Window ▸ `<panel>.jsx`).
|
||||
pub fn panel(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, id: u32) {
|
||||
let t = app.tokens;
|
||||
let Some(mut w) = app.session.script_ui.window(id).cloned() else {
|
||||
|
||||
@@ -200,6 +200,12 @@ const SIDE_KEYS: &[(&str, &str, f32, f32)] = &[
|
||||
("→", "ArrowRight", 2.0, 5.0),
|
||||
];
|
||||
|
||||
/// The Keyboard Shortcuts dialog: while a shortcut field listens it records the next key
|
||||
/// combination (dropping those key events so the field does not type them), the preset bar switches
|
||||
/// keymaps, the drawn keyboard lists what the selected key runs, and the command list and editor
|
||||
/// remove, assign and reset bindings, warning about conflicts. Enter is OK unless a rename field, a
|
||||
/// preset menu or the delete confirmation is open. The collected `shortcuts.*` commands are run
|
||||
/// with the session when the frame ends; OK closes the dialog.
|
||||
pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let mut st = std::mem::take(&mut app.dialog_state.shortcuts);
|
||||
let mut run: Vec<(String, Value)> = vec![];
|
||||
|
||||
@@ -61,6 +61,9 @@ pub fn text_target(app: &EffectcraftApp) -> Option<TextTarget> {
|
||||
effectcraft_engine::render::text::source_text(&ectx, layer).map(|d| TextTarget { layer: layer.id.0, doc: d, range: None })
|
||||
}
|
||||
|
||||
/// A small toggle button at `r` showing `label` (semibold 13 when `bold`, else 12), filled with
|
||||
/// the accent while `on`; returns whether it was clicked. It only reports the click: the caller
|
||||
/// decides the new state.
|
||||
fn toggle(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, r: Rect, id: &str, label: &str, on: bool, bold: bool) -> bool {
|
||||
let t = app.tokens;
|
||||
let resp = ui.interact(r, egui::Id::unique(("text-toggle", id)), Sense::click());
|
||||
@@ -81,6 +84,12 @@ fn toggle(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, r: Rec
|
||||
resp.clicked()
|
||||
}
|
||||
|
||||
/// The Character panel for the text being edited (or the selected text layer): font family and
|
||||
/// style, size, leading, tracking, stroke, scale, baseline shift and other numeric fields, kerning,
|
||||
/// fill/stroke colours with eyedroppers, style toggles, OpenType features and variable-font axes.
|
||||
/// Without a text target the panel is inert — no edit is collected and it asks for a text layer.
|
||||
/// Edits are collected and, when a target exists, sent as `layer.setText` commands scoped to the
|
||||
/// edited range; failures show in `app.ui.status`.
|
||||
pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
@@ -452,6 +461,10 @@ pub fn paint_justify_glyph(p: &egui::Painter, r: Rect, j: Justify, c: egui::Colo
|
||||
}
|
||||
}
|
||||
|
||||
/// The Paragraph panel: alignment, left/right/first-line indents, space before and after,
|
||||
/// paragraph direction (when the text engine allows it), the composer, and Roman hanging
|
||||
/// punctuation. Edits are queued and sent as `layer.setText` for the edited range when a text
|
||||
/// target exists; failures show in `app.ui.status`.
|
||||
pub fn paragraph(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
|
||||
@@ -163,6 +163,8 @@ const WHIP_PARENT: u8 = 0;
|
||||
const WHIP_EXPR: u8 = 1;
|
||||
const WHIP_PROP: u8 = 2;
|
||||
|
||||
/// The `egui::Id` under which a pick-whip drag in progress ([`PickWhip`]) is kept during the
|
||||
/// drag.
|
||||
fn pick_whip_id() -> egui::Id {
|
||||
egui::Id::unique("tl-pickwhip")
|
||||
}
|
||||
@@ -194,6 +196,8 @@ fn pick_whip(
|
||||
/// (row, layer, prop uid, its value fields per dimension), and the pass they were shown in.
|
||||
type WhipTargets = (u64, Vec<(Rect, u64, u64, Vec<Rect>)>);
|
||||
|
||||
/// The `egui::Id` of the drop targets other panels register for a pick-whip drag
|
||||
/// ([`whip_target`]); [`show`] clears it when the whip is released or dropped.
|
||||
fn whip_targets_id() -> egui::Id {
|
||||
egui::Id::unique("tl-pickwhip-targets")
|
||||
}
|
||||
@@ -263,6 +267,8 @@ fn centered_on(span: f64, duration: f64, cti: f64) -> f64 {
|
||||
/// it (characters): the pick whip's reference goes in there.
|
||||
type WhipInto = (String, [usize; 2]);
|
||||
|
||||
/// The `egui::Id` under which the expression text captured by an expression pick whip
|
||||
/// ([`WhipInto`]) is held for property `prop` until that editor is drawn again.
|
||||
fn whip_into_id(prop: u64) -> egui::Id {
|
||||
egui::Id::unique(("tl-pickwhip-into", prop))
|
||||
}
|
||||
@@ -279,6 +285,8 @@ struct ExprFocus {
|
||||
pass: u64,
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the pending [`ExprFocus`]: which expression editor takes the keyboard on
|
||||
/// its next draw, and what to select in it.
|
||||
fn expr_focus_id() -> egui::Id {
|
||||
egui::Id::unique("tl-expr-focus")
|
||||
}
|
||||
@@ -560,6 +568,8 @@ fn ruler_label(fno: i64, fps_i: i64, settings: &effectcraft_engine::project::Pro
|
||||
}
|
||||
}
|
||||
|
||||
/// The `egui::Id` under which [`show`] records this frame's time mapping as `(x0, start, pps)`,
|
||||
/// i.e. the [`TMap`] fields it lays the ruler and graph out with.
|
||||
fn tl_map_id() -> egui::Id {
|
||||
egui::Id::unique("timeline-map")
|
||||
}
|
||||
@@ -713,6 +723,8 @@ fn group_rename_id() -> egui::Id {
|
||||
egui::Id::unique("tl-group-rename")
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the one-shot flag that makes the open layer rename field take the keyboard
|
||||
/// on its first frame (see [`start_rename`]).
|
||||
fn rename_focus_id() -> egui::Id {
|
||||
egui::Id::unique("tl-rename-focus")
|
||||
}
|
||||
@@ -1290,6 +1302,8 @@ struct KeyDrag {
|
||||
quick: Option<usize>,
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the keyframe drag in progress ([`KeyDrag`]): its start, the grabbed key's
|
||||
/// time and how far the selection has moved so far.
|
||||
fn key_drag_id() -> egui::Id {
|
||||
egui::Id::unique("tl-key-drag")
|
||||
}
|
||||
@@ -1304,6 +1318,7 @@ struct QuickBar {
|
||||
applied: i64,
|
||||
}
|
||||
|
||||
/// The `egui::Id` of a Quick Offset drag of layer bars in progress ([`QuickBar`]).
|
||||
fn quick_bar_id() -> egui::Id {
|
||||
egui::Id::unique("tl-quick-bar")
|
||||
}
|
||||
@@ -1345,6 +1360,8 @@ struct SwitchDrag {
|
||||
merge: String,
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the switch press-and-drag in progress ([`SwitchDrag`]): the switch, the
|
||||
/// state the press gave it and the undo step the drag folds into.
|
||||
fn switch_drag_id() -> egui::Id {
|
||||
egui::Id::unique("tl-switch-drag")
|
||||
}
|
||||
@@ -1438,6 +1455,8 @@ fn layer_switch(
|
||||
}
|
||||
}
|
||||
|
||||
/// The `egui::Id` under which composition `comp` remembers the layer the last click selected, so
|
||||
/// a following Shift-click can select the range from there ([`shift_range`]).
|
||||
fn anchor_id(comp: ItemId) -> egui::Id {
|
||||
egui::Id::unique(("timeline-selection-anchor", comp.0))
|
||||
}
|
||||
@@ -1471,6 +1490,8 @@ fn insertion(layer_rows: &[(Rect, LayerId)], py: f32, top: f32) -> (Option<usize
|
||||
(at, y)
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the flag the row drag's release sets, telling the same frame's reorder
|
||||
/// drawing code to apply the drop.
|
||||
fn layer_drop_id() -> egui::Id {
|
||||
egui::Id::unique("tl-layer-drop")
|
||||
}
|
||||
@@ -1495,6 +1516,14 @@ fn replace_source_target(app: &EffectcraftApp, cid: ItemId, item: ItemId, lid: L
|
||||
.is_some_and(|l| !l.switches.locked && matches!(l.source, LayerSource::Footage { .. } | LayerSource::Comp { .. } | LayerSource::Solid { .. }))
|
||||
}
|
||||
|
||||
/// The Timeline panel for the active composition inside `rect`: draws the header, ruler, work
|
||||
/// area and cache bars, the layer rows with their switch and value cells, the time graph and its
|
||||
/// keys, and the footer; and turns this frame's clicks, drags and drops into commands. Without an
|
||||
/// active comp it shows a hint, and dropping footage on it creates a composition. Most edits
|
||||
/// use [`crate::menus::invoke`]; time, switch and work-area gestures call `Session::execute`
|
||||
/// directly and ignore errors. Reported failures go to `app.ui.status`. UI state (scroll, zoom,
|
||||
/// open rows and the drags in progress) lives in
|
||||
/// `app.ui.timeline` and egui's temp data.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
@@ -51,6 +51,8 @@ fn current(app: &EffectcraftApp) -> Option<(LayerId, Uid)> {
|
||||
l.trackers().next().map(|(g, _)| (l.id, g.uid))
|
||||
}
|
||||
|
||||
/// The `egui::Id` of the Motion Source dropdown's transient state; picking a new source removes
|
||||
/// whatever is stored under it, so the choice starts fresh.
|
||||
fn source_id() -> egui::Id {
|
||||
egui::Id::unique("tracker-motion-source")
|
||||
}
|
||||
@@ -113,6 +115,11 @@ fn analyze_glyph(p: &egui::Painter, r: Rect, forward: bool, frame: bool, stop: b
|
||||
}
|
||||
}
|
||||
|
||||
/// The Tracker panel: the camera/warp and motion/stabilize buttons, Motion Source and Current
|
||||
/// Track pickers, the track type and its channels, Motion Target with Edit Target and Options, the
|
||||
/// four Analyze buttons (the one whose direction is running offers Stop), Reset and Apply, and the
|
||||
/// analysis progress. With a mask selected it switches to its mask-tracking mode. Every change runs a
|
||||
/// `track.*` command; while an analysis is running it polls the job and asks for repaints.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
@@ -37,6 +37,7 @@ impl ViewerMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `egui::Id` under which the last pass's `ViewerMap` is published.
|
||||
fn map_id() -> egui::Id {
|
||||
egui::Id::unique("viewer-map")
|
||||
}
|
||||
@@ -769,6 +770,13 @@ pub(crate) fn checker(p: &egui::Painter, r: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the Composition viewer in `rect` and handle its input for one pass.
|
||||
/// Publishes the fit magnification and `ViewerMap`, requests and draws the frame, then paints the nav breadcrumbs,
|
||||
/// view layout, guides/grid, layer boxes, handles, anchors, 3D gizmos, motion paths, masks, puppet pins and tracker
|
||||
/// overlays and records their automation regions. Pointer and drop gestures run engine commands
|
||||
/// (move/scale/rotate/anchor/camera/shape/mask/guide edits, picks, marquees); failures worth showing go to
|
||||
/// `app.ui.status`, while per-frame drag updates ignore errors so a gesture is not aborted mid-drag. With no active
|
||||
/// composition it draws `empty_state` and returns.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let ctx = ui.ctx().clone();
|
||||
@@ -2269,6 +2277,10 @@ fn pen_cursor(ctx: &egui::Context, at: Pos2, icon: crate::icons::Icon) {
|
||||
crate::icons::paint(&painter, rect, icon, Color32::WHITE);
|
||||
}
|
||||
|
||||
/// Handle a Pen-tool press at `pos`: close the current path when its first vertex is hit; otherwise append a snapped
|
||||
/// (Shift: 45-degree) vertex, resume an open path's end, add a motion-path keyframe, insert a vertex on a visible
|
||||
/// path segment, or start a new mask/shape path on the selected layer or a new shape layer. Records the resulting
|
||||
/// path id and tangent gesture in context data for the drag that follows.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn pen_press(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -2600,6 +2612,8 @@ fn create_shape(app: &mut EffectcraftApp, tool: Tool, a: [f64; 2], b: [f64; 2],
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the no-composition viewer with New Composition and New Composition From Footage buttons, accepting Project
|
||||
/// items dropped on it to create the first composition.
|
||||
fn empty_state(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().clone();
|
||||
|
||||
@@ -393,6 +393,7 @@ pub(crate) fn motion_path(
|
||||
|
||||
// ---------------------------------------------------------------- free transform
|
||||
|
||||
/// The `egui::Id` under which the free-transform target (layer and mask or path id) is stored.
|
||||
fn ft_id() -> egui::Id {
|
||||
egui::Id::unique("viewer-free-transform")
|
||||
}
|
||||
|
||||
@@ -40,9 +40,12 @@ enum Drag {
|
||||
Resize { layer: LayerId, handle: usize, start_box: [f64; 4] },
|
||||
}
|
||||
|
||||
/// The `egui::Id` under which the in-progress text drag (selection, creation or box resize) is kept between frames.
|
||||
fn drag_id() -> egui::Id {
|
||||
egui::Id::unique("viewer-text-drag")
|
||||
}
|
||||
/// The `egui::Id` of the invisible focusable widget that owns keyboard and IME focus while on-canvas text editing is
|
||||
/// active.
|
||||
fn focus_id() -> egui::Id {
|
||||
egui::Id::unique("viewer-text-focus")
|
||||
}
|
||||
@@ -97,6 +100,8 @@ enum ImeComposition {
|
||||
struct OutputContext(egui::Id, u64, egui::IMEPurpose);
|
||||
|
||||
impl From<&egui::output::IMEOutput> for OutputContext {
|
||||
/// Copy the owner, context generation and purpose out of egui's IME output so they can be cached and compared
|
||||
/// cheaply.
|
||||
fn from(output: &egui::output::IMEOutput) -> Self {
|
||||
Self(output.owner, output.context_generation, output.purpose)
|
||||
}
|
||||
@@ -111,33 +116,43 @@ struct ResetGuard {
|
||||
suspended: bool,
|
||||
}
|
||||
|
||||
/// The `egui::Id` under which this viewport's `ResetGuard` is stored.
|
||||
fn reset_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("effectcraft.ime.reset", viewport))
|
||||
}
|
||||
/// The `egui::Id` holding whether this viewport is currently inside a full UI pass (set by the pass hooks).
|
||||
fn mode_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("effectcraft.ime.ui-pass", viewport))
|
||||
}
|
||||
/// The `egui::Id` holding the last committed IME mutation stamp accepted for this viewport.
|
||||
fn committed_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("effectcraft.ime.committed", viewport))
|
||||
}
|
||||
/// The `egui::Id` holding the frame number whose text-editor keyboard events were already consumed.
|
||||
fn input_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("effectcraft.ime.input-frame", viewport))
|
||||
}
|
||||
/// The `egui::Id` holding whether this viewport may request an IME reset in the current pass.
|
||||
fn admission_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("effectcraft.ime.can-request", viewport))
|
||||
}
|
||||
|
||||
/// Allow or forbid IME reset requests for the current viewport's pass by storing `allowed` in context data.
|
||||
fn set_admission(ctx: &egui::Context, allowed: bool) {
|
||||
let id = admission_id(ctx.viewport_id());
|
||||
ctx.data_mut(|data| data.insert_temp(id, allowed));
|
||||
}
|
||||
|
||||
/// Whether the window can receive input: focused, not minimized and not occluded. Unknown viewport flags count as
|
||||
/// available.
|
||||
fn window_available(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|input| {
|
||||
input.focused && input.viewport().focused.unwrap_or(true) && input.viewport().minimized != Some(true) && input.viewport().occluded != Some(true)
|
||||
})
|
||||
}
|
||||
|
||||
/// Install the per-pass IME hooks on `ctx` once: the begin hook marks a full UI pass and the end hook clears it.
|
||||
/// Later calls do nothing.
|
||||
pub(crate) fn install_hooks(ctx: &egui::Context) {
|
||||
let id = egui::Id::unique("effectcraft.ime.hooks-installed");
|
||||
let first = ctx.data_mut(|data| {
|
||||
@@ -167,11 +182,15 @@ pub(crate) fn install_hooks(ctx: &egui::Context) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Read the current viewport's pending reset guard, if any.
|
||||
fn read_reset(ctx: &egui::Context) -> Option<ResetGuard> {
|
||||
let id = reset_id(ctx.viewport_id());
|
||||
ctx.data(|data| data.get_temp(id))
|
||||
}
|
||||
|
||||
/// Record a reset for the current viewport: request a fresh IME fence when this pass is admitted, then store the
|
||||
/// destination selection, committed stamp and output identity and mark the composition stale. An unadmitted pass
|
||||
/// stores a suspended guard instead.
|
||||
fn request_reset_with_output(app: &EffectcraftApp, ctx: &egui::Context, output: Option<OutputContext>) {
|
||||
let viewport = ctx.viewport_id();
|
||||
let allowed = ctx.data(|data| data.get_temp::<bool>(admission_id(viewport)).unwrap_or(false));
|
||||
@@ -183,11 +202,14 @@ fn request_reset_with_output(app: &EffectcraftApp, ctx: &egui::Context, output:
|
||||
});
|
||||
}
|
||||
|
||||
/// Re-request a reset for the current viewport, keeping the output identity published earlier in the pass.
|
||||
fn request_reset(app: &EffectcraftApp, ctx: &egui::Context) {
|
||||
let output = read_reset(ctx).and_then(|guard| guard.output);
|
||||
request_reset_with_output(app, ctx, output);
|
||||
}
|
||||
|
||||
/// Re-request the reset when an unsuspended guard's destination selection or committed stamp no longer matches the
|
||||
/// app.
|
||||
fn sync_reset(app: &EffectcraftApp, ctx: &egui::Context) {
|
||||
if let Some(guard) = read_reset(ctx)
|
||||
&& !guard.suspended
|
||||
@@ -197,6 +219,7 @@ fn sync_reset(app: &EffectcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retire the guard's request id locally and suspend it, so only a later eligible pass can request a new fence.
|
||||
fn suspend_reset(ctx: &egui::Context) {
|
||||
if let Some(mut guard) = read_reset(ctx) {
|
||||
guard.request = None;
|
||||
@@ -206,6 +229,8 @@ fn suspend_reset(ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept `request` only when it is this viewport's current, unsuspended request and the destination and stamp still
|
||||
/// match; then sync target and selection, clear the composition and preview and remember the selection.
|
||||
fn complete_reset(app: &EffectcraftApp, ui: &egui::Ui, request: egui::ImeResetId) {
|
||||
if !read_reset(ui.ctx()).is_some_and(|guard| !guard.suspended && guard.request == Some(request)) {
|
||||
return;
|
||||
@@ -228,6 +253,8 @@ fn complete_reset(app: &EffectcraftApp, ui: &egui::Ui, request: egui::ImeResetId
|
||||
}
|
||||
}
|
||||
|
||||
/// Withhold custom IME controls for this pass: suspend the reset guard and drop every `ImeResetCompleted` event.
|
||||
/// No custom mutation route can be waiting for completion here; stock text widgets keep their keys and text events.
|
||||
fn unavailable_controls(ui: &egui::Ui) {
|
||||
suspend_reset(ui.ctx());
|
||||
// No custom mutation route runs here, so controls can only be obsolete. Stock text/keys
|
||||
@@ -235,12 +262,15 @@ fn unavailable_controls(ui: &egui::Ui) {
|
||||
ui.ctx().input_mut(|input| input.events.retain(|event| !matches!(event, egui::Event::ImeResetCompleted { .. })));
|
||||
}
|
||||
|
||||
/// Start a UI pass: revoke IME reset admission and clear the geometry published by the previous pass.
|
||||
pub(crate) fn begin_ui(ctx: &egui::Context) {
|
||||
set_admission(ctx, false);
|
||||
let id = ime_geometry_id(ctx.viewport_id());
|
||||
ctx.data_mut(|data| data.remove::<ImeGeometry>(id));
|
||||
}
|
||||
|
||||
/// Close a logic-only tick: when no full UI pass ran, revoke admission and resync and suspend the reset guard so a
|
||||
/// hidden tick never publishes IME state.
|
||||
pub(crate) fn finish_logic(app: &EffectcraftApp, ctx: &egui::Context) {
|
||||
let id = mode_id(ctx.viewport_id());
|
||||
if !ctx.data(|data| data.get_temp::<bool>(id).unwrap_or(false)) {
|
||||
@@ -259,30 +289,40 @@ struct SurroundingCache {
|
||||
value: Option<ImeSurrounding>,
|
||||
}
|
||||
|
||||
/// The `egui::Id` under which this viewport's visible IME pre-edit preview is stored.
|
||||
fn ime_preedit_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("viewer-text-preedit", viewport))
|
||||
}
|
||||
/// The `egui::Id` under which this viewport's in-flight IME composition state is stored.
|
||||
fn ime_composition_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("viewer-text-ime-composition", viewport))
|
||||
}
|
||||
/// The `egui::Id` under which the current pass number, text region and caret rectangle are stored for the
|
||||
/// pass-end finalizer.
|
||||
fn ime_geometry_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("viewer-text-ime-geometry", viewport))
|
||||
}
|
||||
/// The `egui::Id` under which the last text-edit target given keyboard focus is stored.
|
||||
fn ime_seen_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("viewer-text-seen", viewport))
|
||||
}
|
||||
/// The `egui::Id` under which the last `ImeOwner` observed by `sync_ime_target` is stored.
|
||||
fn ime_target_seen_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("viewer-text-ime-target", viewport))
|
||||
}
|
||||
/// The `egui::Id` under which the last accepted IME selection is stored.
|
||||
fn ime_selection_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("viewer-text-ime-selection", viewport))
|
||||
}
|
||||
/// The `egui::Id` under which the last published IME owner and context generation are stored.
|
||||
fn ime_last_context_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("viewer-text-ime-last-context", viewport))
|
||||
}
|
||||
/// The `egui::Id` under which the bounded IME surrounding-text snapshot and its cache key are stored.
|
||||
fn ime_surrounding_cache_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("viewer-text-ime-surrounding", viewport))
|
||||
}
|
||||
/// The `egui::Id` under which the typing merge counter is stored.
|
||||
fn typing_key_id(viewport: egui::ViewportId) -> egui::Id {
|
||||
egui::Id::unique(("viewer-text-typing", viewport))
|
||||
}
|
||||
@@ -299,6 +339,8 @@ fn ime_target(app: &EffectcraftApp) -> Option<egui::ImeTarget> {
|
||||
Some(egui::ImeTarget::new(egui::Id::unique(("effectcraft-ime-text", comp.0, edit.layer.0)), edit.session))
|
||||
}
|
||||
|
||||
/// The layer and edit-session token of the active text edit, or `None` when no editor is open. Caret, selection and
|
||||
/// text are deliberately excluded.
|
||||
fn ime_owner(app: &EffectcraftApp) -> Option<ImeOwner> {
|
||||
let edit = app.session.state.text_edit.as_ref()?;
|
||||
Some(ImeOwner { layer: edit.layer, session: edit.session })
|
||||
@@ -331,6 +373,9 @@ fn ime_surrounding_cached(app: &EffectcraftApp, ctx: &egui::Context) -> Option<I
|
||||
value
|
||||
}
|
||||
|
||||
/// The committed-text stamp an IME receipt compares against: the bounded snapshot when it is available, else the
|
||||
/// editor revision marked unavailable. It deliberately does not use the global project revision, so an unrelated edit
|
||||
/// leaves a wide selection's stamp unchanged.
|
||||
fn ime_stamp(app: &EffectcraftApp, ctx: &egui::Context) -> egui::ImeMutationStamp {
|
||||
match ime_surrounding_cached(app, ctx) {
|
||||
Some(w) => egui::ImeMutationStamp::from_committed(&w.text, w.cursor, w.anchor, None),
|
||||
@@ -372,16 +417,19 @@ fn note_ime_application_change(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
app.ime_tracker.note_application_change(viewport, frame, target);
|
||||
}
|
||||
|
||||
/// Drop this viewport's visible pre-edit preview.
|
||||
fn clear_preedit(ui: &egui::Ui) {
|
||||
let viewport = ui.ctx().viewport_id();
|
||||
ui.data_mut(|d| d.remove::<Preedit>(ime_preedit_id(viewport)));
|
||||
}
|
||||
|
||||
/// Read this viewport's in-flight IME composition state.
|
||||
fn read_ime_composition(ui: &egui::Ui) -> Option<ImeComposition> {
|
||||
let viewport = ui.ctx().viewport_id();
|
||||
ui.data(|d| d.get_temp::<ImeComposition>(ime_composition_id(viewport)))
|
||||
}
|
||||
|
||||
/// Store this viewport's composition state, or remove it with `None`.
|
||||
fn set_ime_composition(ui: &egui::Ui, composition: Option<ImeComposition>) {
|
||||
let viewport = ui.ctx().viewport_id();
|
||||
ui.data_mut(|d| match composition {
|
||||
@@ -427,6 +475,7 @@ fn sync_ime_target(ui: &egui::Ui, app: &EffectcraftApp) {
|
||||
ui.data_mut(|d| d.insert_temp(ime_target_seen_id(viewport), current));
|
||||
}
|
||||
|
||||
/// The active text edit's owner, anchor and caret, or `None` when no editor is open.
|
||||
fn ime_selection(app: &EffectcraftApp) -> Option<ImeSelection> {
|
||||
let edit = app.session.state.text_edit.as_ref()?;
|
||||
Some(ImeSelection { owner: ime_owner(app)?, anchor: edit.anchor, caret: edit.caret })
|
||||
@@ -458,6 +507,7 @@ fn sync_ime_selection(ui: &egui::Ui, app: &EffectcraftApp) {
|
||||
remember_ime_selection(ui, app);
|
||||
}
|
||||
|
||||
/// Truncate a pre-edit preview to `MAX_IME_TEXT_BYTES` on a character boundary, so no character is split.
|
||||
fn bounded_preedit(mut text: String) -> String {
|
||||
if text.len() > MAX_IME_TEXT_BYTES {
|
||||
let mut end = MAX_IME_TEXT_BYTES;
|
||||
@@ -469,6 +519,7 @@ fn bounded_preedit(mut text: String) -> String {
|
||||
text
|
||||
}
|
||||
|
||||
/// Read this viewport's visible pre-edit preview, whatever owner it belongs to.
|
||||
fn read_preedit(ui: &egui::Ui) -> Option<Preedit> {
|
||||
let viewport = ui.ctx().viewport_id();
|
||||
ui.data(|d| d.get_temp::<Preedit>(ime_preedit_id(viewport)))
|
||||
@@ -495,6 +546,7 @@ fn ime_event_is_stale(ui: &egui::Ui, owner: ImeOwner) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The pre-edit text to draw, only when it belongs to the current edit owner and is non-empty.
|
||||
fn visible_preedit(ui: &egui::Ui, app: &EffectcraftApp) -> Option<String> {
|
||||
let owner = ime_owner(app)?;
|
||||
let p = read_preedit(ui)?;
|
||||
@@ -1298,6 +1350,9 @@ fn ime_delete(app: &mut EffectcraftApp, ui: &egui::Ui, before: usize, after: usi
|
||||
exec(app, "text.deleteSurrounding", json!({"before": before, "after": after, "bytes": bytes, "merge": k})).is_some()
|
||||
}
|
||||
|
||||
/// Paint the paragraph box and handles, selection, caret or pre-edit for `e`; register the editor's and box handles'
|
||||
/// automation regions; request the caret-blink repaint; and record the pass's text and caret rectangles for the IME
|
||||
/// finalizer.
|
||||
fn draw(app: &mut EffectcraftApp, ui: &mut egui::Ui, painter: &egui::Painter, map: &ViewerMap, e: &Edited, sel: std::ops::Range<usize>, caret: usize) {
|
||||
let t = app.tokens;
|
||||
let to_s = |p: Point| e.to_screen(map, (p.x, p.y));
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::{EffectcraftApp, widgets};
|
||||
pub(crate) const SNAP_PX: f64 = 8.0;
|
||||
const SNAP_COLOR: Color32 = Color32::from_rgb(0xff, 0xd2, 0x3c);
|
||||
|
||||
/// The `egui::Id` under which this frame's snap feedback is stored until `draw_snap` consumes it.
|
||||
fn snap_id() -> egui::Id {
|
||||
egui::Id::unique("viewer-snap")
|
||||
}
|
||||
@@ -318,11 +319,13 @@ pub(crate) fn proportional_grid(app: &EffectcraftApp, painter: &egui::Painter, c
|
||||
|
||||
// ---------------------------------------------------------------- the displayed frame
|
||||
|
||||
/// Remember the region of interest the displayed texture was rendered with.
|
||||
/// Store the displayed texture's Region of Interest or Extended Viewer area unchanged.
|
||||
pub(crate) fn set_texture_roi(ctx: &egui::Context, roi: Option<[f64; 4]>) {
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::unique("viewer-tex-roi"), roi));
|
||||
}
|
||||
|
||||
/// The comp region (`[x, y, w, h]` in pixels) used for the texture: Region of Interest or Extended Viewer area,
|
||||
/// or `None` when no explicit region is stored, meaning the whole comp.
|
||||
fn texture_roi(ctx: &egui::Context) -> Option<[f64; 4]> {
|
||||
ctx.data(|d| d.get_temp::<Option<[f64; 4]>>(egui::Id::unique("viewer-tex-roi"))).flatten()
|
||||
}
|
||||
@@ -505,6 +508,7 @@ fn popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: &str, anchor: Rect, it
|
||||
})
|
||||
}
|
||||
|
||||
/// Flip the open flag of the popup with `id`; the caller that owns the popup reads the same flag.
|
||||
fn toggle_popup(ui: &egui::Ui, id: &str) {
|
||||
let pid = egui::Id::unique(id).with("open");
|
||||
let open: bool = ui.data(|d| d.get_temp(pid).unwrap_or(false));
|
||||
@@ -616,6 +620,10 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect,
|
||||
});
|
||||
}
|
||||
|
||||
/// Lay out the viewer's control row from `bar.min.x`: magnification, resolution, transparency, mask, ROI, grid/guide,
|
||||
/// channel/colour management, exposure, snapshot, fast previews, 3D renderer/view and Extended Viewer, timecode and render-time
|
||||
/// indicators, running commands or toggling viewer UI state. Returns the width the row
|
||||
/// occupies, at least `bar.width()`, for the horizontal scroll area.
|
||||
fn bottom_bar_contents(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, zoom: f32, time: Tick, comp: &Comp) -> f32 {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().clone();
|
||||
|
||||
@@ -12,6 +12,7 @@ struct State {
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
/// The temp-data id of the [`State`] that tracks a numeric scrub across monitor edges.
|
||||
fn state_id() -> Id {
|
||||
Id::unique("numeric-pointer-wrap")
|
||||
}
|
||||
|
||||
@@ -311,6 +311,8 @@ impl Tokens {
|
||||
}
|
||||
}
|
||||
|
||||
/// The default label colours, one RGB colour per [`effectcraft_color::Label`] in
|
||||
/// `Label::ALL` order.
|
||||
fn default_labels() -> [Color32; 17] {
|
||||
effectcraft_color::Label::ALL.map(|l| {
|
||||
let [r, g, b] = l.rgb();
|
||||
@@ -328,7 +330,7 @@ static JETBRAINS_MONO: &[u8] = include_bytes!("../../../assets/fonts/JetBrainsMo
|
||||
static NOTO_SYMBOLS2: &[u8] = include_bytes!("../../../assets/fonts/NotoSansSymbols2-Regular.ttf");
|
||||
|
||||
/// Install fonts (Inter, Inter Medium/SemiBold, JetBrains Mono) and egui visuals. `language` is the
|
||||
/// resolved interface language from [`crate::i18n::language`] (`en`, `ja`, `zh-hans` or `zh-hant`),
|
||||
/// resolved interface language from `crate::i18n::language` (`en`, `ja`, `zh-hans` or `zh-hant`),
|
||||
/// not the stored `general.language` preference: `system` is already resolved there, so a Chinese
|
||||
/// system draws Chinese with the default setting. The CJK fallback that draws the interface's own
|
||||
/// script is registered first, so a Chinese interface is drawn by a Chinese face and a Japanese one
|
||||
|
||||
@@ -22,6 +22,8 @@ struct TabOrder {
|
||||
pending: Option<(egui::Id, bool, u64)>,
|
||||
}
|
||||
|
||||
/// The temp-data id of the [`TabOrder`] for the panel containing `ui` (unique per panel scope;
|
||||
/// `Id::NULL` outside a tagged panel).
|
||||
fn tab_order_id(ui: &Ui) -> egui::Id {
|
||||
let panel = ui.stack().iter().find(|s| s.tags().contains(PANEL_TAG)).map_or(egui::Id::NULL, |s| s.scope_id);
|
||||
egui::Id::unique(("hot-tab-order", panel))
|
||||
@@ -326,6 +328,9 @@ pub fn text_field(ui: &mut Ui, rect: Rect, text: &mut String, hint: &str, t: &To
|
||||
field(ui, rect, text, hint, t, false)
|
||||
}
|
||||
|
||||
/// The shared body of [`text_field`] and [`search_field`]: paints the rounded field (and, for
|
||||
/// `search`, a magnifier), places a single-line `TextEdit` on `text` filling the inner rect,
|
||||
/// and returns its response.
|
||||
fn field(ui: &mut Ui, rect: Rect, text: &mut String, hint: &str, t: &Tokens, search: bool) -> Response {
|
||||
let h = rect.height();
|
||||
ui.painter().rect_filled(rect, h / 2.0, t.field_bg);
|
||||
@@ -509,6 +514,8 @@ pub fn check_label(on: bool, label: &str) -> String {
|
||||
if on { format!("✓ {label}") } else { format!(" {label}") }
|
||||
}
|
||||
|
||||
/// The atom id of [`label_swatch`]'s slot inside a button, which [`paint_label_swatch`] looks
|
||||
/// up on the laid-out button.
|
||||
fn label_swatch_id() -> egui::IdSalt {
|
||||
egui::IdSalt::new("label-swatch")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user