Files
seif cfe0c751e9 #395: Arabic and Hebrew in the interface: fallback fonts, captions in reading order
The interface had no face for Arabic or Hebrew, so caption text, track and media names showed
missing-glyph boxes in the Text panel and on the timeline while the Program monitor drew them.
crates/ui-egui/src/rtl.rs adds the craft-fonts Arab / Hebr faces, else one installed face per
script (Noto Sans Arabic, SF Arabic, Segoe UI...), as the last fallbacks of every UI font family,
as cjk.rs does for Chinese.

egui already shapes each font run with harfrust, so the letters join, but it lays runs out left to
right with no bidi pass and multi-word Arabic read backwards. rtl::Lines lays captions out in
reading order: lines are broken on the logical text as egui wraps it, each line is reordered with
unicode-bidi (UAX #9, as crates/text does for titles) and every right-to-left word is its own
layout section for egui's shaper. Caption blocks on the timeline and the Text panel caption list
use it; a caption being edited keeps egui's logical order, since TextEdit places the caret by
character index. Left-to-right text is laid out exactly as before.
2026-10-11 13:14:30 -06:00

18 KiB
Raw Permalink Blame History

Contributing

Read AGENTS.md first. Its rules on assets, clean-room code and licences are absolute and override everything here. Then read architecture.md and the README of the crate you will change.

1. Setup

All platforms
Rust stable ≥ 1.95 (edition 2024; egui 0.36 requires it), via rustup
wasm target rustup target add wasm32-unknown-unknown (for cargo xtask wasm)
Components rustup component add rustfmt clippy
ffmpeg + ffprobe (optional) test oracle and fixture generator only; tests skip without it
OS Extra
macOS Xcode Command Line Tools. ffmpeg: brew install ffmpeg (includes libx264/libx265).
Linux C toolchain and the system headers cpal and rfd need: ALSA (libasound2-dev) and GTK 3 (libgtk-3-dev), plus the usual X11/Wayland development packages for winit. ffmpeg from your distribution, built with libx264 and libx265.
Windows MSVC build tools. The oracle tests look for ffmpeg only in /opt/homebrew/bin, /usr/local/bin and /usr/bin, so most of them skip on Windows.

The oracle tests find ffmpeg at /opt/homebrew/bin, /usr/local/bin or /usr/bin. H.264 and HEVC fixtures need an ffmpeg built with libx264 and libx265. The VideoToolbox fixtures run only on macOS.

2. Build and run

cargo run --release -p filmcraft                      # desktop app with the demo project
cargo run --release -p filmcraft -- --empty           # start without the demo project
cargo run --release -p filmcraft -- --control 9876    # plus the JSON-lines control server
cargo run --release -p filmcraft -- a.mp4 b.wav       # import media (or open a .fcproj)
cargo run --release -p filmcraft-cli -- commands      # list engine commands
cargo run --release -p filmcraft-cli -- render --demo --seconds 3 --out frame.png
cargo run --release -p filmcraft-cli -- mcp --demo    # headless MCP server on stdio
cargo xtask web --serve 8765                          # the web app on http://127.0.0.1:8765/ (docs/web.md)
Variable Effect
FILMCRAFT_CONTROL_PORT Same as --control <port>
FILMCRAFT_CPU_COMPOSITE=1 Disables the GPU compositor
FILMCRAFT_DEV_DESKTOP_ENTRY=1 Linux, opt-in: a run from a source tree installs a user-level desktop entry (and the icons from assets/app-icon) so the taskbar shows the FilmCraft icon. Nothing is written without it; packaged runs never write one
FILMCRAFT_DATA_DIR Same as --data-dir <dir>: auto-save, crash recovery, settings and logs
RUST_LOG Log levels for standard error and the log file (see Logs)

filmcraft --help lists the app's options. An option it does not know, or a control port that is not a number (from --control or from FILMCRAFT_CONTROL_PORT), is an error on stderr with exit code 2 instead of a window.

Dev builds compile dependencies at opt-level = 2 and workspace crates at opt-level = 1. For playback and codec speed, use --release.

Logs

The desktop app writes its log records to standard error and to Logs/filmcraft.log in the data directory (~/.local/share/filmcraft/Logs/ or $XDG_DATA_HOME/filmcraft/Logs/ on Linux, ~/Library/Application Support/FilmCraft/Logs/ on macOS, %APPDATA%\FilmCraft\Logs\ on Windows, or under --data-dir / FILMCRAFT_DATA_DIR), next to the crash logs; Help ▸ Reveal Log Files opens the folder. A start from a desktop menu or the Dock has no terminal, so this file is what to attach to a bug report: GPU compositor fallbacks, audio output problems, panicked frame jobs and container warnings all land there. Each launch moves the previous log to filmcraft.1.log (and that one to filmcraft.2.log), so the log of a run that crashed survives the next start. The file stops growing at 16 MiB. --help, --version and command-line errors write no file.

By default FilmCraft's own crates log at info and everything else at warn. RUST_LOG replaces that with env_logger-style directives, for example RUST_LOG=debug, RUST_LOG=warn,filmcraft_codecs=trace or RUST_LOG=info,wgpu_core=warn; a directive ending in * covers every target starting with it (filmcraft*=debug). The logger is apps/filmcraft/src/logging.rs; the realtime audio callbacks do not log. The web app logs to the browser console instead.

Building with craft-fonts

Font assets live in storytold/craft-fonts, never in this repo (standard). It is an optional build input, not a Cargo dependency:

git clone https://github.com/storytold/craft-fonts ../craft-fonts
CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo run --release -p filmcraft

Use an absolute path: build.rs runs in crates/text, so a relative CRAFT_FONTS_DIR would resolve from there. crates/text/build.rs reads its fonts/manifest.txt and embeds the fonts as filmcraft_text::fonts::CRAFT_FONTS (native: all of them; the web build: only BIZ UDPGothic Regular, to keep the wasm small). The Japanese ones become the last fallback of every UI font family (crates/ui-egui/src/theme.rs, BIZ UDPGothic first) and fallback faces in the text engine (Gothic for sans text, Mincho for serif text). Without CRAFT_FONTS_DIR, CRAFT_FONTS is empty and FilmCraft uses its bundled and system fonts. A CRAFT_FONTS_DIR that is not a checkout is a build warning, or an error with CRAFT_FONTS_REQUIRED=1 (release builds set both). Tests on these fonts' glyphs skip when it is empty; when you touch fonts, run the gates both with and without it.

Chinese media names and track labels use the Hans / Hant craft-fonts faces after the Japanese fallbacks in every UI font family, regardless of interface language (Japanese and Chinese share code points, so Japanese text keeps Japanese glyph forms; the Chinese faces cover hanzi the Japanese ones lack). Without those build inputs, native builds look up an installed Chinese face (for example Microsoft YaHei or Noto Sans CJK SC) once and reuse it across theme changes. No system font files are bundled or copied into the app. Web builds depend on the font faces selected by crates/text/build.rs; they cannot scan system fonts. theme::tests::chinese_names_render_in_every_theme_and_font_family verifies Chinese glyphs in the timeline's proportional, monospace, medium and semibold families under all three themes.

Arabic and Hebrew work the same way (crates/ui-egui/src/rtl.rs): the Arab / Hebr craft-fonts faces, else one installed face per script (for example Noto Sans Arabic, SF Arabic or Segoe UI), are the last fallbacks of every UI font family. egui shapes each font run (letters join) but lays runs out left to right with no bidi pass, so captions on the timeline and in the Text panel are laid out in reading order by rtl::Lines (UAX #9 via unicode-bidi, each right-to-left word its own layout section). A caption being edited shows egui's logical order, since TextEdit places the caret by character index.

3. Quality gates

Every commit must pass all of these:

Gate Command
Format cargo fmt --check (rustfmt.toml: max_width = 160)
Lints cargo clippy --workspace --all-targets -- -D warnings
Tests cargo test --workspace
Layering cargo xtask layers
Assets cargo xtask assets
wasm cargo xtask wasm (checks every L0–L4 crate, filmcraft-ui-egui and filmcraft-web for wasm32-unknown-unknown)
All of the above cargo xtask ci (runs clippy and tests with --release)

Run them with CRAFT_FONTS_DIR set too when you change font code (craft-fonts).

cargo xtask is an alias in .cargo/config.toml for cargo run -p xtask --.

Never crash

Production code must never crash: a crash loses someone's work, so this outranks feature work. The rules are in AGENTS.md §0. In short: return Result<T, E> instead of unwrap() / expect() / panic! / unreachable! / todo! / unimplemented!; no unsafe; use get(), checked_* / saturating_* and validated bounds on anything that came from a file, a command parameter or the UI; bound recursion; tolerate lock poisoning; catch panics in background threads and report them as errors. Clean crates deny the panicking lints outside tests (clippy.toml allows them in tests). Every crash fix comes with a regression test; parsers, decoders and commands get fuzz / hostile-input tests run under catch_unwind.

4. Commits

  • One task per commit. The subject starts with the task id: M3.2: trim mode engine — …, M9.9: Matroska/WebM import. Milestones (M0–M16) are listed in ROADMAP.md. If your change has no task id, use the area: docs: …, README: …, ROADMAP: ….
  • Commit only green states (all gates pass).
  • When a milestone lands, update its row in roadmap.md (status, what's done, estimates), the summary in ROADMAP.md, and the parity doc and gaps.md entry it moves, with their "Last updated" lines and revision history.
  • AI agents end commit messages with the attribution line their environment requires.

5. Parallel work (several agents or worktrees)

git worktree add ../filmcraft-hevc -b hevc-work
cd ../filmcraft-hevc
export CARGO_TARGET_DIR=target/agent-hevc     # separate build dir, no lock fights
  • One owner per crate at a time.
  • The workspace includes crates/* and apps/* by glob, so one broken Cargo.toml breaks every build. Keep every manifest valid at all times, including half-finished crates.
  • Test fixtures always go to <repo>/target/fixtures/, whatever CARGO_TARGET_DIR says.
  • .mcp.json points at target/release/filmcraft-cli. With a custom target dir, use your own path.

6. How to add…

A command

  1. Add a cmd! (action) or query! (read-only) entry to build() in crates/engine/src/commands.rs:
    cmd!("sequence.addEdit", "Add Edit", ["Sequence"], Some("Cmd+K"), r#"{"time":ticks?}"#, has_seq, |s, p| {
        let t = time_p(s, p, "").unwrap_or(s.playhead());
        let tg = s.targeting().targeted;
        let n = s.edit_sequence("Add Edit", |q, ctx, _| Ok(edit::razor(q, &tg, t, ctx)))?;
        Ok(json!({"cuts": n.len()}))
    }),
    
    • Id: area.camelCase, following the menu it appears in.
    • enabled: reuse a predicate (always, has_seq, has_selection, has_in_out, …) or write one that returns Err("human reason").
    • Read time with time_p so time, frame, seconds and timecode all work.
    • Mutate only through s.edit(…) or s.edit_sequence(…), so the change is undoable and validated.
    • Put pure timeline logic in filmcraft-edit as a function on &mut Sequence, with unit or property tests there.
  2. Add tests to crates/engine/src/tests.rs: execute, assert, undo, redo, and check the disabled case.
  3. Frontend-only actions (tools, zoom, panels) go in UI_COMMANDS in crates/ui-egui/src/menus.rs with a branch in menus::invoke.
  4. The command now appears in menus (from menu), shortcuts, filmcraft-cli commands, engine.commands and MCP command_list without further work.

A video effect

  1. Define it in build_effects() in crates/project/src/effect.rs with the video(id, name, CATEGORY, params) helper. Parameter helpers cover floats with slider ranges, choices, bools, angles, points, colours, curves and wheels. The Effects panel, Effect Controls and agent parameter docs are generated from this.
  2. Implement it as a match arm in render::effects::apply (crates/render/src/effects.rs). It works on a linear-light premultiplied f32 Image. Scale pixel-size parameters by cx.px_scale so reduced-resolution playback matches full resolution. Unknown ids are a no-op.
  3. Add a test in crates/render/src/tests.rs.
  4. The GPU path needs no change: render::plan pre-renders layers with standard effects on the CPU. To run it on the GPU too, make it an FxOp in crates/render/src/gpufx.rs instead (parameter evaluation in FxOp::eval, the CPU reference in FxOp::apply, the id in GPU_EFFECTS), add its passes to crates/gpu/src/fx.rs and its math to fx.wgsl, and a parity case to crates/gpu/src/fx_tests.rs.

Audio effects implement AudioEffect in crates/audio-dsp and register in its effects() registry. Then add an audio(…) definition in effect.rs and a Mapping from project parameters to DSP parameters in crates/render/src/audio_fx.rs. The DSP must give the same output however the stream is cut into blocks (there are tests for this).

A transition

  1. Define video transitions with tr(id, name, FOLDER, badges, params) in crates/project/src/vtransition.rs (folders follow Premiere 26: ten Video Transitions/* folders plus Legacy/Video Transitions). Parameters are not animatable. Never rename an id: projects store it. Audio transitions are EffectKind::AudioTransition defs in effect.rs.
  2. Implement it in the matching module of crates/render/src/transitions/ (wipe, motion, dissolve, lights, grunge, special): read params through Tx, build pixels with paint, shape wipes with wipe::field_wipe (feather/border/anti-aliasing for free) and 3D moves with cards. Audio crossfade curves go in transitions::audio_gains. The property tests run over every transition automatically; re-bless goldens.txt (see testing.md).
  3. It can then be applied with sequence.applyVideoTransition {"effect": "<id or name>", "params": {...}, "reverse": bool} and edited with sequence.setTransition; effects.list {"folder": "Video Transitions/Wipe", "detail": true} lists it with its parameters.

A codec or container crate

  1. Spec first. Work only from the public specification (ITU-T, ISO/IEC, IETF RFCs, SMPTE, published container specs). Never copy GPL/LGPL code, and follow AGENTS.md §2 on reading other implementations. Name the spec edition in the crate README.
  2. Create crates/<name> as an L0 crate that depends only on filmcraft-bitstream (plus thiserror and optional rayon behind a threads feature). Add [lints] workspace = true, add it to [workspace.dependencies], and add it to LAYERS in xtask/src/main.rs. It must build for wasm32.
  3. Test it against ffmpeg as an oracle (see testing.md):
    • generate fixtures with ffmpeg on first use into target/fixtures/<crate>/;
    • skip with a message when ffmpeg is missing;
    • never commit media.
  4. Wire it in:
    • decoders: a VideoDecoderFactory in crates/codecs (video.rs, default list in lib.rs);
    • containers: an Opener in codecs::openers();
    • encoders: an export::VideoEncoder / audio encoder plus a Format in crates/export.
  5. Write the README: features, API, test matrix, accuracy and speed numbers, limitations. Use crates/h264/README.md as the model.

A panel or widget

  • Panels live in crates/ui-egui/src/panels/ and are listed in PanelKind (crates/ui-egui/src/dock.rs).
  • Every interactive element registers an automation id each frame:
    app.auto.add(&format!("effects.item.{}", d.id), rect, d.name);
    
    Ids are stable, dot-separated and start with the panel (timeline.track.V1.lock, tools.Razor).
  • UI state that should survive or be scriptable goes in crates/ui-egui/src/state.rs (serde), so ui.inspect and ui.set can reach it.
  • Project changes go through session.execute(…), never by mutating the project directly.
  • Icons are drawn in code in crates/ui-egui/src/icons.rs, from scratch (AGENTS.md §1).
  • Look at the result: run with --control, drive it, take ui.screenshot (agents.md).

An asset

The rules are in AGENTS.md §1. In short:

  • No Adobe iconography, images, fonts, LUTs, presets or other Adobe assets, in any form, including traced or "inspired-by" copies.

  • Open licences only (MIT, Apache-2.0, BSD, ISC, zlib, OFL, CC0, CC BY, CC BY-SA, or your own work under MIT OR Apache-2.0). No NC or ND licences. If the licence is unclear, leave the asset out.

  • Screenshots show only FilmCraft (or other open projects) with media we generated or that is openly licensed.

  • Fonts live in storytold/craft-fonts, not here. Don't commit new font files to this repo (the Latin UI fonts already in assets/fonts/ stay): add the font to craft-fonts (file, licence, manifest line, ATTRIBUTION.md row) and use it through filmcraft_text::fonts::CRAFT_FONTS. See Building with craft-fonts and craftrules/standards/fonts.md.

For each asset file X, commit a sidecar X.attribution. This is the real one for assets/fonts/Inter-Regular.ttf:

asset:        Inter-Regular.ttf
title:        Inter Regular (UI typeface)
author:       The Inter Project Authors (Rasmus Andersson et al.)
source:       https://github.com/rsms/inter
license:      OFL-1.1
license-file: assets/fonts/OFL-Inter.txt
added:        2026-09-30 by Claude (agent) for Brandon Thomas
notes:        Unmodified font file, embedded in the UI via include_bytes! (crates/ui-egui/src/theme.rs).

For your own work, use source: original work and the project licence (MIT OR Apache-2.0).

Then add a row to ATTRIBUTION.md with the path in backticks. cargo xtask assets checks every tracked file with a media extension (images, fonts, LUTs, audio, video, PDF, project files…):

  • the sidecar exists;
  • asset, title, author, source, license and added are filled in;
  • the licence is not NC, ND, Adobe or proprietary;
  • the path appears in ATTRIBUTION.md;
  • the path does not mention Adobe or Premiere;
  • no sidecar is orphaned.