mirror of
https://github.com/storytold/filmcraft.git
synced 2026-11-02 18:09:21 +00:00
Refactor ui-egui for strict Clippy checks
This commit is contained in:
@@ -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")]
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{ControlRequest, FilmcraftApp, capture, json};
|
||||
use crate::control;
|
||||
|
||||
impl FilmcraftApp {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{EXPORT_SAVE_DIALOGS, FileDialog, FilmcraftApp, Value, json, panels};
|
||||
|
||||
impl FilmcraftApp {
|
||||
// ---------------------------------------------------------------- files
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{FilmcraftApp, Tick, Tokens, crash, header, i18n, panels, state, updates_ui};
|
||||
|
||||
impl FilmcraftApp {
|
||||
// ---------------------------------------------------------------- frame
|
||||
|
||||
@@ -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,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,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,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
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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,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.
|
||||
|
||||
@@ -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,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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user