From cb1804b4c356ccdd4d9dd4f833faa9249f78f25a Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 3 Oct 2026 01:02:24 -0400 Subject: [PATCH] M10.7: Essential Graphics Browse/Edit UI, template/font dialogs, golden scenes - Essential Graphics panel with Browse (Graphics Templates library: engine-rendered thumbnails, search, category filter, apply, drag to timeline, install, remove) and Edit tabs. - Edit tab: Template Properties (one editor per control), Responsive Design - Time (intro/outro, roll/crawl options), Responsive Design - Position (Pin To + edges); Type-tool selections style just the selected characters (graphics.setCharStyle). - Export As Motion Graphics Template and Replace Fonts in Projects dialogs; Install via file picker. - Source graphics listed in the Project panel; caption pickers accept MCC/STL/TTML/DFXP. - ui.set gfxTemplates / gfxEdit; headless UI tests; golden scenes graphic_template and graphic_rich_text (sidecars + ATTRIBUTION rows); docs. Co-Authored-By: Claude Opus 5.5 --- ATTRIBUTION.md | 2 + crates/golden/goldens/graphic_rich_text.png | Bin 0 -> 25584 bytes .../goldens/graphic_rich_text.png.attribution | 8 + crates/golden/goldens/graphic_template.png | Bin 0 -> 21053 bytes .../goldens/graphic_template.png.attribution | 8 + crates/golden/tests/golden.rs | 53 ++ crates/ui-egui/src/control.rs | 16 + crates/ui-egui/src/lib.rs | 8 +- crates/ui-egui/src/menus.rs | 6 + crates/ui-egui/src/panels/dialogs.rs | 1 + crates/ui-egui/src/panels/graphics.rs | 44 +- .../ui-egui/src/panels/graphics_templates.rs | 784 ++++++++++++++++++ crates/ui-egui/src/panels/mod.rs | 15 +- crates/ui-egui/src/panels/project.rs | 5 +- crates/ui-egui/src/panels/timeline.rs | 22 + crates/ui-egui/src/state.rs | 5 + crates/ui-egui/tests/graphics_templates_ui.rs | 205 +++++ docs/graphics.md | 16 + docs/testing.md | 3 +- 19 files changed, 1194 insertions(+), 7 deletions(-) create mode 100644 crates/golden/goldens/graphic_rich_text.png create mode 100644 crates/golden/goldens/graphic_rich_text.png.attribution create mode 100644 crates/golden/goldens/graphic_template.png create mode 100644 crates/golden/goldens/graphic_template.png.attribution create mode 100644 crates/ui-egui/src/panels/graphics_templates.rs create mode 100644 crates/ui-egui/tests/graphics_templates_ui.rs diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 1273dfea1..1b4f22ba5 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -54,6 +54,8 @@ procedural demo footage (`crates/media/src/generators.rs`); no external media. R | `crates/golden/goldens/crop.png` | FilmCraft contributors | Original work: golden reference rendered by FilmCraft (crates/golden/tests/golden.rs, scene `crop`) from procedurally generated media | MIT OR Apache-2.0 | | `crates/golden/goldens/gaussian_blur.png` | FilmCraft contributors | Original work: golden reference rendered by FilmCraft (crates/golden/tests/golden.rs, scene `gaussian_blur`) from procedurally generated media | MIT OR Apache-2.0 | | `crates/golden/goldens/graphic_shapes.png` | FilmCraft contributors | Original work: golden reference rendered by FilmCraft (crates/golden/tests/golden.rs, scene `graphic_shapes`) from procedurally generated media and bundled OFL fonts | MIT OR Apache-2.0 | +| `crates/golden/goldens/graphic_template.png` | FilmCraft contributors | Original work: golden reference rendered by FilmCraft (crates/golden/tests/golden.rs, scene `graphic_template`; a built-in FilmCraft graphics template) from procedurally generated media and bundled OFL fonts | MIT OR Apache-2.0 | +| `crates/golden/goldens/graphic_rich_text.png` | FilmCraft contributors | Original work: golden reference rendered by FilmCraft (crates/golden/tests/golden.rs, scene `graphic_rich_text`) from procedurally generated media and bundled OFL fonts | MIT OR Apache-2.0 | | `crates/golden/goldens/log_to_rec709.png` | FilmCraft contributors | Original work: golden reference rendered by FilmCraft (crates/golden/tests/golden.rs, scene `log_to_rec709`) from procedurally generated media re-encoded as S-Log3 | MIT OR Apache-2.0 | | `crates/golden/goldens/hdr_tone_map.png` | FilmCraft contributors | Original work: golden reference rendered by FilmCraft (crates/golden/tests/golden.rs, scene `hdr_tone_map`) from procedurally generated media re-encoded as Rec. 2100 PQ | MIT OR Apache-2.0 | | `crates/golden/goldens/graphic_title.png` | FilmCraft contributors | Original work: golden reference rendered by FilmCraft (crates/golden/tests/golden.rs, scene `graphic_title`) from procedurally generated media and bundled OFL fonts | MIT OR Apache-2.0 | diff --git a/crates/golden/goldens/graphic_rich_text.png b/crates/golden/goldens/graphic_rich_text.png new file mode 100644 index 0000000000000000000000000000000000000000..34ef3494a1634d6b4fecc43a67a09e167736dbb7 GIT binary patch literal 25584 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 P00000NkvXXu0mjf(&z&} literal 0 HcmV?d00001 diff --git a/crates/golden/goldens/graphic_rich_text.png.attribution b/crates/golden/goldens/graphic_rich_text.png.attribution new file mode 100644 index 000000000..8d08d3ac4 --- /dev/null +++ b/crates/golden/goldens/graphic_rich_text.png.attribution @@ -0,0 +1,8 @@ +asset: graphic_rich_text.png +title: Per-character text styles and a box pinned around the text +author: FilmCraft contributors +source: original work (rendered by FilmCraft's own renderer from a procedurally generated project; no external media) +license: MIT OR Apache-2.0 +license-file: +added: 2026-10-03 by FilmCraft contributors (FILMCRAFT_BLESS=1) +notes: Golden reference image generated by crates/golden/tests/golden.rs (graphic_rich_text). Regenerate with FILMCRAFT_BLESS=1. Contains no Adobe artwork. diff --git a/crates/golden/goldens/graphic_template.png b/crates/golden/goldens/graphic_template.png new file mode 100644 index 0000000000000000000000000000000000000000..75ec9efffa43bd0fe43b3a2a347a2acd4f87ce1f GIT binary patch literal 21053 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 IM6N<$f?sPA4FCWD literal 0 HcmV?d00001 diff --git a/crates/golden/goldens/graphic_template.png.attribution b/crates/golden/goldens/graphic_template.png.attribution new file mode 100644 index 000000000..4e1add78f --- /dev/null +++ b/crates/golden/goldens/graphic_template.png.attribution @@ -0,0 +1,8 @@ +asset: graphic_template.png +title: Graphics template (Lower Third – Slab) with overridden properties +author: FilmCraft contributors +source: original work (rendered by FilmCraft's own renderer from a procedurally generated project; no external media) +license: MIT OR Apache-2.0 +license-file: +added: 2026-10-03 by FilmCraft contributors (FILMCRAFT_BLESS=1) +notes: Golden reference image generated by crates/golden/tests/golden.rs (graphic_template). Regenerate with FILMCRAFT_BLESS=1. Contains no Adobe artwork. diff --git a/crates/golden/tests/golden.rs b/crates/golden/tests/golden.rs index fbbc74db8..3a95baf8a 100644 --- a/crates/golden/tests/golden.rs +++ b/crates/golden/tests/golden.rs @@ -309,6 +309,55 @@ fn graphic_scene(bg: DemoScene, layers: Vec) -> Scene { b.at(12) } +/// A built-in graphics template (Lower Third – Slab) placed on a 320×180 sequence with its +/// editable properties overridden (name, slab and accent colours, hidden role line): checks the +/// template's scaling, pins (the slab widens with the longer name) and the overrides. +fn graphic_template() -> Scene { + use filmcraft_project::gtemplate::{builtin_templates, layer_uid}; + let t = builtin_templates().into_iter().find(|t| t.id == "builtin:lower-third-slab").unwrap(); + let (mut layers, meta) = t.instantiate((W, H)); + let mut over = |control: &str, v: ParamValue| { + let c = t.controls.iter().find(|c| c.id == control).unwrap(); + let l = layers.iter_mut().find(|l| layer_uid(l) == c.layer).unwrap(); + if c.param == "enabled" { + l.enabled = v.as_bool().unwrap(); + } else { + l.params.get_mut(&c.param).unwrap().value = v; + } + }; + over("name", ParamValue::Text("Golden Overrides".into())); + over("slab_color", ParamValue::Color([0.75, 0.15, 0.18, 1.0])); + over("accent_color", ParamValue::Color([0.2, 0.85, 0.4, 1.0])); + over("show_role", ParamValue::Bool(false)); + let mut s = graphic_scene(DemoScene::Forest, layers); + let seq = s.seq; + let q = s.project.sequence_mut(seq).unwrap(); + q.video_tracks[1].items[0].graphic = Some(Box::new(meta)); + s +} + +/// Per-character styles (a bold, bigger, coloured word and an underlined one) and a rounded box +/// pinned around the text on all four edges. +fn graphic_rich_text() -> Scene { + use filmcraft_project::graphic::{new_shape_layer, new_text_layer}; + use filmcraft_project::graphic_design::{CharStyle, LayerExtra, Pin, PinTarget, StyleRun}; + let mut text = new_text_layer("Mixed STYLES in one layer", Vec2::new(28.0, 100.0), 16.0); + let runs = vec![ + StyleRun { start: 6, end: 12, style: CharStyle { size: Some(26.0), faux_bold: Some(true), fill: Some([1.0, 0.8, 0.1, 1.0]), ..Default::default() } }, + StyleRun { + start: 16, + end: 19, + style: CharStyle { underline: Some(true), font: Some("Noto Serif".into()), faux_italic: Some(true), ..Default::default() }, + }, + ]; + text.layer = Some(Box::new(LayerExtra { uid: 1, runs, ..Default::default() })); + let mut bx = new_shape_layer(0, Vec2::new(150.0, 90.0), Vec2::new(10.0, 10.0), vec![]); + set(&mut bx, &[("corner_radius", fl(6.0)), ("fill_color", ParamValue::Color([0.1, 0.12, 0.3, 1.0])), ("opacity", fl(80.0))]); + let pin = Pin { to: PinTarget::Layer(1), left: true, top: true, right: true, bottom: true, offsets: [-10.0, -8.0, 10.0, 8.0] }; + bx.layer = Some(Box::new(LayerExtra { uid: 2, pin: Some(pin), ..Default::default() })); + graphic_scene(DemoScene::Dunes, vec![bx, text]) +} + /// A title: bold centred text with an outer stroke and a soft drop shadow, a lower-third bar with /// rounded corners and a background-boxed caption line. fn graphic_title() -> Scene { @@ -555,6 +604,8 @@ fn scenes() -> Vec<(&'static str, &'static str, fn() -> Scene)> { ("text_burnin", "Timecode and Clip Name burn-in text", text_burnin), ("graphic_title", "Graphic clip: title text with stroke and shadow, lower-third bar", graphic_title), ("graphic_shapes", "Graphic clip: ellipse, polygon, path and rotated text with background", graphic_shapes), + ("graphic_template", "Graphics template (Lower Third – Slab) with overridden properties", graphic_template), + ("graphic_rich_text", "Per-character text styles and a box pinned around the text", graphic_rich_text), ("log_to_rec709", "Colour management: S-Log3/S-Gamut3.Cine footage to Rec. 709 (tone mapped)", log_to_rec709), ("hdr_tone_map", "Colour management: Rec. 2100 PQ footage tone mapped into a Rec. 709 sequence", hdr_tone_map), ("mask_blur", "Masks: Gaussian Blur inside a feathered ellipse mask", mask_blur), @@ -620,6 +671,8 @@ goldens!( text_burnin, graphic_title, graphic_shapes, + graphic_template, + graphic_rich_text, log_to_rec709, hdr_tone_map, mask_blur, diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 0cdfe3428..668de4321 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -208,6 +208,22 @@ pub fn handle(app: &mut FilmcraftApp, ctx: &egui::Context, req: &ControlRequest) Err(e) => return err(format!("`panels`: {e}")), } } + // Essential Graphics ▸ Browse and the template / font dialogs (`panels::graphics_templates`), merged + if let Some(patch) = p.get("gfxTemplates") { + let mut cur = serde_json::to_value(&app.ui.gfx_templates).unwrap_or_default(); + merge(&mut cur, patch); + match serde_json::from_value(cur) { + Ok(v) => app.ui.gfx_templates = v, + Err(e) => return err(format!("`gfxTemplates`: {e}")), + } + } + // the Type tool's text editing state: {"clip", "layer", "caret", "anchor"} (byte offsets) or null + if let Some(v) = p.get("gfxEdit") { + match serde_json::from_value(v.clone()) { + Ok(e) => app.ui.gfx_edit = e, + Err(e) => return err(format!("`gfxEdit`: {e}")), + } + } // fields of the open Edit / Clip / File dialog (`panels::clip_dialogs`) if let Some(m) = p.get("clipDialog").and_then(Value::as_object) { let Some(d) = app.ui.clip_dialog.as_mut() else { return err("no clip dialog is open") }; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3c05e27c2..405c9e341 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -684,14 +684,18 @@ impl FilmcraftApp { self.session.execute("graphics.newFromFile", json!({"path": path})).map_err(|e| e.to_string()) } "captions.import" => { - let paths = self.hooks.pick_files.as_mut().map(|f| f(&["srt", "vtt", "scc"])).unwrap_or_default(); + let paths = self.hooks.pick_files.as_mut().map(|f| f(&["srt", "vtt", "scc", "mcc", "stl", "ttml", "dfxp", "xml"])).unwrap_or_default(); let Some(path) = paths.into_iter().next() else { return Ok(Value::Null) }; self.session.execute("captions.import", json!({"path": path})).map_err(|e| e.to_string()) } "captions.export" => { let name = self.session.state.active_sequence.and_then(|s| self.session.project.item(s)).map(|i| i.name.clone()).unwrap_or_default(); let suggested = format!("{}.srt", name.replace(' ', "_")); - let Some(path) = self.hooks.pick_save_as.as_mut().and_then(|f| f("Captions (SRT, WebVTT, SCC)", &["srt", "vtt", "scc"], &suggested)) else { + let Some(path) = + self.hooks.pick_save_as.as_mut().and_then(|f| { + f("Captions (SRT, WebVTT, SCC, MCC, EBU STL, TTML, DFXP)", &["srt", "vtt", "scc", "mcc", "stl", "ttml", "dfxp"], &suggested) + }) + else { return Ok(Value::Null); }; let mut p = params.clone(); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index cc3ccb2a9..619f9f50b 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -179,6 +179,12 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, params: Val if let Some(r) = crate::panels::settings::route(app, id) { return r; } + if let Some(r) = crate::panels::graphics_templates::route(app, ctx, id, ¶ms) { + if let Err(e) = &r { + app.ui.status = e.clone(); + } + return r; + } if let Some(r) = crate::panels::menu_dialogs::route(app, ctx, id, ¶ms) { if let Err(e) = &r { app.ui.status = e.clone(); diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index bac1ade8f..8a54c2a8f 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -8,6 +8,7 @@ pub fn show(app: &mut FilmcraftApp, ctx: &egui::Context) { crate::panels::color_dialogs::show(app, ctx); crate::panels::clip_dialogs::show(app, ctx); crate::panels::menu_dialogs::show(app, ctx); + crate::panels::graphics_templates::show(app, ctx); crate::panels::presets::save_dialog(app, ctx); crate::panels::audio_fx_editor::show(app, ctx); crate::panels::multicam::show_dialog(app, ctx); diff --git a/crates/ui-egui/src/panels/graphics.rs b/crates/ui-egui/src/panels/graphics.rs index 464a9914b..d2e7f4e3b 100644 --- a/crates/ui-egui/src/panels/graphics.rs +++ b/crates/ui-egui/src/panels/graphics.rs @@ -739,10 +739,33 @@ struct Ctx<'a> { t: Tokens, actions: Vec<(String, Value)>, autos: Vec<(String, Rect, String)>, + /// Characters selected with the Type tool in this layer: character properties apply to them. + range: Option<(usize, usize)>, +} + +/// Character properties a Type-tool selection can carry, with their `graphics.setCharStyle` names. +fn char_style_key(id: &str) -> Option<&'static str> { + Some(match id { + "font" => "font", + "font_style" => "fontStyle", + "size" => "size", + "fill_color" => "color", + "faux_bold" => "bold", + "faux_italic" => "italic", + "underline" => "underline", + "tracking" => "tracking", + "baseline_shift" => "baselineShift", + "caps" => "caps", + _ => return None, + }) } impl Ctx<'_> { fn set(&mut self, id: &str, v: Value) { + if let (Some((a, b)), Some(k)) = (self.range, char_style_key(id)) { + self.actions.push(("graphics.setCharStyle".into(), json!({"clip": self.clip.0, "layer": self.layer, "start": a, "end": b, "style": {k: v}}))); + return; + } self.actions.push(("graphics.set".into(), json!({"clip": self.clip.0, "layer": self.layer, "props": {id: v}}))); } fn auto(&mut self, id: &str, r: Rect, label: &str) { @@ -1051,13 +1074,32 @@ pub fn properties(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { } } } + // ---- template properties (a graphic made from a graphics template) + if it.graphic.as_ref().is_some_and(|m| m.template.is_some()) && section(ui, app, "Template Properties", &t) { + crate::panels::graphics_templates::template_controls(app, ui, clip, &it, mt, &mut autos, &mut actions); + } let Some(l) = sel else { + if section(ui, app, "Responsive Design - Time", &t) { + crate::panels::graphics_templates::responsive_time(app, ui, clip, &it, &mut autos, &mut actions); + } ui.add_space(12.0); ui.label(egui::RichText::new("Select a layer to edit its properties.").color(t.text_faint)); return; }; let e = &it.effects[idx[l]]; - let mut cx = Ctx { clip, layer: l, e, mt, t, actions: Vec::new(), autos: Vec::new() }; + // a Type-tool selection in this layer: character properties apply to the selected characters + let range = app.ui.gfx_edit.as_ref().filter(|ed| ed.clip == clip.0 && ed.layer == l && ed.caret != ed.anchor).and_then(|ed| { + let text = match pv(e, "text", mt) { + ParamValue::Text(s) => s, + _ => return None, + }; + let ch = |b: usize| text.get(..b.min(text.len())).map_or(0, |s| s.chars().count()); + Some((ch(ed.caret.min(ed.anchor)), ch(ed.caret.max(ed.anchor)))) + }); + let mut cx = Ctx { clip, layer: l, e, mt, t, actions: Vec::new(), autos: Vec::new(), range }; + if section(ui, app, "Responsive Design - Position", &t) { + crate::panels::graphics_templates::responsive_position(app, ui, clip, &it, l, &mut cx.autos, &mut cx.actions); + } // ---- align and transform if section(ui, app, "Align and Transform", &t) { ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/panels/graphics_templates.rs b/crates/ui-egui/src/panels/graphics_templates.rs new file mode 100644 index 000000000..eb402ee8c --- /dev/null +++ b/crates/ui-egui/src/panels/graphics_templates.rs @@ -0,0 +1,784 @@ +//! Essential Graphics ▸ Browse (the Graphics Templates library), the template / responsive-design +//! sections of the Edit tab, and the Export As Motion Graphics Template, Install Motion Graphics +//! Template and Replace Fonts in Projects dialogs (M10.7). +//! +//! The library lists the built-in templates (original FilmCraft designs) and the user's +//! `.fcgt` templates with thumbnails rendered by the engine, a search field and a category +//! filter. Click selects, double-click (or Apply) places the template at the playhead, dragging a +//! card onto the timeline places it there. Everything runs engine commands +//! (`graphics.template.*`, `graphics.setRoll`, `graphics.setResponsiveTime`, `graphics.pin`, +//! `file.replaceFonts`). +//! +//! Automation ids: `essentialGraphics.tab.browse|edit`, `gfxTemplates.search`, +//! `gfxTemplates.category`, `gfxTemplates.item.