Refactor ui-egui for strict Clippy checks

This commit is contained in:
Dylan M. Taylor
2026-10-28 02:05:38 +00:00
parent 9b9b410981
commit 9bd2f83473
41 changed files with 507 additions and 368 deletions
-4
View File
@@ -1,9 +1,5 @@
#[cfg(test)]
use super::audio::shuttle_mix_rate;
use super::audio::{AUDIO_RETRY_S, AUDIO_STALL_S};
use super::gpu::{gpu_compositor_unsupported, plan_side};
#[cfg(test)]
use super::hooks::AudioOut;
use super::hooks::HostHooks;
use super::raw_input::drop_native_input;
#[cfg(target_os = "macos")]
+5 -1
View File
@@ -1,4 +1,8 @@
use super::*;
use super::{FilmcraftApp, menus};
#[cfg(test)]
use filmcraft_engine::Session;
#[cfg(test)]
use serde_json::json;
impl FilmcraftApp {
// ---------------------------------------------------------------- input
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{ControlRequest, FilmcraftApp, capture, json};
use crate::control;
impl FilmcraftApp {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{EXPORT_SAVE_DIALOGS, FileDialog, FilmcraftApp, Value, json, panels};
impl FilmcraftApp {
// ---------------------------------------------------------------- files
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{FilmcraftApp, Tick, Tokens, crash, header, i18n, panels, state, updates_ui};
impl FilmcraftApp {
// ---------------------------------------------------------------- frame
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{FilmcraftApp, FrameKey, GpuState, frames, gpu_compositor_unsupported, plan_side};
impl FilmcraftApp {
/// Enable the GPU compositor on the eframe wgpu device.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{FilmcraftApp, ImeComposition, PanelKind, ime_reset, panels};
impl FilmcraftApp {
/// Start a new replacement epoch for the Program monitor's text edit target.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{FilmcraftApp, PanelKind, dock_ui, json};
impl FilmcraftApp {
/// Import dropped files, on the main window and on dock panels in their own OS windows.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Dialog, FilmcraftApp, PanelKind, dock, dock_ui, json, menus, panels};
impl FilmcraftApp {
/// Per-pass shortcut handling: reset the pasted image, rebuild bindings only when the shortcut revision or workspace set changes, and leave unmodified keys
+3 -1
View File
@@ -1,4 +1,6 @@
use super::*;
use super::{AUDIO_RETRY_S, AUDIO_STALL_S, FilmcraftApp, Tick, frames, json, panels, play_ahead, shuttle};
#[cfg(test)]
use filmcraft_engine::Session;
impl FilmcraftApp {
// ---------------------------------------------------------------- playback
@@ -1,4 +1,7 @@
use super::*;
use crate::{AudioOut, shuttle_mix_rate};
use filmcraft_engine::Session;
use serde_json::json;
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{FilmcraftApp, ThemeKind, Tokens, dock, i18n, state, theme};
impl FilmcraftApp {
/// Show theme `k` with the Settings ▸ Appearance highlight colour and contrast.
+5 -3
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Session, panels};
/// How long (s) the audio clock may stand still during playback before the wall clock takes over.
/// An output stream can open and then never call back (ALSA with a busy or misconfigured device,
@@ -52,7 +52,7 @@ pub fn playback_mix(session: &Session, seq_id: filmcraft_project::ItemId, sr: u3
use filmcraft_audio_dsp::channels::Layout;
let layout = if ch >= 6 && seq.settings.audio_master == filmcraft_project::AudioChannels::Surround51 { Layout::Surround51 } else { Layout::Stereo };
let mix = if seq_sr == sr {
previews.mix_layout(&project, seq_id, cursor, n, &provider, layout, mixdown)
previews.mix_layout(&project, seq_id, cursor, n, &provider, filmcraft_engine::previews::MixOutput { layout, mixdown })
} else {
// Mix at the sequence rate as one continuous stream, interpolated to the device rate
// (started afresh when the rate or the channel layout changes while playing).
@@ -64,7 +64,9 @@ pub fn playback_mix(session: &Session, seq_id: filmcraft_project::ItemId, sr: u3
buf.fill(0.0);
return;
};
let channels = r.process(cursor, n, |s0, m| previews.mix_layout(&project, seq_id, s0, m, &provider, layout, mixdown).channels);
let channels = r.process(cursor, n, |s0, m| {
previews.mix_layout(&project, seq_id, s0, m, &provider, filmcraft_engine::previews::MixOutput { layout, mixdown }).channels
});
filmcraft_frame::AudioBuffer { sample_rate: sr, channels }
};
if layout == Layout::Surround51 {
+270 -261
View File
@@ -117,274 +117,228 @@ fn point(app: &FilmcraftApp, p: &Value) -> Result<egui::Pos2, String> {
Ok(egui::pos2(x as f32, y as f32))
}
/// Execute one control request. Synthetic input is capped by `MAX_SYNTHETIC_EVENTS` before queueing, screenshot crops stay symbolic until capture, and
/// `gfxEdit` layer identity is resolved from the document.
pub fn handle(app: &mut FilmcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome {
let p = &req.params;
fn handle_ui_set(app: &mut FilmcraftApp, ctx: &egui::Context, p: &Value) -> Outcome {
let s = |k: &str| p.get(k).and_then(Value::as_str);
if matches!(req.method.as_str(), "ui.click" | "ui.move" | "ui.drag" | "ui.scroll" | "ui.key" | "ui.type") && app.synthetic.len() >= MAX_SYNTHETIC_EVENTS {
return err("too many synthetic input events are queued; wait for the current input to be processed");
// Text to Speech panel draft (voice, pitch, pace, text…), merged into the current one
if let Some(patch) = p.get("tts") {
let mut cur = serde_json::to_value(&app.ui.tts).unwrap_or_default();
merge(&mut cur, patch);
match serde_json::from_value(cur) {
Ok(d) => app.ui.tts = d,
Err(e) => return err(format!("bad `tts`: {e}")),
}
}
match req.method.as_str() {
"engine.execute" | "ui.menu.invoke" => {
let Some(id) = s("command").or(s("id")) else { return err("missing `command`") };
let params = p.get("params").cloned().unwrap_or(json!({}));
match crate::menus::invoke(app, ctx, id, params) {
Ok(v) => ok(v),
Err(e) => err(e),
}
if let Some(t) = s("tool") {
match Tool::from_name(t) {
Some(t) => app.ui.tool = t,
None => return err(format!("unknown tool `{t}`")),
}
"engine.commands" => match app.session.execute("command.list", json!({})) {
Ok(v) => ok(v),
Err(e) => err(e),
},
"ui.menu.list" => ok(serde_json::to_value(crate::menus::menu_items(app)).unwrap_or_default()),
"ui.inspect" => ok(inspect(app, ctx)),
"ui.docking" => {
let hits = craft_ui::docking::DockArea::<PanelKind>::hit_rects(ctx, crate::dock_ui::area_id());
let hits: Vec<Value> = hits.iter().map(|(hit, r)| json!({"hit": hit, "rect": [r.left(), r.top(), r.width(), r.height()]})).collect();
ok(json!({"layout": app.ui.dock, "focused": app.ui.focused, "maximized": app.ui.keys.maximized, "hitRects": hits}))
}
if let Some(w) = s("workspace") {
match crate::dock::find(&app.workspaces, w) {
Some(w) => app.set_workspace(&w),
None => return err(format!("unknown workspace `{w}`")),
}
"perf.stats" => ok(crate::perf::stats(app)),
"ui.elements" => {
if app.timeline_still < 2 {
// rects of timeline elements are still moving (e.g. the fit after opening a sequence)
return Outcome::Retry("the timeline is still zooming".into());
}
let prefix = s("prefix").unwrap_or("");
ok(serde_json::to_value(app.auto.query(prefix)).unwrap_or_default())
}
if let Some(m) = s("mode") {
app.ui.mode = match m.to_ascii_lowercase().as_str() {
"import" => Mode::Import,
"export" => Mode::Export,
_ => Mode::Edit,
};
}
if let Some(th) = s("theme") {
match crate::theme::ThemeKind::from_name(th) {
Some(k) => crate::panels::settings::set_theme(app, ctx, k),
None => return err("unknown theme (dark, medium, light)"),
}
"ui.set" => {
// Text to Speech panel draft (voice, pitch, pace, text…), merged into the current one
if let Some(patch) = p.get("tts") {
let mut cur = serde_json::to_value(&app.ui.tts).unwrap_or_default();
merge(&mut cur, patch);
match serde_json::from_value(cur) {
Ok(d) => app.ui.tts = d,
Err(e) => return err(format!("bad `tts`: {e}")),
}
}
if let Some(t) = s("tool") {
match Tool::from_name(t) {
Some(t) => app.ui.tool = t,
None => return err(format!("unknown tool `{t}`")),
}
}
if let Some(w) = s("workspace") {
match crate::dock::find(&app.workspaces, w) {
Some(w) => app.set_workspace(&w),
None => return err(format!("unknown workspace `{w}`")),
}
}
if let Some(m) = s("mode") {
app.ui.mode = match m.to_ascii_lowercase().as_str() {
"import" => Mode::Import,
"export" => Mode::Export,
_ => Mode::Edit,
};
}
if let Some(th) = s("theme") {
match crate::theme::ThemeKind::from_name(th) {
Some(k) => crate::panels::settings::set_theme(app, ctx, k),
None => return err("unknown theme (dark, medium, light)"),
}
}
// the open Settings dialog: page and draft values (`{"page": id, "values": {key: value}}`)
if let Some(st) = p.get("settings")
&& let Err(e) = crate::panels::settings::patch(app, st)
{
return err(e);
}
if let Some(f) = s("focused").and_then(PanelKind::from_name) {
app.ui.focused = f;
}
if let Some(r) = s("playbackRes") {
app.ui.program.res = PlaybackRes::ALL
.into_iter()
.find(|x| x.label().eq_ignore_ascii_case(r) || format!("{x:?}").eq_ignore_ascii_case(r))
.unwrap_or(app.ui.program.res);
}
if let Some(tl) = p.get("timeline") {
if let Some(v) = tl.get("pps").and_then(Value::as_f64) {
app.ui.timeline.target_pps = v;
}
if let Some(v) = tl.get("scroll").and_then(Value::as_f64) {
app.ui.timeline.target_scroll = v;
}
if tl.get("fit").and_then(Value::as_bool) == Some(true) {
app.ui.timeline.fit_pending = true;
}
if let Some(v) = tl.get("videoTrackHeight").and_then(Value::as_f64) {
app.ui.timeline.video_track_h = v as f32;
}
if let Some(v) = tl.get("audioTrackHeight").and_then(Value::as_f64) {
app.ui.timeline.audio_track_h = v as f32;
}
// Show Video Thumbnails: "off" or a mode ("head", "headAndTail", "continuous")
if let Some(m) = tl.get("thumbnails").and_then(Value::as_str) {
if m.eq_ignore_ascii_case("off") {
app.ui.timeline.show_thumbnails = false;
} else {
let Some(mode) = ThumbnailMode::from_name(m) else {
return err(format!("unknown thumbnail mode `{m}` (off, head, headAndTail, continuous)"));
};
app.ui.timeline.show_thumbnails = true;
app.ui.timeline.thumbnail_mode = mode;
}
}
}
if let Some(q) = s("effectsSearch") {
app.ui.effects_search = q.to_string();
}
// Effect Controls keyframe selection (`[{clip, effect, param, mask?, time}]`), replaced;
// references to missing keyframes are dropped by the panel
if let Some(v) = p.get("keyframeSelection") {
match serde_json::from_value(v.clone()) {
Ok(sel) => app.ui.keyframe_selection = sel,
Err(e) => return err(format!("`keyframeSelection`: {e}")),
}
}
if let Some(v) = p.get("safeMargins").and_then(Value::as_bool) {
app.ui.program.safe_margins = v;
}
// Monitor view state (serde fields of `MonitorView`), merged into the current state.
for k in ["program", "source"] {
let Some(patch) = p.get(k).and_then(Value::as_object) else { continue };
let mv = if k == "program" { &mut app.ui.program } else { &mut app.ui.source };
let mut cur = serde_json::to_value(&*mv).unwrap_or_default();
if let Some(o) = cur.as_object_mut() {
for (f, v) in patch {
o.insert(f.clone(), v.clone());
}
}
match serde_json::from_value(cur) {
Ok(m) => *mv = m,
Err(e) => return err(format!("`{k}`: {e}")),
}
}
// Export mode state (`panels::export_mode::ExportUi`), deep-merged; `"preset": name`
// applies that preset's settings first
if let Some(patch) = p.get("export").filter(|v| v.is_object()) {
if let Some(name) = patch.get("preset").and_then(Value::as_str)
&& name != crate::panels::export_mode::CUSTOM
&& !crate::panels::export_mode::apply_preset(app, name)
{
return err(format!("no export preset named `{name}`"));
}
let mut cur = serde_json::to_value(&app.ui.export).unwrap_or_default();
merge(&mut cur, patch);
match serde_json::from_value(cur) {
Ok(e) => app.ui.export = e,
Err(e) => return err(format!("`export`: {e}")),
}
}
// panel settings (`panels::panel_state`): {"scopes": {...}, "timecode": {...}, …}, merged
if let Some(patch) = p.get("panels") {
let mut cur = serde_json::to_value(&app.ui.panels).unwrap_or_default();
merge(&mut cur, patch);
match serde_json::from_value(cur) {
Ok(v) => app.ui.panels = v,
Err(e) => return err(format!("`panels`: {e}")),
}
}
// Project panel / Media Browser view state (`panels::project`, `panels::media_browser`), merged
for (k, which) in [("projectPanel", 0), ("mediaBrowser", 1)] {
let Some(patch) = p.get(k) else { continue };
let mut cur =
if which == 0 { serde_json::to_value(&app.ui.project_panel) } else { serde_json::to_value(&app.ui.media_browser) }.unwrap_or_default();
merge(&mut cur, patch);
let r = if which == 0 {
serde_json::from_value(cur).map(|v| app.ui.project_panel = v)
} else {
serde_json::from_value(cur).map(|v| app.ui.media_browser = v)
};
if let Err(e) = r {
return err(format!("`{k}`: {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::<Option<crate::state::GfxEdit>>(v.clone()) {
Ok(mut e) => {
// An explicit `ui.set.gfxEdit {clip, layer}` names the layer currently at
// that index. Resolve its identity from the document instead of inheriting
// a stale uid/fingerprint from a previous edit session: after a reorder,
// delete or undo the requested index may now hold a different layer.
if let Some(new) = e.as_mut() {
match crate::panels::graphics::current_layer_identity(&app.session, new.clip, new.layer) {
Some((uid, fingerprint)) => {
new.uid = uid;
new.fingerprint = fingerprint;
}
None => {
new.uid = 0;
new.fingerprint = 0;
}
}
}
let changed = match (&app.ui.gfx_edit, &e) {
(None, None) => false,
(Some(old), Some(new)) => {
// A stable uid is the owner key. The compact fingerprint only
// decides identity for a uid-less layer, and zero there means
// "the requested index" rather than a different owner.
old.clip != new.clip
|| old.layer != new.layer
|| old.uid != new.uid
|| (new.uid == 0 && old.fingerprint != 0 && old.fingerprint != new.fingerprint)
}
_ => true,
};
if changed {
app.restart_ime_target();
} else if let (Some(old), Some(new)) = (&app.ui.gfx_edit, &e)
&& (old.caret, old.anchor) != (new.caret, new.anchor)
{
// Caret is not part of the stable owner. Only a known composition
// becomes stale; commit-only input without one remains eligible.
app.invalidate_ime_composition();
}
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") };
for (k, v) in m {
d.params[k.as_str()] = v.clone();
}
}
// fields of the open M3.11 menu dialog (`panels::menu_dialogs`)
if let Some(m) = p.get("menuDialog").and_then(Value::as_object) {
let Some(d) = app.ui.extras.dialog.as_mut() else { return err("no menu dialog is open") };
for (k, v) in m {
d.params[k.as_str()] = v.clone();
}
}
ok(Value::Null)
}
// the open Settings dialog: page and draft values (`{"page": id, "values": {key: value}}`)
if let Some(st) = p.get("settings")
&& let Err(e) = crate::panels::settings::patch(app, st)
{
return err(e);
}
if let Some(f) = s("focused").and_then(PanelKind::from_name) {
app.ui.focused = f;
}
if let Some(r) = s("playbackRes") {
app.ui.program.res =
PlaybackRes::ALL.into_iter().find(|x| x.label().eq_ignore_ascii_case(r) || format!("{x:?}").eq_ignore_ascii_case(r)).unwrap_or(app.ui.program.res);
}
if let Some(tl) = p.get("timeline") {
if let Some(v) = tl.get("pps").and_then(Value::as_f64) {
app.ui.timeline.target_pps = v;
}
"ui.panel.show" | "ui.panel.close" => {
let Some(panel) = s("panel").and_then(PanelKind::from_name) else { return err("unknown `panel`") };
if req.method == "ui.panel.show" {
app.show_panel(panel);
if let Some(v) = tl.get("scroll").and_then(Value::as_f64) {
app.ui.timeline.target_scroll = v;
}
if tl.get("fit").and_then(Value::as_bool) == Some(true) {
app.ui.timeline.fit_pending = true;
}
if let Some(v) = tl.get("videoTrackHeight").and_then(Value::as_f64) {
app.ui.timeline.video_track_h = v as f32;
}
if let Some(v) = tl.get("audioTrackHeight").and_then(Value::as_f64) {
app.ui.timeline.audio_track_h = v as f32;
}
// Show Video Thumbnails: "off" or a mode ("head", "headAndTail", "continuous")
if let Some(m) = tl.get("thumbnails").and_then(Value::as_str) {
if m.eq_ignore_ascii_case("off") {
app.ui.timeline.show_thumbnails = false;
} else {
if let Err(error) = app.apply_dock(craft_ui::docking::Action::Close { panel }) {
return err(&error);
}
let Some(mode) = ThumbnailMode::from_name(m) else {
return err(format!("unknown thumbnail mode `{m}` (off, head, headAndTail, continuous)"));
};
app.ui.timeline.show_thumbnails = true;
app.ui.timeline.thumbnail_mode = mode;
}
ok(Value::Null)
}
}
if let Some(q) = s("effectsSearch") {
app.ui.effects_search = q.to_string();
}
// Effect Controls keyframe selection (`[{clip, effect, param, mask?, time}]`), replaced;
// references to missing keyframes are dropped by the panel
if let Some(v) = p.get("keyframeSelection") {
match serde_json::from_value(v.clone()) {
Ok(sel) => app.ui.keyframe_selection = sel,
Err(e) => return err(format!("`keyframeSelection`: {e}")),
}
}
if let Some(v) = p.get("safeMargins").and_then(Value::as_bool) {
app.ui.program.safe_margins = v;
}
// Monitor view state (serde fields of `MonitorView`), merged into the current state.
for k in ["program", "source"] {
let Some(patch) = p.get(k).and_then(Value::as_object) else { continue };
let mv = if k == "program" { &mut app.ui.program } else { &mut app.ui.source };
let mut cur = serde_json::to_value(&*mv).unwrap_or_default();
if let Some(o) = cur.as_object_mut() {
for (f, v) in patch {
o.insert(f.clone(), v.clone());
}
}
match serde_json::from_value(cur) {
Ok(m) => *mv = m,
Err(e) => return err(format!("`{k}`: {e}")),
}
}
// Export mode state (`panels::export_mode::ExportUi`), deep-merged; `"preset": name`
// applies that preset's settings first
if let Some(patch) = p.get("export").filter(|v| v.is_object()) {
if let Some(name) = patch.get("preset").and_then(Value::as_str)
&& name != crate::panels::export_mode::CUSTOM
&& !crate::panels::export_mode::apply_preset(app, name)
{
return err(format!("no export preset named `{name}`"));
}
let mut cur = serde_json::to_value(&app.ui.export).unwrap_or_default();
merge(&mut cur, patch);
match serde_json::from_value(cur) {
Ok(e) => app.ui.export = e,
Err(e) => return err(format!("`export`: {e}")),
}
}
// panel settings (`panels::panel_state`): {"scopes": {...}, "timecode": {...}, …}, merged
if let Some(patch) = p.get("panels") {
let mut cur = serde_json::to_value(&app.ui.panels).unwrap_or_default();
merge(&mut cur, patch);
match serde_json::from_value(cur) {
Ok(v) => app.ui.panels = v,
Err(e) => return err(format!("`panels`: {e}")),
}
}
// Project panel / Media Browser view state (`panels::project`, `panels::media_browser`), merged
for (k, which) in [("projectPanel", 0), ("mediaBrowser", 1)] {
let Some(patch) = p.get(k) else { continue };
let mut cur = if which == 0 { serde_json::to_value(&app.ui.project_panel) } else { serde_json::to_value(&app.ui.media_browser) }.unwrap_or_default();
merge(&mut cur, patch);
let r = if which == 0 {
serde_json::from_value(cur).map(|v| app.ui.project_panel = v)
} else {
serde_json::from_value(cur).map(|v| app.ui.media_browser = v)
};
if let Err(e) = r {
return err(format!("`{k}`: {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::<Option<crate::state::GfxEdit>>(v.clone()) {
Ok(mut e) => {
// An explicit `ui.set.gfxEdit {clip, layer}` names the layer currently at
// that index. Resolve its identity from the document instead of inheriting
// a stale uid/fingerprint from a previous edit session: after a reorder,
// delete or undo the requested index may now hold a different layer.
if let Some(new) = e.as_mut() {
match crate::panels::graphics::current_layer_identity(&app.session, new.clip, new.layer) {
Some((uid, fingerprint)) => {
new.uid = uid;
new.fingerprint = fingerprint;
}
None => {
new.uid = 0;
new.fingerprint = 0;
}
}
}
let changed = match (&app.ui.gfx_edit, &e) {
(None, None) => false,
(Some(old), Some(new)) => {
// A stable uid is the owner key. The compact fingerprint only
// decides identity for a uid-less layer, and zero there means
// "the requested index" rather than a different owner.
old.clip != new.clip
|| old.layer != new.layer
|| old.uid != new.uid
|| (new.uid == 0 && old.fingerprint != 0 && old.fingerprint != new.fingerprint)
}
_ => true,
};
if changed {
app.restart_ime_target();
} else if let (Some(old), Some(new)) = (&app.ui.gfx_edit, &e)
&& (old.caret, old.anchor) != (new.caret, new.anchor)
{
// Caret is not part of the stable owner. Only a known composition
// becomes stale; commit-only input without one remains eligible.
app.invalidate_ime_composition();
}
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") };
for (k, v) in m {
d.params[k.as_str()] = v.clone();
}
}
// fields of the open M3.11 menu dialog (`panels::menu_dialogs`)
if let Some(m) = p.get("menuDialog").and_then(Value::as_object) {
let Some(d) = app.ui.extras.dialog.as_mut() else { return err("no menu dialog is open") };
for (k, v) in m {
d.params[k.as_str()] = v.clone();
}
}
ok(Value::Null)
}
fn handle_synthetic_input(app: &mut FilmcraftApp, p: &Value, method: &str) -> Outcome {
let s = |key: &str| p.get(key).and_then(Value::as_str);
match method {
"ui.click" | "ui.move" => {
let pos = match point(app, p) {
Ok(p) => p,
@@ -392,7 +346,7 @@ pub fn handle(app: &mut FilmcraftApp, ctx: &egui::Context, req: &ControlRequest)
Err(e) => return err(e),
};
// validate before queuing anything, so a refused count leaves no modifier held
let count = if req.method == "ui.click" {
let count = if method == "ui.click" {
match bounded_repetitions(p, "count", 1, 0, MAX_CLICK_COUNT) {
Ok(count) => count,
Err(error) => return err(error),
@@ -403,7 +357,7 @@ pub fn handle(app: &mut FilmcraftApp, ctx: &egui::Context, req: &ControlRequest)
let m = modifiers(p);
app.synthetic.push(egui::Event::ModifiersChanged(m));
app.synthetic.push(egui::Event::PointerMoved(pos));
if req.method == "ui.click" {
if method == "ui.click" {
let button = match s("button") {
Some("right") => egui::PointerButton::Secondary,
Some("middle") => egui::PointerButton::Middle,
@@ -482,6 +436,61 @@ pub fn handle(app: &mut FilmcraftApp, ctx: &egui::Context, req: &ControlRequest)
app.synthetic.push(egui::Event::Key { key: egui::Key::F35, physical_key: None, pressed: false, repeat: false, modifiers: Default::default() });
Outcome::AfterInput
}
_ => err("unsupported synthetic input"),
}
}
/// Execute one control request. Synthetic input is capped by `MAX_SYNTHETIC_EVENTS` before queueing, screenshot crops stay symbolic until capture, and
/// `gfxEdit` layer identity is resolved from the document.
pub fn handle(app: &mut FilmcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome {
let p = &req.params;
let s = |k: &str| p.get(k).and_then(Value::as_str);
if matches!(req.method.as_str(), "ui.click" | "ui.move" | "ui.drag" | "ui.scroll" | "ui.key" | "ui.type") && app.synthetic.len() >= MAX_SYNTHETIC_EVENTS {
return err("too many synthetic input events are queued; wait for the current input to be processed");
}
match req.method.as_str() {
"engine.execute" | "ui.menu.invoke" => {
let Some(id) = s("command").or(s("id")) else { return err("missing `command`") };
let params = p.get("params").cloned().unwrap_or(json!({}));
match crate::menus::invoke(app, ctx, id, params) {
Ok(v) => ok(v),
Err(e) => err(e),
}
}
"engine.commands" => match app.session.execute("command.list", json!({})) {
Ok(v) => ok(v),
Err(e) => err(e),
},
"ui.menu.list" => ok(serde_json::to_value(crate::menus::menu_items(app)).unwrap_or_default()),
"ui.inspect" => ok(inspect(app, ctx)),
"ui.docking" => {
let hits = craft_ui::docking::DockArea::<PanelKind>::hit_rects(ctx, crate::dock_ui::area_id());
let hits: Vec<Value> = hits.iter().map(|(hit, r)| json!({"hit": hit, "rect": [r.left(), r.top(), r.width(), r.height()]})).collect();
ok(json!({"layout": app.ui.dock, "focused": app.ui.focused, "maximized": app.ui.keys.maximized, "hitRects": hits}))
}
"perf.stats" => ok(crate::perf::stats(app)),
"ui.elements" => {
if app.timeline_still < 2 {
// rects of timeline elements are still moving (e.g. the fit after opening a sequence)
return Outcome::Retry("the timeline is still zooming".into());
}
let prefix = s("prefix").unwrap_or("");
ok(serde_json::to_value(app.auto.query(prefix)).unwrap_or_default())
}
"ui.set" => handle_ui_set(app, ctx, p),
"ui.panel.show" | "ui.panel.close" => {
let Some(panel) = s("panel").and_then(PanelKind::from_name) else { return err("unknown `panel`") };
if req.method == "ui.panel.show" {
app.show_panel(panel);
} else {
if let Err(error) = app.apply_dock(craft_ui::docking::Action::Close { panel }) {
return err(&error);
}
}
ok(Value::Null)
}
"ui.click" | "ui.move" | "ui.drag" | "ui.scroll" | "ui.key" | "ui.type" => handle_synthetic_input(app, p, req.method.as_str()),
"ui.timeline.hit" => {
let pos = match point(app, p) {
Ok(p) => p,
+31 -4
View File
@@ -415,6 +415,16 @@ pub struct FrameServer {
repaint: Arc<Mutex<Option<Box<dyn Fn() + Send + Sync>>>>,
}
struct FrameRequest<'a> {
key: FrameKey,
time: Tick,
scale: f32,
project: &'a Arc<Project>,
priority: u32,
prefetch: bool,
catch_up: Option<Tick>,
}
/// Frames queued ahead of the playhead while playing (and how far ahead a queued job may be
/// before it is dropped as stale).
pub fn prefetch_depth(speed: f64) -> i64 {
@@ -574,10 +584,11 @@ impl FrameServer {
/// non-reference pictures, while nearby frames stay decoded (and cached) for scrubbing back.
pub fn request(&self, key: FrameKey, time: Tick, scale: f32, project: &Arc<Project>, prio: u32) {
let late = (prio == 0).then(|| Tick::from_seconds_f64(SCRUB_KEEP_S));
self.request_job(key, time, scale, project, prio, false, late);
self.request_job(FrameRequest { key, time, scale, project, priority: prio, prefetch: false, catch_up: late });
}
fn request_job(&self, key: FrameKey, time: Tick, scale: f32, project: &Arc<Project>, prio: u32, prefetch: bool, catch_up: Option<Tick>) {
fn request_job(&self, request: FrameRequest<'_>) {
let FrameRequest { key, time, scale, project, priority: prio, prefetch, catch_up } = request;
let hit = || self.shared.stats.lock().unwrap_or_else(PoisonError::into_inner).request_hits += 1;
if self.shared.done.lock().unwrap_or_else(PoisonError::into_inner).map.contains_key(&key)
|| self.shared.plans.lock().unwrap_or_else(PoisonError::into_inner).map.contains_key(&key)
@@ -639,7 +650,15 @@ impl FrameServer {
let (lead, stride) = if preroll { (1, 1) } else { playback_plan(self.render_cost(), fps, self.shared.workers) };
if lead <= 1 {
// the frame due now; once the clock runs, the frames before it are late
self.request_job(key, rate.tick_of(key.frame), scale, project, 0, false, (!preroll && dir > 0).then_some(Tick::ZERO));
self.request_job(FrameRequest {
key,
time: rate.tick_of(key.frame),
scale,
project,
priority: 0,
prefetch: false,
catch_up: (!preroll && dir > 0).then_some(Tick::ZERO),
});
}
// During preroll the clock has not started: only the frames that gate start, not a
// prefetch run that walks the rest of a LongGOP and evicts the playhead pictures.
@@ -649,7 +668,15 @@ impl FrameServer {
if f >= 0 {
// forward play: frames before the playhead are late (reverse play skips nothing)
let late = (!preroll && dir > 0).then(|| rate.tick_of(f) - rate.tick_of(key.frame));
self.request_job(FrameKey { frame: f, ..key }, rate.tick_of(f), scale, project, i as u32 + 1, true, late);
self.request_job(FrameRequest {
key: FrameKey { frame: f, ..key },
time: rate.tick_of(f),
scale,
project,
priority: i as u32 + 1,
prefetch: true,
catch_up: late,
});
}
}
let (target, rev, cur) = (key.target, key.revision, key.frame);
+1 -1
View File
@@ -5,7 +5,7 @@
use std::sync::Arc;
use super::*;
use super::ime_reset;
use egui::{Context, IMEPurpose, Id, ImeResetId, ImeTarget, ViewportId, ViewportIdMap};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+12 -7
View File
@@ -273,11 +273,16 @@ fn text(ui: &mut egui::Ui, elems: &mut Elems, pre: &str, p: &mut Value, key: &st
});
}
fn number(ui: &mut egui::Ui, elems: &mut Elems, pre: &str, p: &mut Value, key: &str, label: &str, range: std::ops::RangeInclusive<f64>, suffix: &str) {
struct NumberOptions {
range: std::ops::RangeInclusive<f64>,
suffix: &'static str,
}
fn number(ui: &mut egui::Ui, elems: &mut Elems, pre: &str, p: &mut Value, key: &str, label: &str, options: NumberOptions) {
ui.horizontal(|ui| {
ui.label(label);
let mut v = p.get(key).and_then(Value::as_f64).unwrap_or(0.0);
let r = ui.add(crate::widgets::number_drag(&mut v).range(range).suffix(suffix));
let r = ui.add(crate::widgets::number_drag(&mut v).range(options.range).suffix(options.suffix));
push(elems, format!("{pre}.{key}"), &r, label);
if r.changed() {
p[key] = json!(v);
@@ -365,12 +370,12 @@ pub fn show(app: &mut FilmcraftApp, ctx: &egui::Context) {
});
let has_v = p["video"].as_bool().unwrap_or(true);
ui.add_enabled_ui(has_v, |ui| {
number(ui, &mut elems, pre, p, "width", tl!("Width:"), 16.0..=16384.0, " px");
number(ui, &mut elems, pre, p, "height", tl!("Height:"), 16.0..=16384.0, " px");
number(ui, &mut elems, pre, p, "fps", tl!("Frame Rate:"), 1.0..=240.0, " fps");
number(ui, &mut elems, pre, p, "width", tl!("Width:"), NumberOptions { range: 16.0..=16384.0, suffix: " px" });
number(ui, &mut elems, pre, p, "height", tl!("Height:"), NumberOptions { range: 16.0..=16384.0, suffix: " px" });
number(ui, &mut elems, pre, p, "fps", tl!("Frame Rate:"), NumberOptions { range: 1.0..=240.0, suffix: " fps" });
});
text(ui, &mut elems, pre, p, "timecode", tl!("Media Start:"), 120.0);
number(ui, &mut elems, pre, p, "seconds", tl!("Duration:"), 0.05..=86_400.0, " s");
number(ui, &mut elems, pre, p, "seconds", tl!("Duration:"), NumberOptions { range: 0.05..=86_400.0, suffix: " s" });
}
"clip.nest" => {
// the name is ready to type over, and Enter accepts it
@@ -517,7 +522,7 @@ pub fn show(app: &mut FilmcraftApp, ctx: &egui::Context) {
}
}
"clip.speedDuration" => {
number(ui, &mut elems, pre, p, "speed", tl!("Speed:"), 0.01..=100_000.0, " %");
number(ui, &mut elems, pre, p, "speed", tl!("Speed:"), NumberOptions { range: 0.01..=100_000.0, suffix: " %" });
// Duration follows the speed: the media shown stays the same
let (dur, was, fps) =
(d.info["duration"].as_i64().unwrap_or(0), d.info["speed"].as_f64().unwrap_or(1.0), d.info["fps"].as_f64().unwrap_or(24.0));
@@ -1,6 +1,6 @@
use super::quick::list_row;
use super::settings::check;
use super::*;
use super::{Align2, Color32, FileDialog, FilmcraftApp, Rect, Reg, Sense, Value, apply_preset, json, pos2, preset_key, settings_json, vec2};
// ---------------------------------------------------------------------------------------------
// Preset Manager
@@ -1,5 +1,8 @@
use super::settings::{combo, drag};
use super::*;
use super::settings::{DragOptions, combo, drag};
use super::{
Align2, CUSTOM, Color32, ExportUi, FilmcraftApp, FrameKey, Rect, Reg, Scaling, Sense, Target, Tick, Tokens, export_params, format_bytes, json, output_path,
pos2, vec2,
};
// ---------------------------------------------------------------------------------------------
// right: preview, range, summary, buttons
@@ -55,8 +58,8 @@ pub(super) fn preview_column(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Re
ex.range = RANGES[i].0.into();
}
if ex.range == "custom" {
drag(ui, &mut reg, "export.range.start", &mut ex.custom_start, 0.0..=86_400.0, 0.04, " s", 2);
drag(ui, &mut reg, "export.range.end", &mut ex.custom_end, 0.0..=86_400.0, 0.04, " s", 2);
drag(ui, &mut reg, "export.range.start", &mut ex.custom_start, DragOptions { range: 0.0..=86_400.0, speed: 0.04, suffix: " s", decimals: 2 });
drag(ui, &mut reg, "export.range.end", &mut ex.custom_end, DragOptions { range: 0.0..=86_400.0, speed: 0.04, suffix: " s", decimals: 2 });
}
});
if ex.settings.has_video() {
@@ -1,4 +1,4 @@
use super::*;
use super::{Align2, CUSTOM, Color32, FilmcraftApp, Rect, Reg, Tokens, Value, json, pos2, vec2};
// ---------------------------------------------------------------------------------------------
// left: destinations + queue
@@ -1,5 +1,5 @@
use super::settings::{draft_text, location_link};
use super::*;
use super::{Align2, Color32, DEFAULT_PRESET, FilmcraftApp, Reg, Sense, Tokens, builtin_presets, json, pos2, preset_key, vec2};
// ---------------------------------------------------------------------------------------------
// Quick Export (header)
@@ -1,4 +1,7 @@
use super::*;
use super::{
AudioCodec, BitrateMode, ExportSettings, ExrCompression, FieldOrder, FileDialog, FilmcraftApp, Format, FrameRate, GpuRendering, H264Profile,
HardwareEncoding, Multiplexer, MxfVideoCodec, Placement, PresetManager, Rect, Reg, SampleFormat, Sense, TextOverlay, Tokens, apply_preset, pos2, vec2,
};
// ---------------------------------------------------------------------------------------------
// middle: settings
@@ -77,18 +80,16 @@ fn section(ui: &mut egui::Ui, reg: &mut Reg, open: &mut Vec<String>, key: &str,
now
}
pub(super) fn drag(
ui: &mut egui::Ui,
reg: &mut Reg,
id: &str,
v: &mut f64,
range: std::ops::RangeInclusive<f64>,
speed: f64,
suffix: &str,
decimals: usize,
) -> bool {
pub(super) struct DragOptions {
pub(super) range: std::ops::RangeInclusive<f64>,
pub(super) speed: f64,
pub(super) suffix: &'static str,
pub(super) decimals: usize,
}
pub(super) fn drag(ui: &mut egui::Ui, reg: &mut Reg, id: &str, v: &mut f64, options: DragOptions) -> bool {
let before = *v;
let r = ui.add(crate::widgets::number_drag(v).range(range).speed(speed).suffix(suffix).max_decimals(decimals));
let r = ui.add(crate::widgets::number_drag(v).range(options.range).speed(options.speed).suffix(options.suffix).max_decimals(options.decimals));
reg.add(id, r.rect, format!("{v}"));
// a drag ended by Escape goes back to the value it started from (egui ends the drag, #580)
let home = r.id.with("before-drag");
@@ -416,9 +417,9 @@ fn video_section(ui: &mut egui::Ui, reg: &mut Reg, s: &mut ExportSettings, t: &T
if let Some((w, h)) = s.frame_size.as_mut() {
row(ui, t, "", |ui| {
let (mut fw, mut fh) = (*w as f64, *h as f64);
drag(ui, reg, "export.video.width", &mut fw, 16.0..=8192.0, 1.0, "", 0);
drag(ui, reg, "export.video.width", &mut fw, DragOptions { range: 16.0..=8192.0, speed: 1.0, suffix: "", decimals: 0 });
ui.label("×");
drag(ui, reg, "export.video.height", &mut fh, 16.0..=8192.0, 1.0, "", 0);
drag(ui, reg, "export.video.height", &mut fh, DragOptions { range: 16.0..=8192.0, speed: 1.0, suffix: "", decimals: 0 });
(*w, *h) = (fw as u32, fh as u32);
});
}
@@ -511,7 +512,7 @@ fn video_section(ui: &mut egui::Ui, reg: &mut Reg, s: &mut ExportSettings, t: &T
if s.bitrate_mode == BitrateMode::Crf {
row(ui, t, tl!("Quality (CRF)"), |ui| {
let mut crf = if s.crf.is_finite() { f64::from(s.crf) } else { f64::from(filmcraft_engine::export::DEFAULT_CRF) };
if drag(ui, reg, "export.video.crf", &mut crf, 0.0..=51.0, 0.1, "", 0) {
if drag(ui, reg, "export.video.crf", &mut crf, DragOptions { range: 0.0..=51.0, speed: 0.1, suffix: "", decimals: 0 }) {
s.crf = crf.round() as f32;
}
});
@@ -527,14 +528,14 @@ fn video_section(ui: &mut egui::Ui, reg: &mut Reg, s: &mut ExportSettings, t: &T
} else {
row(ui, t, tl!("Target Bitrate"), |ui| {
let mut mbps = s.bitrate_kbps as f64 / 1000.0;
if drag(ui, reg, "export.video.target", &mut mbps, 0.1..=800.0, 0.1, " Mbps", 1) {
if drag(ui, reg, "export.video.target", &mut mbps, DragOptions { range: 0.1..=800.0, speed: 0.1, suffix: " Mbps", decimals: 1 }) {
s.bitrate_kbps = (mbps * 1000.0).round() as u32;
}
});
if s.bitrate_mode != BitrateMode::Cbr {
row(ui, t, tl!("Maximum Bitrate"), |ui| {
let mut mbps = s.max_bitrate_kbps.unwrap_or(s.bitrate_kbps * 3 / 2) as f64 / 1000.0;
if drag(ui, reg, "export.video.max", &mut mbps, 0.1..=1000.0, 0.1, " Mbps", 1) {
if drag(ui, reg, "export.video.max", &mut mbps, DragOptions { range: 0.1..=1000.0, speed: 0.1, suffix: " Mbps", decimals: 1 }) {
s.max_bitrate_kbps = Some((mbps * 1000.0).round() as u32);
}
});
@@ -547,7 +548,7 @@ fn video_section(ui: &mut egui::Ui, reg: &mut Reg, s: &mut ExportSettings, t: &T
}
if let Some(k) = s.keyframe_distance.as_mut() {
let mut v = *k as f64;
drag(ui, reg, "export.video.keyframe", &mut v, 1.0..=600.0, 1.0, " frames", 0);
drag(ui, reg, "export.video.keyframe", &mut v, DragOptions { range: 1.0..=600.0, speed: 1.0, suffix: " frames", decimals: 0 });
*k = v as u32;
}
});
@@ -613,7 +614,7 @@ fn video_section(ui: &mut egui::Ui, reg: &mut Reg, s: &mut ExportSettings, t: &T
Format::Mjpeg | Format::JpegSequence => {
row(ui, t, tl!("Quality"), |ui| {
let mut q = s.quality as f64;
drag(ui, reg, "export.video.quality", &mut q, 1.0..=100.0, 1.0, "", 0);
drag(ui, reg, "export.video.quality", &mut q, DragOptions { range: 1.0..=100.0, speed: 1.0, suffix: "", decimals: 0 });
s.quality = q as u8;
});
}
@@ -710,7 +711,7 @@ fn audio_section(ui: &mut egui::Ui, reg: &mut Reg, s: &mut ExportSettings, t: &T
// every level is lossless: higher levels take longer for smaller files
row(ui, t, tl!("Compression Level"), |ui| {
let mut level = f64::from(s.audio.flac_level.min(8));
if drag(ui, reg, "export.audio.flacLevel", &mut level, 0.0..=8.0, 0.05, "", 0) {
if drag(ui, reg, "export.audio.flacLevel", &mut level, DragOptions { range: 0.0..=8.0, speed: 0.05, suffix: "", decimals: 0 }) {
s.audio.flac_level = level.round() as u8;
}
ui.label(egui::RichText::new(tl!("0 fastest, 8 smallest")).size(12.0));
@@ -730,8 +731,8 @@ fn text_overlay(ui: &mut egui::Ui, reg: &mut Reg, key: &str, o: &mut TextOverlay
row(ui, t, tl!("Position"), |ui| placement_combo(ui, reg, &format!("export.effects.{key}.position"), &mut o.placement));
row(ui, t, tl!("Size / Opacity"), |ui| {
let (mut sz, mut op) = (o.size_percent as f64, o.opacity as f64);
drag(ui, reg, &format!("export.effects.{key}.size"), &mut sz, 1.0..=50.0, 0.2, " %", 1);
drag(ui, reg, &format!("export.effects.{key}.opacity"), &mut op, 0.0..=100.0, 1.0, " %", 0);
drag(ui, reg, &format!("export.effects.{key}.size"), &mut sz, DragOptions { range: 1.0..=50.0, speed: 0.2, suffix: " %", decimals: 1 });
drag(ui, reg, &format!("export.effects.{key}.opacity"), &mut op, DragOptions { range: 0.0..=100.0, speed: 1.0, suffix: " %", decimals: 0 });
(o.size_percent, o.opacity) = (sz as f32, op as f32);
});
}
@@ -755,8 +756,8 @@ fn effects_section(ui: &mut egui::Ui, reg: &mut Reg, s: &mut ExportSettings, t:
row(ui, t, tl!("Position"), |ui| placement_combo(ui, reg, "export.effects.imageOverlay.position", &mut o.placement));
row(ui, t, tl!("Size / Opacity"), |ui| {
let (mut sz, mut op) = (o.size_percent as f64, o.opacity as f64);
drag(ui, reg, "export.effects.imageOverlay.size", &mut sz, 1.0..=100.0, 0.5, " %", 1);
drag(ui, reg, "export.effects.imageOverlay.opacity", &mut op, 0.0..=100.0, 1.0, " %", 0);
drag(ui, reg, "export.effects.imageOverlay.size", &mut sz, DragOptions { range: 1.0..=100.0, speed: 0.5, suffix: " %", decimals: 1 });
drag(ui, reg, "export.effects.imageOverlay.opacity", &mut op, DragOptions { range: 0.0..=100.0, speed: 1.0, suffix: " %", decimals: 0 });
(o.size_percent, o.opacity) = (sz as f32, op as f32);
});
}
@@ -772,8 +773,8 @@ fn effects_section(ui: &mut egui::Ui, reg: &mut Reg, s: &mut ExportSettings, t:
if fx.video_limiter.enabled {
row(ui, t, tl!("Luma Min / Max"), |ui| {
let (mut lo, mut hi) = (fx.video_limiter.min_percent as f64, fx.video_limiter.max_percent as f64);
drag(ui, reg, "export.effects.videoLimiter.min", &mut lo, 0.0..=50.0, 0.5, " %", 1);
drag(ui, reg, "export.effects.videoLimiter.max", &mut hi, 50.0..=100.0, 0.5, " %", 1);
drag(ui, reg, "export.effects.videoLimiter.min", &mut lo, DragOptions { range: 0.0..=50.0, speed: 0.5, suffix: " %", decimals: 1 });
drag(ui, reg, "export.effects.videoLimiter.max", &mut hi, DragOptions { range: 50.0..=100.0, speed: 0.5, suffix: " %", decimals: 1 });
(fx.video_limiter.min_percent, fx.video_limiter.max_percent) = (lo as f32, hi as f32);
});
}
@@ -782,10 +783,22 @@ fn effects_section(ui: &mut egui::Ui, reg: &mut Reg, s: &mut ExportSettings, t:
check(ui, reg, "export.effects.loudness", &mut fx.loudness.enabled, tl!("Loudness Normalization"));
if fx.loudness.enabled {
row(ui, t, tl!("Target Loudness"), |ui| {
drag(ui, reg, "export.effects.loudness.target", &mut fx.loudness.target_lufs, -40.0..=-5.0, 0.1, " LUFS", 1);
drag(
ui,
reg,
"export.effects.loudness.target",
&mut fx.loudness.target_lufs,
DragOptions { range: -40.0..=-5.0, speed: 0.1, suffix: " LUFS", decimals: 1 },
);
});
row(ui, t, tl!("True Peak Limit"), |ui| {
drag(ui, reg, "export.effects.loudness.truePeak", &mut fx.loudness.true_peak_dbtp, -9.0..=0.0, 0.1, " dBTP", 1);
drag(
ui,
reg,
"export.effects.loudness.truePeak",
&mut fx.loudness.true_peak_dbtp,
DragOptions { range: -9.0..=0.0, speed: 0.1, suffix: " dBTP", decimals: 1 },
);
});
}
}
+3 -1
View File
@@ -1,4 +1,6 @@
use super::*;
use super::{
Affine, ClipId, FilmcraftApp, ItemKind, LayerView, Pos2, Rect, Vec2, eval_layer, layer_indices, layer_local_bounds, layer_matrix, pos2, selected_graphic,
};
pub(super) fn sp(a: &Affine, x: f32, y: f32) -> Pos2 {
let v = a.apply(Vec2::new(x as f64, y as f64));
@@ -1,6 +1,6 @@
use super::canvas::{anchor_screen, screen_scale, sp, unscale};
use super::text_edit::text_of;
use super::*;
use super::{ClipId, LayerContent, LayerView, Pos2, Value, json};
#[derive(Clone, Copy, Debug)]
pub(super) enum DragKind {
+1 -1
View File
@@ -1,7 +1,7 @@
use super::layer_identity::{current_layer_fingerprint, current_layer_layout_fingerprint, current_layer_text, current_layer_uid, layer_fingerprint};
use super::overlay::TEXT_EDIT_ID;
use super::text_edit::{char_boundary_floor, text_of};
use super::*;
use super::{ClipId, Color32, FilmcraftApp, GfxEdit, ImeTarget, LayerView, Pos2, Rect, Session, Stroke, layer_indices, pos2, vec2};
/// The Program monitor editor drawn in one pass, remembered so the end-of-pass IME check can run
/// after that pass's control requests have been drained. It holds only geometry, never a copy of
@@ -1,4 +1,4 @@
use super::*;
use super::{ClipId, EffectInstance, FilmcraftApp, LayerView, ParamValue, Session, Tick, graphic, layer_display_name, layer_indices};
pub(super) fn layer_display_name_view(app: &FilmcraftApp, v: &LayerView) -> String {
app.session
@@ -5,7 +5,10 @@ use super::handles::{
use super::ime::{current_graphics_ime_target, draw_preedit, graphics_ime_target, note_graphics_ime_editor, reconcile_gfx_edit};
use super::layer_identity::{current_layer_text, current_layer_uid, layer_display_name_view};
use super::text_edit::{char_boundary_floor, text_edit, text_of};
use super::*;
use super::{
Affine, ClipId, Color32, FilmcraftApp, GfxEdit, ImeTarget, ItemKind, LayerView, Pos2, Rect, Sense, Stroke, StrokeKind, Tool, Value, Vec2, eval_layer, json,
layer_indices, pos2, selected_graphic, text_layout, vec2,
};
/// `graphics.newText` for the Type tool: point text at `at`, or paragraph text in the box from
/// `at` to `corner`.
@@ -1,6 +1,6 @@
use super::ime::{graphics_ime_target, ime_event_is_stale, local_reset_destination};
use super::overlay::{TEXT_EDIT_ID, end_edit};
use super::*;
use super::{FilmcraftApp, GfxEdit, ImeTarget, LayerContent, LayerSpec, LayerView, Rect, Sense, json, text_layout};
pub(super) fn text_of(spec: &LayerSpec) -> Option<&filmcraft_project::graphic::TextProps> {
match &spec.content {
+2 -2
View File
@@ -620,7 +620,7 @@ fn preview_item(
id: ItemId(id),
name: e.name.clone(),
label: Label::Iris,
kind: ItemKind::Media(MediaClip {
kind: ItemKind::Media(Box::new(MediaClip {
media: MediaRef::File { path: e.path.clone() },
info: info.clone(),
interpret: Default::default(),
@@ -630,7 +630,7 @@ fn preview_item(
offline: false,
proxy: None,
identity: None,
}),
})),
metadata: Default::default(),
created: 0,
split: Default::default(),
+15 -8
View File
@@ -390,11 +390,16 @@ fn text(ui: &mut egui::Ui, elems: &mut Elems, pre: &str, p: &mut Value, key: &st
});
}
fn number(ui: &mut egui::Ui, elems: &mut Elems, pre: &str, p: &mut Value, key: &str, label: &str, range: std::ops::RangeInclusive<f64>, suffix: &str) {
struct NumberOptions {
range: std::ops::RangeInclusive<f64>,
suffix: &'static str,
}
fn number(ui: &mut egui::Ui, elems: &mut Elems, pre: &str, p: &mut Value, key: &str, label: &str, options: NumberOptions) {
ui.horizontal(|ui| {
ui.label(label);
let mut v = p.get(key).and_then(Value::as_f64).unwrap_or(0.0);
let r = ui.add(crate::widgets::number_drag(&mut v).range(range).suffix(suffix));
let r = ui.add(crate::widgets::number_drag(&mut v).range(options.range).suffix(options.suffix));
push(elems, format!("{pre}.{key}"), &r, label);
if r.changed() {
p[key] = json!(v);
@@ -706,8 +711,8 @@ pub fn show(app: &mut FilmcraftApp, ctx: &egui::Context) {
&[("sequentially", tl!("Sequentially")), ("unnumberedMarkers", tl!("At Unnumbered Markers"))],
);
radios(ui, &mut elems, pre, p, "method", tl!("Method:"), &[("insert", tl!("Insert Edit")), ("overwrite", tl!("Overwrite Edit"))]);
number(ui, &mut elems, pre, p, "overlapFrames", tl!("Clip Overlap:"), 0.0..=600.0, " frames");
number(ui, &mut elems, pre, p, "stillFrames", tl!("Frames per Still:"), 1.0..=100_000.0, " frames");
number(ui, &mut elems, pre, p, "overlapFrames", tl!("Clip Overlap:"), NumberOptions { range: 0.0..=600.0, suffix: " frames" });
number(ui, &mut elems, pre, p, "stillFrames", tl!("Frames per Still:"), NumberOptions { range: 1.0..=100_000.0, suffix: " frames" });
ui.label(RichText::new(tl!("Transitions")).strong());
check(ui, &mut elems, pre, p, "audioTransition", tl!("Apply Default Audio Transition"));
check(ui, &mut elems, pre, p, "videoTransition", tl!("Apply Default Video Transition"));
@@ -727,7 +732,9 @@ pub fn show(app: &mut FilmcraftApp, ctx: &egui::Context) {
p["sensitivity"] = json!(v);
});
}
"sequence.normalizeMixTrack" => number(ui, &mut elems, pre, p, "db", tl!("Normalize Mix Track to:"), -96.0..=24.0, " dB"),
"sequence.normalizeMixTrack" => {
number(ui, &mut elems, pre, p, "db", tl!("Normalize Mix Track to:"), NumberOptions { range: -96.0..=24.0, suffix: " dB" })
}
"sequence.simplify" => {
text(ui, &mut elems, pre, p, "name", tl!("Sequence Name:"), 240.0);
ui.label(RichText::new(tl!("Remove")).strong());
@@ -769,9 +776,9 @@ pub fn show(app: &mut FilmcraftApp, ctx: &egui::Context) {
"file.newColorMatte" => {
color(ui, &mut elems, pre, p, "color", tl!("Color:"));
text(ui, &mut elems, pre, p, "name", tl!("Name:"), 220.0);
number(ui, &mut elems, pre, p, "width", tl!("Width:"), 1.0..=16384.0, " px");
number(ui, &mut elems, pre, p, "height", tl!("Height:"), 1.0..=16384.0, " px");
number(ui, &mut elems, pre, p, "seconds", tl!("Duration:"), 0.04..=36000.0, " s");
number(ui, &mut elems, pre, p, "width", tl!("Width:"), NumberOptions { range: 1.0..=16384.0, suffix: " px" });
number(ui, &mut elems, pre, p, "height", tl!("Height:"), NumberOptions { range: 1.0..=16384.0, suffix: " px" });
number(ui, &mut elems, pre, p, "seconds", tl!("Duration:"), NumberOptions { range: 0.04..=36000.0, suffix: " s" });
}
"project.matteColor" => color(ui, &mut elems, pre, p, "color", tl!("Color:")),
"markers.addFlashCue" => {
+24 -14
View File
@@ -185,17 +185,17 @@ fn click_selection(current: &[ItemId], order: &[ItemId], anchor: Option<ItemId>,
}
/// Click / double-click / drag / context menu of an item (row, card).
pub fn item_interactions(
app: &mut FilmcraftApp,
ui: &egui::Ui,
v: &View,
resp: &egui::Response,
id: ItemId,
kind: &ItemKind,
actions: &mut Actions,
drag: bool,
menu: bool,
) {
pub(super) struct ItemInteraction<'a> {
view: &'a View,
response: &'a egui::Response,
item: ItemId,
kind: &'a ItemKind,
allow_drag: bool,
allow_menu: bool,
}
pub(super) fn item_interactions(app: &mut FilmcraftApp, ui: &egui::Ui, actions: &mut Actions, interaction: ItemInteraction<'_>) {
let ItemInteraction { view: v, response: resp, item: id, kind, allow_drag: drag, allow_menu: menu } = interaction;
if resp.clicked() {
let mods = click_modifiers(ui);
let order = if mods.shift { items_in_view(app, v, &app.ui.project_search.to_ascii_lowercase()) } else { Vec::new() };
@@ -785,7 +785,7 @@ fn list_bin(app: &mut FilmcraftApp, ui: &mut egui::Ui, bin: &Bin, depth: usize,
}
app.auto.add(&format!("{}.item.{}", v.prefix, id.0), r.intersect(ui.clip_rect()), &it.name);
slow_click_rename(app, ui, &resp, name_r, id, selected);
item_interactions(app, ui, v, &resp, id, &it.kind, actions, true, true);
item_interactions(app, ui, actions, ItemInteraction { view: v, response: &resp, item: id, kind: &it.kind, allow_drag: true, allow_menu: true });
}
}
@@ -988,7 +988,12 @@ pub fn icon_view(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect, v: &View
note_hit(ui, v, id, r);
let name_r = Rect::from_min_max(pos2(r.min.x, r.min.y + th_h), r.max);
slow_click_rename(app, ui, &resp, name_r, id, was_selected);
item_interactions(app, ui, v, &resp, id, &kind, actions, true, true);
item_interactions(
app,
ui,
actions,
ItemInteraction { view: v, response: &resp, item: id, kind: &kind, allow_drag: true, allow_menu: true },
);
}
}
}
@@ -1058,7 +1063,12 @@ pub fn freeform_view(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect, v: &
ui.painter().text(br.center(), Align2::CENTER_CENTER, format!("×{n}"), Tokens::ui(10.0), Color32::WHITE);
app.auto.add(&format!("{}.stack.{s}", v.prefix), br, &format!("Stack of {n}"));
}
item_interactions(app, ui, v, &resp, ItemId(c.item), &kind, actions, false, false);
item_interactions(
app,
ui,
actions,
ItemInteraction { view: v, response: &resp, item: ItemId(c.item), kind: &kind, allow_drag: false, allow_menu: false },
);
// Escape puts the cards back and drops nothing until the button comes up (#580); egui
// also ends the drag on Escape, so it arrives as a stop as often as during the drag
let escape = ui.input(|i| i.key_pressed(egui::Key::Escape));
+4 -1
View File
@@ -1,6 +1,9 @@
use super::layout::Layout;
use super::waveforms::draw_waveform;
use super::*;
use super::{
Align2, Arc, ClipId, Color32, DuplicateFrames, FilmcraftApp, FrameRate, HashMap, ItemId, Pos2, Rect, Sequence, Stroke, StrokeKind, ThumbnailMode, Tick,
TimeRange, Tokens, TrackItem, TrackKind, duplicate_frames, label_color, pos2, vec2,
};
/// (session revision, sequence, project address)
pub(super) type DuplicatesKey = (u64, ItemId, usize);
+44 -4
View File
@@ -1,8 +1,22 @@
use super::layout::Row;
use super::*;
use super::{
Align2, Color32, CursorIcon, FilmcraftApp, Icon, MASTER_H, Rect, Sense, Sequence, Stroke, StrokeKind, TRACK_HEADER_TINT, Tokens, TrackKind, Value, json,
label_color, pos2, vec2,
};
/// A Premiere track-header button (patch / target): highlight-blue fill when on, full track height.
fn patch_button(ui: &mut egui::Ui, r: Rect, clip: Rect, label: &str, on: bool, show_off: bool, id: egui::Id, t: &Tokens) -> egui::Response {
struct PatchButton<'a> {
rect: Rect,
clip: Rect,
label: &'a str,
active: bool,
show_inactive: bool,
id: egui::Id,
tokens: &'a Tokens,
}
fn patch_button(ui: &mut egui::Ui, button: PatchButton<'_>) -> egui::Response {
let PatchButton { rect: r, clip, label, active: on, show_inactive: show_off, id, tokens: t } = button;
let resp = ui.interact(r.intersect(clip), id, Sense::click());
let p = ui.painter().with_clip_rect(clip);
if on {
@@ -76,7 +90,20 @@ pub(super) fn draw_headers(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequ
let patched = if r.kind == TrackKind::Video { tg.video_dest == Some(r.track) } else { tg.audio_dest == Some(r.track) };
let pr = btn_rect(13.0);
app.auto.add(&format!("timeline.track.{label}.sourcePatch"), pr, "Source patch");
if patch_button(ui, pr, visible, &label, patched, false, egui::Id::unique(("patch", r.track.0)), t).clicked() {
if patch_button(
ui,
PatchButton {
rect: pr,
clip: visible,
label: &label,
active: patched,
show_inactive: false,
id: egui::Id::unique(("patch", r.track.0)),
tokens: t,
},
)
.clicked()
{
actions.push(("timeline.setTargeting".into(), json!({"track": r.track.0, "sourcePatch": !patched})));
}
// 2. track lock
@@ -104,7 +131,20 @@ pub(super) fn draw_headers(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequ
let targeted = tg.targeted.contains(&r.track);
let trr = btn_rect(61.0);
app.auto.add(&format!("timeline.track.{label}.target"), trr, "Toggle track targeting");
if patch_button(ui, trr, visible, &label, targeted, true, egui::Id::unique(("target", r.track.0)), t).clicked() {
if patch_button(
ui,
PatchButton {
rect: trr,
clip: visible,
label: &label,
active: targeted,
show_inactive: true,
id: egui::Id::unique(("target", r.track.0)),
tokens: t,
},
)
.clicked()
{
actions.push(("timeline.setTargeting".into(), json!({"track": r.track.0, "targeted": !targeted})));
}
// 4. upper line icons
@@ -2,7 +2,11 @@ use super::clips::paint_hatch;
use super::layout::{Layout, max_scroll, new_track_at};
use super::menus::{CLIP_MENU, EditPointTarget, edit_point_menu, gap_menu, marker_menu, multicam_menu};
use super::snap::{snap, snap_playhead};
use super::*;
use super::{
Align2, ClipId, Color32, CursorIcon, Drag, EdgeKind, FilmcraftApp, Grab, HashMap, Hit, MarkerId, Pos2, Pressed, Rect, Sense, Sequence, Stroke, StrokeKind,
Tick, Tokens, Tool, TrackId, TrackItem, TrackKind, Value, edge_geometry, grab_at, hit, json, marker_at, marker_under_pointer, pos2, selection_trim_kind,
vec2,
};
pub(super) fn preview_drag(
app: &FilmcraftApp,
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{DIVIDER_H, FilmcraftApp, MASTER_H, Rect, Sequence, Tick, TimelineView, TrackId, TrackKind, pos2};
/// Geometry of one visible track row.
#[derive(Clone, Debug)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{ClipId, FilmcraftApp, MarkerId, Sequence, TrackId, TrackItem, Value, json};
/// The clip context menu: groups (separated by rules) of (label, command id). Entries marked `…`
/// open their dialog through `menus::invoke`, like the same item in the Clip menu.
+4 -1
View File
@@ -1,5 +1,8 @@
use super::layout::Layout;
use super::*;
use super::{
Align2, Color32, Drag, EDGE_PX, FilmcraftApp, Icon, ItemId, Rect, Sense, Sequence, Stroke, ThumbnailMode, Tick, TimeDisplay, TimeRange, Tokens, Value,
format_time, icons, json, marker_range_rect, marker_rect, pos2, vec2, work_area_rect,
};
/// The Ruler's two strides, in frames: `(tick, label)`.
///
@@ -1,5 +1,5 @@
use super::layout::timeline_extent;
use super::*;
use super::{Drag, FilmcraftApp, Rect, Sense, Stroke, Tokens, pos2, vec2};
/// The narrowest the zoom scroll bar's thumb gets: room for its two handles and a part between
/// them to grab.
+1 -1
View File
@@ -1,5 +1,5 @@
use super::layout::Layout;
use super::*;
use super::{ClipId, FilmcraftApp, Grab, Hit, Pos2, SNAP_PX, Sequence, Tick, Value, grab_at, json};
/// Snap `t` to nearby candidates (edits, playhead, markers, in/out). Returns snapped tick.
pub(in super::super) fn snap(app: &mut FilmcraftApp, seq: &Sequence, layout: &Layout, t: Tick, exclude: &[ClipId]) -> Tick {
@@ -1,4 +1,4 @@
use super::*;
use super::{Align2, Arc, Color32, FilmcraftApp, ItemId, Rect, Tick, Tokens, TrackItem, pos2, vec2};
pub(super) fn draw_waveform(app: &mut FilmcraftApp, p: &egui::Painter, body: Rect, it: &TrackItem, col: Color32) {
let Some(peaks) = request_peaks(app, it.item, it.audio_stream) else { return };
+4 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{HashMap, Mutex};
/// `--control-input-only`: drop the window system's input. Screenshot results arrive through
/// `ScreenshotCallback`, not raw input; all viewport lifecycle and geometry data stay.
@@ -149,8 +149,11 @@ mod mac_ctrl_click_tests {
#[cfg(test)]
mod native_window_input_tests {
use super::*;
use serde_json::json;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::{FilmcraftApp, PanelKind};
use craft_ui::docking::WindowFiles;
use egui_kittest::Harness;
use filmcraft_engine::Session;