clippy: clear the workspace errors and apply the machine fixes
- Mark the raw-pointer interop entry points unsafe with # Safety docs (oak-core upload/download/frame-from-pixels, oak-audio convert) and satisfy the existing callers (tests). - mut_from_ref: allow with the ABI contract documented (the handle get_mut helpers in oak-timeline/oak-render/oak-task take the shared reference the C ABI passes; exclusivity is the caller's unsafe contract). - Fix the eq_op in the white-balance normalization (green / green). - Apply cargo clippy --fix across the workspace (redundant closures and field names, field reassignment, items after test modules, ...). - Revert the replace_box fix in image_effect's clip_define: a redefinition must allocate a new box, otherwise the old clip handle stays valid and the HS-map replace contract (clip != clip2) breaks. - 283 warnings remain; they are all non-machine-applicable (chunks_exact -> as_chunks needs a manual iter_mut, too_many_arguments, complex types, missing Safety docs, ...) and are tracked as the follow-up.
This commit is contained in:
@@ -5653,7 +5653,7 @@ mod tests {
|
||||
/// the mock engine's playback ticks with executor time, so a clock
|
||||
/// advance would move the playhead out from under the assertions.
|
||||
fn settle_key(cx: &mut TestAppContext, window: AnyWindowHandle, key: &str) {
|
||||
cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse(key).unwrap());
|
||||
cx.dispatch_keystroke(window, gpui::Keystroke::parse(key).unwrap());
|
||||
cx.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
+167
-179
@@ -36,23 +36,23 @@ use gpui::colors::DefaultColors;
|
||||
use gpui::prelude::*;
|
||||
use gpui::timeline::FrameRate;
|
||||
use gpui::{
|
||||
div, px, App, ClickEvent, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, Keystroke, PathPromptOptions, Render, SharedString, Window,
|
||||
div, px, App, ClickEvent, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, Keystroke, PathPromptOptions, Render, SharedString, Window,
|
||||
};
|
||||
use gpui_elements::editable_text::{EditableTextState, StringStorage, TextChanged};
|
||||
|
||||
use crate::actions::ActionId;
|
||||
use crate::i18n;
|
||||
use crate::oakui::real::{
|
||||
audio_input_device, audio_input_devices, audio_output_device, audio_output_devices,
|
||||
config_get_bool, config_get_int, config_get_string, config_set_bool, config_set_int,
|
||||
config_set_string, encoding_formats, proxy_dividers, renderer_backends, set_audio_input_device,
|
||||
set_audio_output_device, set_theme_dark, theme_is_dark, CONFIG_KEY_DEFAULT_TRANSITION_SEC,
|
||||
CONFIG_KEY_DISK_CACHE_PATH, CONFIG_KEY_FFMPEG_PATH, CONFIG_KEY_PG_URL,
|
||||
CONFIG_KEY_PREVIEW_WINDOW, CONFIG_KEY_PROXY_DIVIDER, CONFIG_KEY_RENDERER_BACKEND,
|
||||
CONFIG_KEY_SNAPSHOT_INTERVAL_SEC, CONFIG_KEY_STORAGE_BACKEND, CONFIG_KEY_USE_PROXY,
|
||||
DEFAULT_PREVIEW_WINDOW_FORWARD, DEFAULT_SNAPSHOT_INTERVAL_SEC, DEFAULT_TRANSITION_SEC,
|
||||
EXPORT_FORMAT_MP4,
|
||||
audio_input_device, audio_input_devices, audio_output_device, audio_output_devices,
|
||||
config_get_bool, config_get_int, config_get_string, config_set_bool, config_set_int,
|
||||
config_set_string, encoding_formats, proxy_dividers, renderer_backends, set_audio_input_device,
|
||||
set_audio_output_device, set_theme_dark, theme_is_dark, CONFIG_KEY_DEFAULT_TRANSITION_SEC,
|
||||
CONFIG_KEY_DISK_CACHE_PATH, CONFIG_KEY_FFMPEG_PATH, CONFIG_KEY_PG_URL,
|
||||
CONFIG_KEY_PREVIEW_WINDOW, CONFIG_KEY_PROXY_DIVIDER, CONFIG_KEY_RENDERER_BACKEND,
|
||||
CONFIG_KEY_SNAPSHOT_INTERVAL_SEC, CONFIG_KEY_STORAGE_BACKEND, CONFIG_KEY_USE_PROXY,
|
||||
DEFAULT_PREVIEW_WINDOW_FORWARD, DEFAULT_SNAPSHOT_INTERVAL_SEC, DEFAULT_TRANSITION_SEC,
|
||||
EXPORT_FORMAT_MP4,
|
||||
};
|
||||
// The `DisplayBitDepth` config key lives with the format mapping it
|
||||
// drives (oak-render's backend); the preferences dropdown and the
|
||||
@@ -184,20 +184,22 @@ impl PreferencesContent {
|
||||
];
|
||||
ComboBox::new(12, options, window, cx)
|
||||
});
|
||||
cx.subscribe(&display_bit_depth, |_this, _combo, event: &ComboBoxEvent, cx| {
|
||||
let ComboBoxEvent::Selected { value, .. } = event;
|
||||
let depth = if *value == 1 { "8" } else { "10" };
|
||||
config_set_string(CONFIG_KEY_DISPLAY_BIT_DEPTH, depth);
|
||||
println!("[preferences] display bit depth → {depth}");
|
||||
let _ = cx;
|
||||
})
|
||||
cx.subscribe(
|
||||
&display_bit_depth,
|
||||
|_this, _combo, event: &ComboBoxEvent, cx| {
|
||||
let ComboBoxEvent::Selected { value, .. } = event;
|
||||
let depth = if *value == 1 { "8" } else { "10" };
|
||||
config_set_string(CONFIG_KEY_DISPLAY_BIT_DEPTH, depth);
|
||||
println!("[preferences] display bit depth → {depth}");
|
||||
let _ = cx;
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
let bit_depth_selected =
|
||||
if config_get_string(CONFIG_KEY_DISPLAY_BIT_DEPTH) == "8" {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let bit_depth_selected = if config_get_string(CONFIG_KEY_DISPLAY_BIT_DEPTH) == "8" {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
display_bit_depth.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(bit_depth_selected), cx)
|
||||
});
|
||||
@@ -269,11 +271,8 @@ impl PreferencesContent {
|
||||
// playhead during playback (`PlaybackPreRenderFrames`, consumed by
|
||||
// `update_preview_window` every playback tick — no restart needed).
|
||||
let cache_ahead = cx.new(|cx| {
|
||||
let current = config_get_int(
|
||||
CONFIG_KEY_PREVIEW_WINDOW,
|
||||
DEFAULT_PREVIEW_WINDOW_FORWARD,
|
||||
)
|
||||
.clamp(8, 1200);
|
||||
let current = config_get_int(CONFIG_KEY_PREVIEW_WINDOW, DEFAULT_PREVIEW_WINDOW_FORWARD)
|
||||
.clamp(8, 1200);
|
||||
SpinBox::new(
|
||||
10,
|
||||
SliderModel::new(ValueKind::Integer, 8.0, 1200.0, 8.0, current as f64),
|
||||
@@ -366,13 +365,13 @@ impl PreferencesContent {
|
||||
})
|
||||
.detach();
|
||||
|
||||
// --- 色彩 Color: display ICC color management -----------------------
|
||||
// On by default: the viewer frames are transformed through the
|
||||
// display's ICC profile (system profile, or a custom file below).
|
||||
// A mode change re-evaluates the platform display policy and
|
||||
// retags the windows immediately (no restart).
|
||||
use crate::oakui::displaycolor::{CONFIG_KEY_COLOR_MODE, CONFIG_KEY_CUSTOM_ICC};
|
||||
let display_icc = cx.new(|cx| {
|
||||
// --- 色彩 Color: display ICC color management -----------------------
|
||||
// On by default: the viewer frames are transformed through the
|
||||
// display's ICC profile (system profile, or a custom file below).
|
||||
// A mode change re-evaluates the platform display policy and
|
||||
// retags the windows immediately (no restart).
|
||||
use crate::oakui::displaycolor::{CONFIG_KEY_COLOR_MODE, CONFIG_KEY_CUSTOM_ICC};
|
||||
let display_icc = cx.new(|cx| {
|
||||
let mode = config_get_string(CONFIG_KEY_COLOR_MODE);
|
||||
CheckBox::new(
|
||||
13,
|
||||
@@ -500,18 +499,21 @@ impl PreferencesContent {
|
||||
];
|
||||
ComboBox::new(11, options, window, cx)
|
||||
});
|
||||
cx.subscribe(&storage_backend, |this, _combo, event: &ComboBoxEvent, cx| {
|
||||
match *event {
|
||||
ComboBoxEvent::Selected { value } => {
|
||||
let backend = if value == 1 { "pg" } else { "sqlite" };
|
||||
this.storage_is_pg = backend == "pg";
|
||||
config_set_string(CONFIG_KEY_STORAGE_BACKEND, backend);
|
||||
// Re-render so the connection-string row shows/hides with
|
||||
// the selection.
|
||||
cx.notify();
|
||||
cx.subscribe(
|
||||
&storage_backend,
|
||||
|this, _combo, event: &ComboBoxEvent, cx| {
|
||||
match *event {
|
||||
ComboBoxEvent::Selected { value } => {
|
||||
let backend = if value == 1 { "pg" } else { "sqlite" };
|
||||
this.storage_is_pg = backend == "pg";
|
||||
config_set_string(CONFIG_KEY_STORAGE_BACKEND, backend);
|
||||
// Re-render so the connection-string row shows/hides with
|
||||
// the selection.
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
storage_backend.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(if storage_is_pg { 1 } else { 0 }), cx)
|
||||
@@ -1085,8 +1087,7 @@ impl ExportDialogContent {
|
||||
ComboBoxOption::new(0, i18n::tr("export.color.sdr")),
|
||||
ComboBoxOption::new(1, i18n::tr("export.color.hdr")),
|
||||
];
|
||||
ComboBox::new(7, options, window, cx)
|
||||
.with_placeholder(i18n::tr("export.color"))
|
||||
ComboBox::new(7, options, window, cx).with_placeholder(i18n::tr("export.color"))
|
||||
});
|
||||
color.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
|
||||
@@ -1095,8 +1096,7 @@ impl ExportDialogContent {
|
||||
ComboBoxOption::new(0, i18n::tr("export.range.all")),
|
||||
ComboBoxOption::new(1, i18n::tr("export.range.inout")),
|
||||
];
|
||||
ComboBox::new(8, options, window, cx)
|
||||
.with_placeholder(i18n::tr("export.range"))
|
||||
ComboBox::new(8, options, window, cx).with_placeholder(i18n::tr("export.range"))
|
||||
});
|
||||
range.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
|
||||
@@ -1149,8 +1149,7 @@ impl ExportDialogContent {
|
||||
// an empty list disables the dialog's OK via the picker staying
|
||||
// unselected.
|
||||
let sequence = cx.new(|cx| {
|
||||
ComboBox::new(13, Vec::new(), window, cx)
|
||||
.with_placeholder(i18n::tr("export.sequence"))
|
||||
ComboBox::new(13, Vec::new(), window, cx).with_placeholder(i18n::tr("export.sequence"))
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -1188,7 +1187,8 @@ impl ExportDialogContent {
|
||||
.enumerate()
|
||||
.map(|(i, (_, name))| ComboBoxOption::new(i, name.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
self.sequence.update(cx, |combo, cx| combo.set_options(options, cx));
|
||||
self.sequence
|
||||
.update(cx, |combo, cx| combo.set_options(options, cx));
|
||||
self.sequence
|
||||
.update(cx, |combo, cx| combo.set_selected(preselect, cx));
|
||||
self.sequences = sequences;
|
||||
@@ -1386,10 +1386,7 @@ impl Render for ExportDialogContent {
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(self.resolution_w.clone())
|
||||
.child(div()
|
||||
.text_color(colors.disabled)
|
||||
.text_xs()
|
||||
.child("×"))
|
||||
.child(div().text_color(colors.disabled).text_xs().child("×"))
|
||||
.child(self.resolution_h.clone()),
|
||||
))
|
||||
.child(form_row(
|
||||
@@ -1639,7 +1636,11 @@ impl<E: crate::oakui::engine::AppEngine> ProxyDialogContent<E> {
|
||||
config_set_bool("ProxyIncludeAudio", params.include_audio);
|
||||
config_set_int(
|
||||
"ProxyMaxConcurrent",
|
||||
self.max_concurrent.read(cx).value().to_f64().clamp(1.0, 16.0) as i64,
|
||||
self.max_concurrent
|
||||
.read(cx)
|
||||
.value()
|
||||
.to_f64()
|
||||
.clamp(1.0, 16.0) as i64,
|
||||
);
|
||||
config_set_string(
|
||||
CONFIG_KEY_FFMPEG_PATH,
|
||||
@@ -1727,8 +1728,8 @@ fn divider_label(divider: i32) -> String {
|
||||
|
||||
/// The display string of a proxy lifecycle state.
|
||||
fn proxy_state_label(state: crate::oakui::engine::ProxyMediaState) -> String {
|
||||
use crate::oakui::engine::ProxyMediaState;
|
||||
match state {
|
||||
use crate::oakui::engine::ProxyMediaState;
|
||||
match state {
|
||||
ProxyMediaState::Missing => i18n::tr("proxydialog.state.missing"),
|
||||
ProxyMediaState::Generating => i18n::tr("proxydialog.state.generating"),
|
||||
ProxyMediaState::Ready => i18n::tr("proxydialog.state.ready"),
|
||||
@@ -1747,7 +1748,7 @@ impl<E: crate::oakui::engine::AppEngine> Render for ProxyDialogContent<E> {
|
||||
.map(|row| {
|
||||
let mut state = proxy_state_label(row.state);
|
||||
if row.has_custom {
|
||||
state.push_str(&i18n::tr("proxydialog.custom_suffix"));
|
||||
state.push_str(i18n::tr("proxydialog.custom_suffix"));
|
||||
}
|
||||
let enabled =
|
||||
row.enabled && row.state == crate::oakui::engine::ProxyMediaState::Ready;
|
||||
@@ -1960,20 +1961,20 @@ impl<E: crate::oakui::engine::AppEngine> ProjectPropertiesContent<E> {
|
||||
let (working, gamut, transfer) = engine.read(cx).project_color_settings();
|
||||
working_space.update(cx, |combo, cx| {
|
||||
combo.set_selected(
|
||||
Some(oak_core::colormath::WorkingColorSpace::from_setting(&working) as usize),
|
||||
cx,
|
||||
Some(oak_core::colormath::WorkingColorSpace::from_setting(&working) as usize),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
output_gamut.update(cx, |combo, cx| {
|
||||
combo.set_selected(
|
||||
Some(oak_core::colormath::OutputGamut::from_setting(&gamut) as usize),
|
||||
cx,
|
||||
Some(oak_core::colormath::OutputGamut::from_setting(&gamut) as usize),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
output_transfer.update(cx, |combo, cx| {
|
||||
combo.set_selected(
|
||||
Some(oak_core::colormath::OutputTransfer::from_setting(&transfer) as usize),
|
||||
cx,
|
||||
Some(oak_core::colormath::OutputTransfer::from_setting(&transfer) as usize),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -2057,8 +2058,8 @@ impl<E: crate::oakui::engine::AppEngine> ProjectPropertiesContent<E> {
|
||||
/// The color pipeline settings currently selected in the combos, as
|
||||
/// the canonical persisted strings.
|
||||
fn color_settings(&self, cx: &App) -> (String, String, String) {
|
||||
use oak_core::colormath::{OutputGamut, OutputTransfer, WorkingColorSpace};
|
||||
let working = self
|
||||
use oak_core::colormath::{OutputGamut, OutputTransfer, WorkingColorSpace};
|
||||
let working = self
|
||||
.working_space
|
||||
.read(cx)
|
||||
.selected()
|
||||
@@ -2139,7 +2140,7 @@ impl<E: crate::oakui::engine::AppEngine> Render for ProjectPropertiesContent<E>
|
||||
directories: false,
|
||||
multiple: false,
|
||||
prompt: None,
|
||||
allowed_extensions: Vec::new(),
|
||||
allowed_extensions: Vec::new(),
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Ok(Ok(Some(paths))) = receiver.await {
|
||||
@@ -2742,7 +2743,7 @@ impl Render for KeyboardTabContent {
|
||||
.child(
|
||||
div()
|
||||
.id(ElementId::named_usize("keyboard-shortcut-capture", index))
|
||||
.debug_selector(move || format!("keyboard-capture-{index}").into())
|
||||
.debug_selector(move || format!("keyboard-capture-{index}"))
|
||||
.min_w(px(150.0))
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
@@ -3348,7 +3349,11 @@ impl Render for TextValue {
|
||||
.bg(colors.background)
|
||||
.px_2()
|
||||
.py_1()
|
||||
.child(text_input("oak-seq-name", cx).state(weak).accepts_input(true))
|
||||
.child(
|
||||
text_input("oak-seq-name", cx)
|
||||
.state(weak)
|
||||
.accepts_input(true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3388,12 +3393,13 @@ impl SequenceFormatSeed {
|
||||
pub fn from_format(format: &crate::oakui::engine::VideoFormat, interlaced: bool) -> Self {
|
||||
let preset = (1..=SEQUENCE_PRESET_COUNT)
|
||||
.find(|i| {
|
||||
sequence_preset_format(*i) == Some((
|
||||
format.width,
|
||||
format.height,
|
||||
format.rate.num,
|
||||
format.rate.den,
|
||||
))
|
||||
sequence_preset_format(*i)
|
||||
== Some((
|
||||
format.width,
|
||||
format.height,
|
||||
format.rate.num,
|
||||
format.rate.den,
|
||||
))
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let rate = SEQUENCE_RATES
|
||||
@@ -3424,26 +3430,14 @@ pub struct SequenceFormatFields {
|
||||
|
||||
impl SequenceFormatFields {
|
||||
/// Builds the fields and wires the preset / field cross-updates.
|
||||
pub fn build(
|
||||
seed: SequenceFormatSeed,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
pub fn build(seed: SequenceFormatSeed, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let preset = cx.new(|cx| ComboBox::new(40, sequence_preset_options(), window, cx));
|
||||
preset.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(seed.preset), cx)
|
||||
});
|
||||
preset.update(cx, |combo, cx| combo.set_selected(Some(seed.preset), cx));
|
||||
|
||||
let width = cx.new(|cx| {
|
||||
SpinBox::new(
|
||||
41,
|
||||
SliderModel::new(
|
||||
ValueKind::Integer,
|
||||
16.0,
|
||||
8192.0,
|
||||
2.0,
|
||||
f64::from(seed.width),
|
||||
),
|
||||
SliderModel::new(ValueKind::Integer, 16.0, 8192.0, 2.0, f64::from(seed.width)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -3504,27 +3498,24 @@ impl SequenceFormatFields {
|
||||
// Editing a dimension or the frame rate reverts to the custom entry.
|
||||
cx.subscribe(&width, |this, _spin, event: &SpinBoxEvent, cx| {
|
||||
if let SpinBoxEvent::ValueChanged { .. } = event {
|
||||
this.preset.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(0), cx)
|
||||
});
|
||||
this.preset
|
||||
.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
cx.subscribe(&height, |this, _spin, event: &SpinBoxEvent, cx| {
|
||||
if let SpinBoxEvent::ValueChanged { .. } = event {
|
||||
this.preset.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(0), cx)
|
||||
});
|
||||
this.preset
|
||||
.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
cx.subscribe(&rate, |this, _combo, event: &ComboBoxEvent, cx| {
|
||||
let ComboBoxEvent::Selected { .. } = event;
|
||||
this.preset.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(0), cx)
|
||||
});
|
||||
this.preset
|
||||
.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
@@ -3709,17 +3700,15 @@ impl<E: crate::oakui::engine::AppEngine> Render for NewSequenceContent<E> {
|
||||
self.name.clone(),
|
||||
))
|
||||
.child(format_rows)
|
||||
.child(
|
||||
if let Some(error) = &self.error {
|
||||
div()
|
||||
.debug_selector(|| "seqprops-error".into())
|
||||
.text_color(gpui::rgb(0xe5484d))
|
||||
.text_xs()
|
||||
.child(error.clone())
|
||||
} else {
|
||||
div()
|
||||
},
|
||||
)
|
||||
.child(if let Some(error) = &self.error {
|
||||
div()
|
||||
.debug_selector(|| "seqprops-error".into())
|
||||
.text_color(gpui::rgb(0xe5484d))
|
||||
.text_xs()
|
||||
.child(error.clone())
|
||||
} else {
|
||||
div()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,13 +3807,7 @@ impl<E: crate::oakui::engine::AppEngine> SequencePropertiesContent<E> {
|
||||
let format = self.format(cx);
|
||||
let interlaced = self.interlaced(cx);
|
||||
self.engine.update(cx, |engine, cx| {
|
||||
engine.update_sequence_parameters(
|
||||
self.sequence_id,
|
||||
name,
|
||||
format,
|
||||
interlaced,
|
||||
cx,
|
||||
)
|
||||
engine.update_sequence_parameters(self.sequence_id, name, format, interlaced, cx)
|
||||
})?;
|
||||
self.set_error(None, cx);
|
||||
Ok(())
|
||||
@@ -3858,17 +3841,15 @@ impl<E: crate::oakui::engine::AppEngine> Render for SequencePropertiesContent<E>
|
||||
self.name.clone(),
|
||||
))
|
||||
.child(format_rows)
|
||||
.child(
|
||||
if let Some(error) = &self.error {
|
||||
div()
|
||||
.debug_selector(|| "seqprops-error".into())
|
||||
.text_color(gpui::rgb(0xe5484d))
|
||||
.text_xs()
|
||||
.child(error.clone())
|
||||
} else {
|
||||
div()
|
||||
},
|
||||
)
|
||||
.child(if let Some(error) = &self.error {
|
||||
div()
|
||||
.debug_selector(|| "seqprops-error".into())
|
||||
.text_color(gpui::rgb(0xe5484d))
|
||||
.text_xs()
|
||||
.child(error.clone())
|
||||
} else {
|
||||
div()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3970,11 +3951,7 @@ const WIZARD_SYNC_MODES: &[&str] = &[
|
||||
|
||||
impl<E: crate::oakui::engine::AppEngine> MulticamWizardContent<E> {
|
||||
/// Builds the content seeded with the engine's wizard footage.
|
||||
pub fn new(
|
||||
engine: Entity<E>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
pub fn new(engine: Entity<E>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let name = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
TextValue { editor }
|
||||
@@ -4061,18 +4038,22 @@ impl<E: crate::oakui::engine::AppEngine> Render for MulticamWizardContent<E> {
|
||||
.rounded_sm()
|
||||
.hover(|style| style.bg(colors.container))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(
|
||||
move |this: &mut Self, _event: &ClickEvent, _window, cx| {
|
||||
.on_click(
|
||||
cx.listener(move |this: &mut Self, _event: &ClickEvent, _window, cx| {
|
||||
this.toggle_row(index, cx);
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size(px(12.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(if is_checked { colors.selected } else { colors.background })
|
||||
.bg(if is_checked {
|
||||
colors.selected
|
||||
} else {
|
||||
colors.background
|
||||
})
|
||||
.text_color(colors.text)
|
||||
.child(if is_checked { "✓" } else { "" }),
|
||||
)
|
||||
@@ -4083,10 +4064,12 @@ impl<E: crate::oakui::engine::AppEngine> Render for MulticamWizardContent<E> {
|
||||
.child(entry.name.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.disabled)
|
||||
.child(entry.duration_s.map(|d| format!("{d:.1}s")).unwrap_or_default()),
|
||||
div().text_xs().text_color(colors.disabled).child(
|
||||
entry
|
||||
.duration_s
|
||||
.map(|d| format!("{d:.1}s"))
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
);
|
||||
rows_div = rows_div.child(row);
|
||||
}
|
||||
@@ -4157,16 +4140,11 @@ impl RenameContent {
|
||||
impl Render for RenameContent {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.context.rename").into(),
|
||||
self.field.clone(),
|
||||
))
|
||||
div().flex().flex_col().gap_3().w_full().child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.context.rename").into(),
|
||||
self.field.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4193,7 +4171,10 @@ impl ExportProjectDialogContent {
|
||||
let formats: Vec<(i32, String)> = vec![
|
||||
(PROJECT_FORMAT_OTIO, "OpenTimelineIO (.otio)".to_string()),
|
||||
(PROJECT_FORMAT_OVE, "Oak (.ove)".to_string()),
|
||||
(PROJECT_FORMAT_FCPXML, "Final Cut Pro XML (.fcpxml)".to_string()),
|
||||
(
|
||||
PROJECT_FORMAT_FCPXML,
|
||||
"Final Cut Pro XML (.fcpxml)".to_string(),
|
||||
),
|
||||
];
|
||||
let options = formats
|
||||
.iter()
|
||||
@@ -4204,7 +4185,9 @@ impl ExportProjectDialogContent {
|
||||
ComboBox::new(4, options, window, cx)
|
||||
.with_placeholder(i18n::tr("project.export.format"))
|
||||
});
|
||||
format.update(cx, |combo, cx| combo.set_selected(Some(PROJECT_FORMAT_OTIO as usize), cx));
|
||||
format.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(PROJECT_FORMAT_OTIO as usize), cx)
|
||||
});
|
||||
Self { format, formats }
|
||||
}
|
||||
|
||||
@@ -4232,16 +4215,11 @@ impl ExportProjectDialogContent {
|
||||
impl Render for ExportProjectDialogContent {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.export.format").into(),
|
||||
self.format.clone(),
|
||||
))
|
||||
div().flex().flex_col().gap_3().w_full().child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.export.format").into(),
|
||||
self.format.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4264,9 +4242,8 @@ impl NewProjectContent {
|
||||
name.update(cx, |field, cx| {
|
||||
field.set_value(i18n::tr("manager.new.default_name"), cx)
|
||||
});
|
||||
let format = cx.new(|cx| {
|
||||
SequenceFormatFields::build(SequenceFormatSeed::hd_1080p25(), window, cx)
|
||||
});
|
||||
let format =
|
||||
cx.new(|cx| SequenceFormatFields::build(SequenceFormatSeed::hd_1080p25(), window, cx));
|
||||
Self { name, format }
|
||||
}
|
||||
|
||||
@@ -4305,8 +4282,8 @@ impl Render for NewProjectContent {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn keystroke(key: &str) -> Keystroke {
|
||||
use super::*;
|
||||
fn keystroke(key: &str) -> Keystroke {
|
||||
gpui::Keystroke::parse(key).unwrap()
|
||||
}
|
||||
|
||||
@@ -4316,9 +4293,10 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn export_dialog_sequence_picker(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(gpui::size(gpui::px(440.0), gpui::px(400.0)), |window, cx| {
|
||||
ExportDialogContent::new(window, cx)
|
||||
});
|
||||
let window = cx.open_window(
|
||||
gpui::size(gpui::px(440.0), gpui::px(400.0)),
|
||||
ExportDialogContent::new,
|
||||
);
|
||||
cx.run_until_parked();
|
||||
let content = window.root(cx).expect("dialog content root");
|
||||
|
||||
@@ -4349,7 +4327,9 @@ mod tests {
|
||||
);
|
||||
|
||||
cx.update(|cx| {
|
||||
content.update(cx, |content, cx| content.set_sequences(Vec::new(), None, cx));
|
||||
content.update(cx, |content, cx| {
|
||||
content.set_sequences(Vec::new(), None, cx)
|
||||
});
|
||||
});
|
||||
assert_eq!(
|
||||
cx.read(|cx| content.read(cx).selected_sequence(cx)),
|
||||
@@ -4370,8 +4350,16 @@ mod tests {
|
||||
assert_eq!(sequence_preset_format(7), None);
|
||||
// The preset labels come from the language packs (default English
|
||||
// here): the 4K entries say what they set.
|
||||
assert!(labels[5].contains("3840×2160"), "4K UHD label: {}", labels[5]);
|
||||
assert!(labels[6].contains("4096×2160"), "4K DCI label: {}", labels[6]);
|
||||
assert!(
|
||||
labels[5].contains("3840×2160"),
|
||||
"4K UHD label: {}",
|
||||
labels[5]
|
||||
);
|
||||
assert!(
|
||||
labels[6].contains("4096×2160"),
|
||||
"4K DCI label: {}",
|
||||
labels[6]
|
||||
);
|
||||
}
|
||||
|
||||
/// A probed footage format seeds the new-sequence fields: an exact
|
||||
|
||||
+17
-17
@@ -296,6 +296,23 @@ fn reload_for_test() {
|
||||
*tables = load_packs();
|
||||
}
|
||||
|
||||
/// Serializes tests that mutate the language global / HOME.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
lang_test_lock().lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Compatibility alias used by the app/actions test modules. Both entry
|
||||
/// points MUST share the one static mutex: with two separate mutexes the
|
||||
/// i18n tests and the app/actions tests do not exclude each other, and
|
||||
/// the language global races (observed as a Windows-only CI failure
|
||||
/// where `tr` returned the English value mid-test).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn lang_test_lock() -> &'static std::sync::Mutex<()> {
|
||||
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
&LOCK
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -454,20 +471,3 @@ mod tests {
|
||||
set_language_code("en-US");
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes tests that mutate the language global / HOME.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
lang_test_lock().lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Compatibility alias used by the app/actions test modules. Both entry
|
||||
/// points MUST share the one static mutex: with two separate mutexes the
|
||||
/// i18n tests and the app/actions tests do not exclude each other, and
|
||||
/// the language global races (observed as a Windows-only CI failure
|
||||
/// where `tr` returned the English value mid-test).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn lang_test_lock() -> &'static std::sync::Mutex<()> {
|
||||
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
&LOCK
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
//! are unit tested here; the app-level flows are covered by the
|
||||
//! `OakApp<MockEngine>` tests in `crate::app`.
|
||||
|
||||
use crate::oakui::component::text_input;
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
@@ -37,7 +38,6 @@ use gpui::{
|
||||
Window,
|
||||
};
|
||||
use gpui_elements::editable_text::{EditableTextState, StringStorage};
|
||||
use crate::oakui::component::text_input;
|
||||
|
||||
use crate::i18n;
|
||||
use crate::oakui::{AppEngine, LibraryProject};
|
||||
@@ -162,7 +162,11 @@ impl<E: AppEngine> ProjectManager<E> {
|
||||
}
|
||||
|
||||
/// An action-row button targeting the selection.
|
||||
fn selected_action(&mut self, action: impl FnOnce(String) -> ManagerEvent, cx: &mut Context<Self>) {
|
||||
fn selected_action(
|
||||
&mut self,
|
||||
action: impl FnOnce(String) -> ManagerEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(uuid) = self.selected_uuid() {
|
||||
self.emit(action(uuid), cx);
|
||||
}
|
||||
@@ -252,13 +256,11 @@ impl<E: AppEngine> Render for ProjectManager<E> {
|
||||
.min_h_0()
|
||||
.overflow_y_scroll()
|
||||
.children(if self.rows.is_empty() {
|
||||
vec![
|
||||
div()
|
||||
.p_4()
|
||||
.text_color(colors.disabled)
|
||||
.child(i18n::tr("manager.empty"))
|
||||
.into_any_element(),
|
||||
]
|
||||
vec![div()
|
||||
.p_4()
|
||||
.text_color(colors.disabled)
|
||||
.child(i18n::tr("manager.empty"))
|
||||
.into_any_element()]
|
||||
} else {
|
||||
self.rows
|
||||
.iter()
|
||||
@@ -272,11 +274,14 @@ impl<E: AppEngine> Render for ProjectManager<E> {
|
||||
.gap_2()
|
||||
.py_1()
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(move |this, event: &ClickEvent, _w, cx| {
|
||||
this.row_clicked(index, event.click_count(), cx);
|
||||
}));
|
||||
.on_click(cx.listener(
|
||||
move |this, event: &ClickEvent, _w, cx| {
|
||||
this.row_clicked(index, event.click_count(), cx);
|
||||
},
|
||||
));
|
||||
if selected {
|
||||
line = line.bg(colors.selected).text_color(colors.selected_text);
|
||||
line =
|
||||
line.bg(colors.selected).text_color(colors.selected_text);
|
||||
} else {
|
||||
line = line.text_color(colors.text);
|
||||
}
|
||||
@@ -301,72 +306,69 @@ impl<E: AppEngine> Render for ProjectManager<E> {
|
||||
}),
|
||||
);
|
||||
|
||||
let mut root = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
tool("manager-new", i18n::tr("manager.new")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.emit(ManagerEvent::Create, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
tool("manager-import", i18n::tr("manager.import")).on_click(
|
||||
let mut root =
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
tool("manager-new", i18n::tr("manager.new")).on_click(cx.listener(
|
||||
|this, _e: &ClickEvent, _w, cx| {
|
||||
this.emit(ManagerEvent::Create, cx);
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(tool("manager-import", i18n::tr("manager.import")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.emit(ManagerEvent::Import, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(list)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.child(
|
||||
action("manager-open", i18n::tr("manager.open")).on_click(
|
||||
)),
|
||||
)
|
||||
.child(list)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.child(action("manager-open", i18n::tr("manager.open")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Open, cx);
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
action("manager-rename", i18n::tr("manager.rename")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Rename, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-duplicate", i18n::tr("manager.duplicate")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Duplicate, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-delete", i18n::tr("manager.delete")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Delete, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-export", i18n::tr("manager.export")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Export, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-rename", i18n::tr("manager.rename")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Rename, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-duplicate", i18n::tr("manager.duplicate")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Duplicate, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-delete", i18n::tr("manager.delete")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Delete, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-export", i18n::tr("manager.export")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Export, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
if let Some(status) = &self.status {
|
||||
root = root.child(
|
||||
div()
|
||||
@@ -396,10 +398,7 @@ pub struct NamePrompt {
|
||||
impl NamePrompt {
|
||||
/// Builds the prompt seeded with `initial`.
|
||||
pub fn new(initial: &str, cx: &mut Context<Self>) -> Self {
|
||||
let editor = cx.new(|cx| {
|
||||
let editor = EditableTextState::new(StringStorage::default(), cx);
|
||||
editor
|
||||
});
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
editor.update(cx, |editor, cx| editor.emplace(initial, cx));
|
||||
Self { editor }
|
||||
}
|
||||
@@ -411,7 +410,8 @@ impl NamePrompt {
|
||||
|
||||
/// Replaces the entered name (tests / prefill).
|
||||
pub fn set_value(&mut self, value: &str, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| editor.emplace(value, cx));
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.emplace(value, cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,8 @@ impl ContextMenuHandle {
|
||||
|
||||
/// Open the menu at `position` (window coordinates).
|
||||
pub fn show(&self, position: Point<Pixels>, menu: Menu, cx: &mut App) {
|
||||
self.menu.update(cx, |menu_view, cx| menu_view.show(position, menu, cx));
|
||||
self.menu
|
||||
.update(cx, |menu_view, cx| menu_view.show(position, menu, cx));
|
||||
}
|
||||
|
||||
/// The popup entity, to be rendered as a child of the panel so the
|
||||
@@ -126,7 +127,8 @@ pub const LANG_ITEM_COUNT: usize = 32;
|
||||
/// Maps a menu item id in the language range back to its index into
|
||||
/// [`crate::i18n::available_languages`].
|
||||
pub fn language_item_index(item: usize) -> Option<usize> {
|
||||
(item >= LANG_ITEM_BASE && item < LANG_ITEM_BASE + LANG_ITEM_COUNT)
|
||||
(LANG_ITEM_BASE..LANG_ITEM_BASE + LANG_ITEM_COUNT)
|
||||
.contains(&item)
|
||||
.then(|| item - LANG_ITEM_BASE)
|
||||
}
|
||||
|
||||
@@ -262,7 +264,8 @@ pub fn color_label_item(selected: Option<usize>) -> MenuItem {
|
||||
/// The color index (`0..COLOR_LABEL_COUNT`) behind a triggered menu item id,
|
||||
/// when `item` is one of the color-label items.
|
||||
pub fn color_label_index(item: usize) -> Option<usize> {
|
||||
(item >= COLOR_LABEL_BASE && item < COLOR_LABEL_BASE + COLOR_LABEL_COUNT)
|
||||
(COLOR_LABEL_BASE..COLOR_LABEL_BASE + COLOR_LABEL_COUNT)
|
||||
.contains(&item)
|
||||
.then(|| item - COLOR_LABEL_BASE)
|
||||
}
|
||||
|
||||
@@ -334,12 +337,17 @@ impl Default for ViewerMenuState {
|
||||
pub fn viewer_menu(state: &ViewerMenuState) -> Menu {
|
||||
use crate::i18n::tr;
|
||||
// Zoom: Fit + one entry per zoom level, checked against the live state.
|
||||
let mut zoom_items = vec![MenuItem::new(LOCAL_VIEWER_ZOOM_FIT, tr("viewer.context.zoom_fit"))
|
||||
.with_checked(state.zoom == ViewerZoom::Fit)];
|
||||
let mut zoom_items = vec![
|
||||
MenuItem::new(LOCAL_VIEWER_ZOOM_FIT, tr("viewer.context.zoom_fit"))
|
||||
.with_checked(state.zoom == ViewerZoom::Fit),
|
||||
];
|
||||
for (index, level) in VIEWER_ZOOM_LEVELS.iter().enumerate() {
|
||||
zoom_items.push(
|
||||
MenuItem::new(viewer_zoom_level_id(index), format!("{:.0}%", level * 100.0))
|
||||
.with_checked(state.zoom == ViewerZoom::Level(index)),
|
||||
MenuItem::new(
|
||||
viewer_zoom_level_id(index),
|
||||
format!("{:.0}%", level * 100.0),
|
||||
)
|
||||
.with_checked(state.zoom == ViewerZoom::Level(index)),
|
||||
);
|
||||
}
|
||||
// Playback Resolution radio group (the C++ `PlaybackDivider` config):
|
||||
@@ -376,9 +384,10 @@ pub fn viewer_menu(state: &ViewerMenuState) -> Menu {
|
||||
Menu::new(vec![
|
||||
MenuItem::new(0, tr("viewer.context.zoom")).with_submenu(Menu::new(zoom_items)),
|
||||
MenuItem::new(LOCAL_VIEWER_FULL_SCREEN, tr("viewer.context.full_screen")),
|
||||
MenuItem::new(0, tr("viewer.context.playback_resolution"))
|
||||
.with_submenu(resolution_menu),
|
||||
MenuItem::new(0, tr("viewer.context.safe_margins")).with_submenu(safe_menu).separated(),
|
||||
MenuItem::new(0, tr("viewer.context.playback_resolution")).with_submenu(resolution_menu),
|
||||
MenuItem::new(0, tr("viewer.context.safe_margins"))
|
||||
.with_submenu(safe_menu)
|
||||
.separated(),
|
||||
MenuItem::new(LOCAL_VIEWER_STOP_ON_LAST, tr("viewer.context.stop_on_last"))
|
||||
.with_checked(state.stop_on_last)
|
||||
.separated(),
|
||||
@@ -426,7 +435,9 @@ pub fn viewer_menu_action(item: usize) -> Option<ViewerMenuAction> {
|
||||
} else if item >= LOCAL_VIEWER_ZOOM_LEVELS_BASE
|
||||
&& item < LOCAL_VIEWER_ZOOM_LEVELS_BASE + VIEWER_ZOOM_LEVELS.len()
|
||||
{
|
||||
Some(ViewerMenuAction::ZoomLevel(item - LOCAL_VIEWER_ZOOM_LEVELS_BASE))
|
||||
Some(ViewerMenuAction::ZoomLevel(
|
||||
item - LOCAL_VIEWER_ZOOM_LEVELS_BASE,
|
||||
))
|
||||
} else {
|
||||
match item {
|
||||
LOCAL_VIEWER_FULL_SCREEN => Some(ViewerMenuAction::FullScreen),
|
||||
@@ -585,7 +596,9 @@ mod tests {
|
||||
fn viewer_menu_offers_every_zoom_level() {
|
||||
// The label lookups below race with tests that flip the process
|
||||
// language: pin en-US under the shared lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = viewer_menu(&ViewerMenuState::default());
|
||||
let zoom = menu
|
||||
@@ -599,7 +612,10 @@ mod tests {
|
||||
assert_eq!(zoom_items[0].checked, Some(true), "Fit checked by default");
|
||||
for (index, level) in VIEWER_ZOOM_LEVELS.iter().enumerate() {
|
||||
assert_eq!(zoom_items[index + 1].id, viewer_zoom_level_id(index));
|
||||
assert_eq!(zoom_items[index + 1].label, format!("{:.0}%", level * 100.0));
|
||||
assert_eq!(
|
||||
zoom_items[index + 1].label,
|
||||
format!("{:.0}%", level * 100.0)
|
||||
);
|
||||
assert_eq!(
|
||||
zoom_items[index + 1].checked,
|
||||
Some(false),
|
||||
@@ -614,7 +630,9 @@ mod tests {
|
||||
fn viewer_menu_radio_groups_default_to_the_first_entry() {
|
||||
// The label lookups below race with tests that flip the process
|
||||
// language: pin en-US under the shared lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = viewer_menu(&ViewerMenuState::default());
|
||||
for label_key in [
|
||||
@@ -630,7 +648,9 @@ mod tests {
|
||||
let sub = item.submenu.as_ref().unwrap();
|
||||
assert_eq!(sub.items[0].checked, Some(true), "{label_key} default");
|
||||
assert!(
|
||||
sub.items[1..].iter().all(|item| item.checked == Some(false)),
|
||||
sub.items[1..]
|
||||
.iter()
|
||||
.all(|item| item.checked == Some(false)),
|
||||
"{label_key} non-defaults unchecked"
|
||||
);
|
||||
}
|
||||
@@ -640,7 +660,9 @@ mod tests {
|
||||
#[test]
|
||||
fn viewer_menu_resolution_radio_follows_the_divider() {
|
||||
// Label lookup: pin en-US under the shared language lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = viewer_menu(&ViewerMenuState {
|
||||
playback_divider: 4,
|
||||
@@ -662,7 +684,9 @@ mod tests {
|
||||
#[test]
|
||||
fn viewer_menu_reflects_live_state() {
|
||||
// Label lookup: pin en-US under the shared language lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = viewer_menu(&ViewerMenuState {
|
||||
playback_divider: 2,
|
||||
@@ -721,7 +745,10 @@ mod tests {
|
||||
None,
|
||||
"zoom id past the last level"
|
||||
);
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_FULL_SCREEN), Some(V::FullScreen));
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_FULL_SCREEN),
|
||||
Some(V::FullScreen)
|
||||
);
|
||||
for (id, divider) in [
|
||||
(LOCAL_VIEWER_RES_FULL, 1),
|
||||
(LOCAL_VIEWER_RES_HALF, 2),
|
||||
@@ -732,8 +759,14 @@ mod tests {
|
||||
}
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAFE_OFF), Some(V::SafeOff));
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAFE_ON), Some(V::SafeOn));
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAFE_CUSTOM), Some(V::SafeCustom));
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_STOP_ON_LAST), Some(V::StopOnLast));
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_SAFE_CUSTOM),
|
||||
Some(V::SafeCustom)
|
||||
);
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_STOP_ON_LAST),
|
||||
Some(V::StopOnLast)
|
||||
);
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_WF_AUTOMATIC),
|
||||
Some(V::Waveform(WaveformMode::Automatic))
|
||||
@@ -747,8 +780,15 @@ mod tests {
|
||||
Some(V::Waveform(WaveformMode::Both))
|
||||
);
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SHOW_FPS), Some(V::ShowFps));
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAVE_FRAME), Some(V::SaveFrame));
|
||||
assert_eq!(viewer_menu_action(0), None, "registry-range id is not local");
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_SAVE_FRAME),
|
||||
Some(V::SaveFrame)
|
||||
);
|
||||
assert_eq!(
|
||||
viewer_menu_action(0),
|
||||
None,
|
||||
"registry-range id is not local"
|
||||
);
|
||||
assert_eq!(viewer_menu_action(LOCAL_ID_BASE), None, "color-label id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,9 @@
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use oak_core::color::{pipeline_output_spec, ColorProcessor};
|
||||
use oak_core::colormath::{output_spec_to_xyz_d65, OutputColorSpec, OutputGamut, OutputTransfer};
|
||||
use oak_core::configstore::ConfigStore;
|
||||
use oak_core::color::{pipeline_output_spec, ColorProcessor};
|
||||
|
||||
/// Config key: the display color management mode ("icc" / "off"). The
|
||||
/// preference only applies where the platform policy allows self-management
|
||||
@@ -167,9 +167,7 @@ pub fn display_policy() -> DisplayPolicy {
|
||||
"os" => return DisplayPolicy::OsManaged,
|
||||
"self" => return DisplayPolicy::SelfManaged,
|
||||
other => {
|
||||
log::warn!(
|
||||
"OAK_DISPLAY_POLICY={other:?} ignored (expected \"os\" or \"self\")"
|
||||
);
|
||||
log::warn!("OAK_DISPLAY_POLICY={other:?} ignored (expected \"os\" or \"self\")");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +206,10 @@ pub fn display_policy() -> DisplayPolicy {
|
||||
// mapping, so self-management is the only way to honor wide-gamut
|
||||
// displays there (the "off" preference opts back into OS-management).
|
||||
#[cfg(target_os = "macos")]
|
||||
match ConfigStore::instance().get(None, CONFIG_KEY_COLOR_MODE).as_deref() {
|
||||
match ConfigStore::instance()
|
||||
.get(None, CONFIG_KEY_COLOR_MODE)
|
||||
.as_deref()
|
||||
{
|
||||
Ok("icc") => DisplayPolicy::SelfManaged,
|
||||
_ => DisplayPolicy::OsManaged,
|
||||
}
|
||||
@@ -288,7 +289,7 @@ fn current_monitor_fingerprint(window: &gpui::Window, cx: &gpui::App) -> Option<
|
||||
let center = window.bounds().center();
|
||||
let x = f64::from(center.x) * f64::from(window.scale_factor());
|
||||
let y = f64::from(center.y) * f64::from(window.scale_factor());
|
||||
return oak_core::displayicc::x11_monitor_fingerprint_at(x, y);
|
||||
oak_core::displayicc::x11_monitor_fingerprint_at(x, y)
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
|
||||
@@ -90,7 +90,7 @@ fn build_display_lut() -> oak_core::lut::Lut3d {
|
||||
);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, n as i64);
|
||||
let mut data = Vec::with_capacity(n * 3);
|
||||
for px in samples.chunks_exact(4) {
|
||||
for px in samples.as_chunks::<4>().0 {
|
||||
data.extend_from_slice(&px[..3]);
|
||||
}
|
||||
oak_core::lut::Lut3d { edge, lo, hi, data }
|
||||
@@ -128,7 +128,8 @@ pub fn register_context(device: std::sync::Arc<wgpu::Device>, queue: std::sync::
|
||||
if let Ok(mut ctx) = GPU_CONTEXT.lock() {
|
||||
*ctx = Some((device.clone(), queue.clone()));
|
||||
}
|
||||
let adopted = oak_core::backend::GpuContext::adopt(device, queue, oak_core::backend::BackendKind::Auto);
|
||||
let adopted =
|
||||
oak_core::backend::GpuContext::adopt(device, queue, oak_core::backend::BackendKind::Auto);
|
||||
if !oak_core::backend::GpuContext::install_shared(Some(adopted)) {
|
||||
// The engine context was already used for GPU work before the
|
||||
// window opened: it cannot be replaced, so present falls back to
|
||||
@@ -193,8 +194,7 @@ pub fn upload_rgba16f(
|
||||
let ctx = GPU_CONTEXT.lock().ok()?;
|
||||
ctx.as_ref().map(|(d, q)| (d.clone(), q.clone()))?
|
||||
};
|
||||
let (bytes, bytes_per_row) =
|
||||
super::frames::f32_rgba_to_16f_bytes(width, height, samples)?;
|
||||
let (bytes, bytes_per_row) = super::frames::f32_rgba_to_16f_bytes(width, height, samples)?;
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("oak_display_rgba16f"),
|
||||
size: wgpu::Extent3d {
|
||||
|
||||
@@ -699,12 +699,11 @@ pub fn ts_to_rational(ts: i64, tb: (i64, i64)) -> Rational {
|
||||
|
||||
/// The sequence's track list of `kind`, when it exists.
|
||||
pub fn track_list_of(g: &Graph, seq: NodeId, kind: TrackType) -> Option<NodeId> {
|
||||
for &list_id in &sequence_behavior(g, seq)?.track_lists {
|
||||
if track_list_behavior(g, list_id).map(|l| l.kind) == Some(kind) {
|
||||
return Some(list_id);
|
||||
}
|
||||
}
|
||||
None
|
||||
sequence_behavior(g, seq)?
|
||||
.track_lists
|
||||
.iter()
|
||||
.find(|&&list_id| track_list_behavior(g, list_id).map(|l| l.kind) == Some(kind))
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Find (or create) the sequence's track list of `kind` (the facade's
|
||||
@@ -1700,7 +1699,12 @@ pub fn create_adjustment_layer(
|
||||
let index = track_list_behavior(&g.graph, list)
|
||||
.and_then(|l| l.tracks.iter().position(|&t| t == track))
|
||||
.ok_or_else(|| "the track is not in its list".to_string())?;
|
||||
(list, index, ts_to_rational(in_ts, tb), ts_to_rational(out_ts, tb))
|
||||
(
|
||||
list,
|
||||
index,
|
||||
ts_to_rational(in_ts, tb),
|
||||
ts_to_rational(out_ts, tb),
|
||||
)
|
||||
};
|
||||
|
||||
// The block itself: no label, no color override, no media wiring —
|
||||
@@ -2370,7 +2374,12 @@ pub fn place_generator_clip(
|
||||
in_r,
|
||||
)
|
||||
.to_command();
|
||||
let edge = connect_command(p, generator, clip, oak_node::block::clip_input::TEXTURE_INPUT)?;
|
||||
let edge = connect_command(
|
||||
p,
|
||||
generator,
|
||||
clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
)?;
|
||||
push_multi(vec![place, edge], "Add Clip")?;
|
||||
Ok(clip)
|
||||
}
|
||||
@@ -4557,7 +4566,15 @@ mod undo_cycle_ops_tests {
|
||||
/// tests use.
|
||||
fn two_touching_clips(
|
||||
media: &std::path::Path,
|
||||
) -> (ProjectRef, NodeId, NodeId, NodeId, NodeId, (i64, i64), Rational) {
|
||||
) -> (
|
||||
ProjectRef,
|
||||
NodeId,
|
||||
NodeId,
|
||||
NodeId,
|
||||
NodeId,
|
||||
(i64, i64),
|
||||
Rational,
|
||||
) {
|
||||
let project = create_project();
|
||||
let seq = create_sequence(&project, "Default Transition");
|
||||
let footage = import_footage(&project, media).expect("import");
|
||||
@@ -4586,8 +4603,8 @@ mod undo_cycle_ops_tests {
|
||||
fn default_transition_covers_both_ends_of_a_lone_clip() {
|
||||
let _g = test_lock();
|
||||
oak_undo::global::clear().unwrap();
|
||||
let media = std::env::temp_dir()
|
||||
.join(format!("oak_lone_transition_{}.mp4", std::process::id()));
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oak_lone_transition_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate");
|
||||
let project = create_project();
|
||||
let seq = create_sequence(&project, "Lone Transition");
|
||||
@@ -4604,10 +4621,14 @@ mod undo_cycle_ops_tests {
|
||||
let half = ts_to_rational(3, tb);
|
||||
|
||||
let before = oak_undo::global::count().unwrap();
|
||||
let created = add_default_transition(&project, seq, &[clip], half)
|
||||
.expect("add default transition");
|
||||
let created =
|
||||
add_default_transition(&project, seq, &[clip], half).expect("add default transition");
|
||||
assert_eq!(created, 2, "a lone clip takes both ends");
|
||||
assert_eq!(oak_undo::global::count().unwrap(), before + 1, "one undo row");
|
||||
assert_eq!(
|
||||
oak_undo::global::count().unwrap(),
|
||||
before + 1,
|
||||
"one undo row"
|
||||
);
|
||||
|
||||
let edge = half + half;
|
||||
{
|
||||
@@ -4615,7 +4636,10 @@ mod undo_cycle_ops_tests {
|
||||
let blocks = track_behavior(&g.graph, track)
|
||||
.map(|t| t.blocks.clone())
|
||||
.unwrap_or_default();
|
||||
let index = blocks.iter().position(|&b| b == clip).expect("clip on track");
|
||||
let index = blocks
|
||||
.iter()
|
||||
.position(|&b| b == clip)
|
||||
.expect("clip on track");
|
||||
// (A leading gap when the clip starts past zero,) the head
|
||||
// transition, the clip, the tail transition.
|
||||
let (head, tail) = (blocks[index - 1], blocks[index + 1]);
|
||||
@@ -4636,12 +4660,14 @@ mod undo_cycle_ops_tests {
|
||||
"the head transition spans the clip's head"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(head, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(head, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
Some(clip),
|
||||
"the head transition wires only the clip in"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(head, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(head, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
None,
|
||||
"the head transition has no outgoing side"
|
||||
);
|
||||
@@ -4662,12 +4688,14 @@ mod undo_cycle_ops_tests {
|
||||
"the tail transition spans the clip's tail"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(tail, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(tail, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
Some(clip),
|
||||
"the tail transition wires only the clip out"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(tail, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(tail, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
None,
|
||||
"the tail transition has no incoming side"
|
||||
);
|
||||
@@ -4694,8 +4722,8 @@ mod undo_cycle_ops_tests {
|
||||
fn default_transition_covers_the_selected_seams() {
|
||||
let _g = test_lock();
|
||||
oak_undo::global::clear().unwrap();
|
||||
let media = std::env::temp_dir()
|
||||
.join(format!("oak_default_transition_{}.mp4", std::process::id()));
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oak_default_transition_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate");
|
||||
let (project, seq, track, a, b, tb, half) = two_touching_clips(&media);
|
||||
|
||||
@@ -4716,7 +4744,11 @@ mod undo_cycle_ops_tests {
|
||||
let blocks = track_behavior(&g.graph, track)
|
||||
.map(|t| t.blocks.clone())
|
||||
.unwrap_or_default();
|
||||
assert_eq!(blocks.len(), 3, "the transition joins the track: {blocks:?}");
|
||||
assert_eq!(
|
||||
blocks.len(),
|
||||
3,
|
||||
"the transition joins the track: {blocks:?}"
|
||||
);
|
||||
let t = blocks[1];
|
||||
let seam = ts_to_rational(10, tb);
|
||||
let core = block_core_of(&g.graph, t).expect("transition core");
|
||||
@@ -4737,12 +4769,14 @@ mod undo_cycle_ops_tests {
|
||||
"a default transition is symmetric"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(t, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(t, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
Some(a),
|
||||
"the previous clip feeds out_block_in"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(t, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(t, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
Some(b),
|
||||
"the next clip feeds in_block_in"
|
||||
);
|
||||
@@ -4787,7 +4821,10 @@ mod undo_cycle_ops_tests {
|
||||
assert_eq!(oak_undo::global::count().unwrap(), rows);
|
||||
{
|
||||
let g = lock(&project);
|
||||
assert!(g.graph.is_valid(transition), "the refused batch changed nothing");
|
||||
assert!(
|
||||
g.graph.is_valid(transition),
|
||||
"the refused batch changed nothing"
|
||||
);
|
||||
}
|
||||
|
||||
oak_undo::global::clear().unwrap();
|
||||
@@ -4844,8 +4881,8 @@ mod undo_cycle_ops_tests {
|
||||
cycle_assert(&project, seq, &post, "transition length");
|
||||
|
||||
// A request longer than the dragged clip is clamped to its length.
|
||||
let applied =
|
||||
set_transition_length(&project, b, true, frame, ts_to_rational(9_999, tb)).expect("clamp");
|
||||
let applied = set_transition_length(&project, b, true, frame, ts_to_rational(9_999, tb))
|
||||
.expect("clamp");
|
||||
assert_eq!(
|
||||
applied,
|
||||
ts_to_rational(10, tb),
|
||||
|
||||
@@ -43,19 +43,19 @@ use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
use gpui::effect_stack::{
|
||||
EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent,
|
||||
EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent,
|
||||
};
|
||||
use gpui::node_graph::{
|
||||
EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeId, PortData,
|
||||
PortDataType, PortId, PortKind,
|
||||
EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeId, PortData,
|
||||
PortDataType, PortId, PortKind,
|
||||
};
|
||||
use gpui::timeline::{
|
||||
ClipData, ClipId, Frame, FrameRange, FrameRate, Marker, TimelineDataSource, TimelineEvent,
|
||||
TrackData, TrackKind,
|
||||
ClipData, ClipId, Frame, FrameRange, FrameRate, Marker, TimelineDataSource, TimelineEvent,
|
||||
TrackData, TrackKind,
|
||||
};
|
||||
use gpui::{
|
||||
hsla, point, prelude::*, px, App, Context, Entity, Hsla, Pixels, Point, RenderImage,
|
||||
SharedString,
|
||||
hsla, point, prelude::*, px, App, Context, Entity, Hsla, Pixels, Point, RenderImage,
|
||||
SharedString,
|
||||
};
|
||||
use gpui_widgets::audio_meter::AudioMeterDataSource;
|
||||
use gpui_widgets::project_explorer::{ProjectDataSource, ProjectEntry};
|
||||
@@ -67,8 +67,8 @@ use oak_node::track::TrackType;
|
||||
use oak_timeline::util::{block_clip_create, track_append_block};
|
||||
|
||||
use super::engine::{
|
||||
AppEngine, EngineGateway, ExportEvent, ExportSession, LibraryProject, Monitor, MulticamState,
|
||||
Project, ScopeData, Sequence, VideoFormat, WizardFootage, WizardSyncOffset,
|
||||
AppEngine, EngineGateway, ExportEvent, ExportSession, LibraryProject, Monitor, MulticamState,
|
||||
Project, ScopeData, Sequence, VideoFormat, WizardFootage, WizardSyncOffset,
|
||||
};
|
||||
use super::graphops;
|
||||
use super::nodegraph::GraphEndpoint;
|
||||
@@ -1001,7 +1001,7 @@ impl MockEngine {
|
||||
NodeGraphEvent::NodeMoveRequested { nodes, delta } => {
|
||||
for id in nodes {
|
||||
if let Some(node) = self.nodes.iter_mut().find(|n| n.id() == *id) {
|
||||
node.position = node.position + *delta;
|
||||
node.position += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2099,8 +2099,7 @@ impl AppEngine for MockEngine {
|
||||
.is_some_and(|t| t.kind == TrackKind::Video)
|
||||
{
|
||||
track_index
|
||||
} else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video)
|
||||
{
|
||||
} else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) {
|
||||
index
|
||||
} else {
|
||||
return Err("no video track".to_string());
|
||||
@@ -2494,7 +2493,7 @@ impl AppEngine for MockEngine {
|
||||
if entry.is_dir {
|
||||
continue;
|
||||
}
|
||||
let is_audio = crate::oakui::filename_is_audio(&entry.name.to_string());
|
||||
let is_audio = crate::oakui::filename_is_audio(entry.name.as_ref());
|
||||
let state = self
|
||||
.proxy_states
|
||||
.get(&entry.id)
|
||||
@@ -2916,7 +2915,11 @@ impl ProjectDataSource for MockEngine {
|
||||
// shared node label `文本`, so the demo's browser stays locale-
|
||||
// independent).
|
||||
for &id in &self.text_footages {
|
||||
roots.push(ProjectEntry::new(id, super::graphops::TEXT_FOOTAGE_LABEL, false));
|
||||
roots.push(ProjectEntry::new(
|
||||
id,
|
||||
super::graphops::TEXT_FOOTAGE_LABEL,
|
||||
false,
|
||||
));
|
||||
}
|
||||
roots
|
||||
}
|
||||
@@ -2969,11 +2972,11 @@ impl DemoMulticamGraph {
|
||||
/// tracks are built directly in the graph (no `Add Track` undo entries —
|
||||
/// the demo's initial state is not a user edit).
|
||||
fn build() -> Self {
|
||||
use oak_node::node::NodeCore;
|
||||
use oak_node::sequence::SequenceBehavior;
|
||||
use oak_node::track::{TrackBehavior, TrackListBehavior};
|
||||
use oak_node::node::NodeCore;
|
||||
use oak_node::sequence::SequenceBehavior;
|
||||
use oak_node::track::{TrackBehavior, TrackListBehavior};
|
||||
|
||||
let project = graphops::create_project();
|
||||
let project = graphops::create_project();
|
||||
let sequence = graphops::create_sequence(&project, "Multicam Demo");
|
||||
// A video track list with four tracks, wired into the sequence.
|
||||
{
|
||||
@@ -3257,12 +3260,12 @@ impl MockEngine {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::timeline::TrimEdge;
|
||||
use super::*;
|
||||
use gpui::timeline::TrimEdge;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
fn demo_engine(app: &mut gpui::App) -> Entity<MockEngine> {
|
||||
app.new(|cx| MockEngine::demo(cx))
|
||||
fn demo_engine(app: &mut gpui::App) -> Entity<MockEngine> {
|
||||
app.new(MockEngine::demo)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -3327,8 +3330,8 @@ mod tests {
|
||||
/// The stop-on-last tick pauses on the final frame instead of wrapping.
|
||||
#[test]
|
||||
fn clock_tick_stops_on_the_last_frame_when_asked() {
|
||||
use std::time::{Duration, Instant};
|
||||
let mut clock = MockClock::new(FrameRate::new(30, 1));
|
||||
use std::time::{Duration, Instant};
|
||||
let mut clock = MockClock::new(FrameRate::new(30, 1));
|
||||
clock.play();
|
||||
// 10 s at 30 fps = 300 frames into a 5-frame sequence: wrapped 60×.
|
||||
clock.started = Some((Instant::now() - Duration::from_secs(10), Frame(0)));
|
||||
@@ -3348,8 +3351,8 @@ mod tests {
|
||||
/// Without stop-on-last the tick wraps modulo the sequence length.
|
||||
#[test]
|
||||
fn clock_tick_loops_when_not_stopping() {
|
||||
use std::time::{Duration, Instant};
|
||||
let mut clock = MockClock::new(FrameRate::new(30, 1));
|
||||
use std::time::{Duration, Instant};
|
||||
let mut clock = MockClock::new(FrameRate::new(30, 1));
|
||||
clock.play();
|
||||
clock.started = Some((Instant::now() - Duration::from_secs(10), Frame(0)));
|
||||
|
||||
@@ -3973,9 +3976,9 @@ mod tests {
|
||||
.collect();
|
||||
assert_eq!(listed.len(), 2, "both text entries are listed in the bin");
|
||||
assert!(
|
||||
listed.iter().all(|e| {
|
||||
e.name == crate::oakui::graphops::TEXT_FOOTAGE_LABEL && !e.is_dir
|
||||
}),
|
||||
listed
|
||||
.iter()
|
||||
.all(|e| { e.name == crate::oakui::graphops::TEXT_FOOTAGE_LABEL && !e.is_dir }),
|
||||
"the entries carry the shared 文本 label as leaves"
|
||||
);
|
||||
});
|
||||
|
||||
+106
-31
@@ -309,7 +309,9 @@ fn active_interact_slot() -> &'static Mutex<Option<ActiveInteract>> {
|
||||
pub fn sync_active_interact(instance: Option<u64>) {
|
||||
// Unchanged target: nothing to do.
|
||||
{
|
||||
let mut slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut slot = active_interact_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if slot
|
||||
.as_ref()
|
||||
.is_some_and(|active| Some(active.instance) == instance)
|
||||
@@ -340,7 +342,9 @@ pub fn sync_active_interact(instance: Option<u64>) {
|
||||
// Describe → CreateInstance before any draw/pen/key action).
|
||||
interact.describe();
|
||||
interact.create_instance();
|
||||
let mut slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut slot = active_interact_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
*slot = Some(ActiveInteract {
|
||||
instance: id,
|
||||
interact,
|
||||
@@ -357,14 +361,19 @@ pub fn sync_active_interact(instance: Option<u64>) {
|
||||
/// interact). The viewport reports the last drawn size and updates as the
|
||||
/// viewer's frame size changes.
|
||||
pub fn active_interact() -> Option<(u64, Arc<Interact>, InteractViewport)> {
|
||||
let slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner());
|
||||
slot.as_ref().map(|a| (a.instance, a.interact.clone(), a.viewport))
|
||||
let slot = active_interact_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
slot.as_ref()
|
||||
.map(|a| (a.instance, a.interact.clone(), a.viewport))
|
||||
}
|
||||
|
||||
/// Records the viewport the interact was last drawn at (keeps
|
||||
/// [`active_interact`]'s viewport current).
|
||||
fn note_interact_viewport(instance: u64, viewport: InteractViewport) {
|
||||
let mut slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut slot = active_interact_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(active) = slot.as_mut() {
|
||||
if active.instance == instance {
|
||||
active.viewport = viewport;
|
||||
@@ -389,7 +398,7 @@ fn bgra_image_to_f32_rgba(img: &RenderImage) -> Option<(u32, u32, Vec<f32>)> {
|
||||
return None;
|
||||
}
|
||||
let mut out = Vec::with_capacity(expected);
|
||||
for px in bytes[..expected].chunks_exact(4) {
|
||||
for px in bytes[..expected].as_chunks::<4>().0 {
|
||||
out.push(px[2] as f32 / 255.0); // R
|
||||
out.push(px[1] as f32 / 255.0); // G
|
||||
out.push(px[0] as f32 / 255.0); // B
|
||||
@@ -401,7 +410,9 @@ fn bgra_image_to_f32_rgba(img: &RenderImage) -> Option<(u32, u32, Vec<f32>)> {
|
||||
/// Reads an `oakplugin` F32 RGBA image into a tightly packed `Vec<f32>`.
|
||||
fn read_image_f32(img: &oak_plugin::image::Image) -> Vec<f32> {
|
||||
img.pixels()
|
||||
.chunks_exact(4)
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|c| f32::from_ne_bytes(c[0..4].try_into().unwrap()))
|
||||
.collect()
|
||||
}
|
||||
@@ -423,7 +434,7 @@ fn read_image_f32(img: &oak_plugin::image::Image) -> Vec<f32> {
|
||||
/// straight-alpha "over". `base` is the displayed frame (alpha 1.0), so
|
||||
/// the result alpha stays 1.0. Returns `None` on a length mismatch.
|
||||
pub fn composite_overlay(overlay: &[f32], base: &[f32]) -> Option<Vec<f32>> {
|
||||
if overlay.len() != base.len() || overlay.len() % 4 != 0 {
|
||||
if overlay.len() != base.len() || !overlay.len().is_multiple_of(4) {
|
||||
return None;
|
||||
}
|
||||
let mut out = vec![0.0f32; overlay.len()];
|
||||
@@ -519,7 +530,9 @@ pub fn draw_interact_composite(
|
||||
}
|
||||
let merged = composite_overlay(&read_image_f32(&overlay), &base_f32)?;
|
||||
note_interact_viewport(instance, *viewport);
|
||||
Some(Arc::new(super::frames::f32_rgba_to_bgra_image(bw, bh, &merged)))
|
||||
Some(Arc::new(super::frames::f32_rgba_to_bgra_image(
|
||||
bw, bh, &merged,
|
||||
)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -695,8 +708,14 @@ mod tests {
|
||||
let p = viewport_pixel_to_pen(point(320.0, 10.0), area, frame);
|
||||
assert_eq!(p, None);
|
||||
// Zero-size inputs.
|
||||
assert_eq!(viewport_pixel_to_pen(point(0.0, 0.0), size(0.0, 0.0), frame), None);
|
||||
assert_eq!(viewport_pixel_to_pen(point(0.0, 0.0), area, size(0.0, 0.0)), None);
|
||||
assert_eq!(
|
||||
viewport_pixel_to_pen(point(0.0, 0.0), size(0.0, 0.0), frame),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
viewport_pixel_to_pen(point(0.0, 0.0), area, size(0.0, 0.0)),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Key mapping: the common keys land on the ofxKeySyms values; printable
|
||||
@@ -710,22 +729,49 @@ mod tests {
|
||||
assert_eq!(key_symbol(&ks("z")), (ofx_key::KEY_Z, "z".to_string()));
|
||||
assert_eq!(key_symbol(&ks("1")), (b'1' as i32, "1".to_string()));
|
||||
// Named keys: symbol only, empty string.
|
||||
assert_eq!(key_symbol(&ks("space")), (ofx_key::KEY_SPACE, String::new()));
|
||||
assert_eq!(key_symbol(&ks("enter")), (ofx_key::KEY_RETURN, String::new()));
|
||||
assert_eq!(key_symbol(&ks("escape")), (ofx_key::KEY_ESCAPE, String::new()));
|
||||
assert_eq!(key_symbol(&ks("backspace")), (ofx_key::KEY_BACKSPACE, String::new()));
|
||||
assert_eq!(key_symbol(&ks("delete")), (ofx_key::KEY_DELETE, String::new()));
|
||||
assert_eq!(
|
||||
key_symbol(&ks("space")),
|
||||
(ofx_key::KEY_SPACE, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("enter")),
|
||||
(ofx_key::KEY_RETURN, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("escape")),
|
||||
(ofx_key::KEY_ESCAPE, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("backspace")),
|
||||
(ofx_key::KEY_BACKSPACE, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("delete")),
|
||||
(ofx_key::KEY_DELETE, String::new())
|
||||
);
|
||||
assert_eq!(key_symbol(&ks("left")), (ofx_key::KEY_LEFT, String::new()));
|
||||
assert_eq!(key_symbol(&ks("right")), (ofx_key::KEY_RIGHT, String::new()));
|
||||
assert_eq!(
|
||||
key_symbol(&ks("right")),
|
||||
(ofx_key::KEY_RIGHT, String::new())
|
||||
);
|
||||
assert_eq!(key_symbol(&ks("up")), (ofx_key::KEY_UP, String::new()));
|
||||
assert_eq!(key_symbol(&ks("down")), (ofx_key::KEY_DOWN, String::new()));
|
||||
assert_eq!(key_symbol(&ks("home")), (ofx_key::KEY_HOME, String::new()));
|
||||
assert_eq!(key_symbol(&ks("end")), (ofx_key::KEY_END, String::new()));
|
||||
assert_eq!(key_symbol(&ks("pageup")), (ofx_key::KEY_PAGE_UP, String::new()));
|
||||
assert_eq!(key_symbol(&ks("pagedown")), (ofx_key::KEY_PAGE_DOWN, String::new()));
|
||||
assert_eq!(
|
||||
key_symbol(&ks("pageup")),
|
||||
(ofx_key::KEY_PAGE_UP, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("pagedown")),
|
||||
(ofx_key::KEY_PAGE_DOWN, String::new())
|
||||
);
|
||||
assert_eq!(key_symbol(&ks("tab")), (ofx_key::KEY_TAB, String::new()));
|
||||
assert_eq!(key_symbol(&ks("f1")), (ofx_key::KEY_F1, String::new()));
|
||||
assert_eq!(key_symbol(&ks("f12")), (ofx_key::KEY_F1 + 11, String::new()));
|
||||
assert_eq!(
|
||||
key_symbol(&ks("f12")),
|
||||
(ofx_key::KEY_F1 + 11, String::new())
|
||||
);
|
||||
// Unknown multi-character keys.
|
||||
let (sym, s) = key_symbol(&ks("insert"));
|
||||
assert_eq!(sym, ofx_key::KEY_UNKNOWN);
|
||||
@@ -745,7 +791,11 @@ mod tests {
|
||||
let overlay = [1.0, 0.0, 0.0, 0.5];
|
||||
let out = composite_overlay(&overlay, &base).unwrap();
|
||||
for (i, expected) in [(0, 0.5), (1, 0.0), (2, 0.5), (3, 1.0)] {
|
||||
assert!((out[i] - expected).abs() < 1e-6, "channel {i}: {} != {expected}", out[i]);
|
||||
assert!(
|
||||
(out[i] - expected).abs() < 1e-6,
|
||||
"channel {i}: {} != {expected}",
|
||||
out[i]
|
||||
);
|
||||
}
|
||||
// Transparent overlay → base unchanged.
|
||||
let overlay = [0.0, 0.0, 0.0, 0.0];
|
||||
@@ -763,8 +813,21 @@ mod tests {
|
||||
let img = super::super::frames::f32_rgba_to_bgra_image(w, h, &samples);
|
||||
let (got_w, got_h, rgba) = bgra_image_to_f32_rgba(&img).unwrap();
|
||||
assert_eq!((got_w, got_h), (w, h));
|
||||
for (i, expected) in [(0, 1.0), (1, 0.0), (2, 0.5), (3, 1.0), (4, 0.25), (5, 0.5), (6, 0.75), (7, 1.0)] {
|
||||
assert!((rgba[i] - expected).abs() < 0.01, "channel {i}: {} != {expected}", rgba[i]);
|
||||
for (i, expected) in [
|
||||
(0, 1.0),
|
||||
(1, 0.0),
|
||||
(2, 0.5),
|
||||
(3, 1.0),
|
||||
(4, 0.25),
|
||||
(5, 0.5),
|
||||
(6, 0.75),
|
||||
(7, 1.0),
|
||||
] {
|
||||
assert!(
|
||||
(rgba[i] - expected).abs() < 0.01,
|
||||
"channel {i}: {} != {expected}",
|
||||
rgba[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -823,7 +886,11 @@ mod tests {
|
||||
println!("SKIP: minimal test plugin not built");
|
||||
return false;
|
||||
};
|
||||
if oak_plugin::host::Host::global().cache.scan_path(&dir).is_err() {
|
||||
if oak_plugin::host::Host::global()
|
||||
.cache
|
||||
.scan_path(&dir)
|
||||
.is_err()
|
||||
{
|
||||
println!("SKIP: test plugin scan failed");
|
||||
return false;
|
||||
}
|
||||
@@ -913,9 +980,7 @@ mod tests {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
let lines = loop {
|
||||
let lines = read_marker(&marker);
|
||||
if lines.iter().any(|l| l == "destroy")
|
||||
|| std::time::Instant::now() >= deadline
|
||||
{
|
||||
if lines.iter().any(|l| l == "destroy") || std::time::Instant::now() >= deadline {
|
||||
break lines;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
@@ -939,15 +1004,21 @@ mod tests {
|
||||
// The forwarded events with real arguments (C %g drops trailing
|
||||
// zeros).
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "pen_motion vp=20,20 canon=20,20 pressure=1"),
|
||||
lines
|
||||
.iter()
|
||||
.any(|l| l == "pen_motion vp=20,20 canon=20,20 pressure=1"),
|
||||
"pen_motion not recorded with pen-down state: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "pen_down vp=20,20 canon=20,20 pressure=1"),
|
||||
lines
|
||||
.iter()
|
||||
.any(|l| l == "pen_down vp=20,20 canon=20,20 pressure=1"),
|
||||
"pen_down not recorded: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "pen_up vp=20,20 canon=20,20 pressure=0"),
|
||||
lines
|
||||
.iter()
|
||||
.any(|l| l == "pen_up vp=20,20 canon=20,20 pressure=0"),
|
||||
"pen_up not recorded: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
@@ -992,7 +1063,11 @@ mod tests {
|
||||
for px in base_samples.chunks_exact_mut(4) {
|
||||
px.copy_from_slice(&[0.0, 0.0, 1.0, 1.0]);
|
||||
}
|
||||
let base = Arc::new(super::super::frames::f32_rgba_to_bgra_image(w, h, &base_samples));
|
||||
let base = Arc::new(super::super::frames::f32_rgba_to_bgra_image(
|
||||
w,
|
||||
h,
|
||||
&base_samples,
|
||||
));
|
||||
let viewport = InteractViewport::at_frame_size(w, h);
|
||||
let composite = draw_interact_composite(handle, &interact, &viewport, 0.0, &base)
|
||||
.expect("GL overlay composite");
|
||||
|
||||
+360
-310
File diff suppressed because it is too large
Load Diff
@@ -179,7 +179,10 @@ fn clip_multicam_media(
|
||||
// The source sequence and the current source's track.
|
||||
let seq = g.connected_output(mc, SEQUENCE_INPUT, -1)?;
|
||||
let kind = {
|
||||
let t = mc_entry.core.standard_value(SEQUENCE_TYPE_INPUT, -1).to_double() as i32;
|
||||
let t = mc_entry
|
||||
.core
|
||||
.standard_value(SEQUENCE_TYPE_INPUT, -1)
|
||||
.to_double() as i32;
|
||||
oak_node::track::TrackType::from_c(t).unwrap_or(oak_node::track::TrackType::Video)
|
||||
};
|
||||
let seq_pos = super::graphops::track_list_of(g, seq, kind)?;
|
||||
@@ -221,7 +224,10 @@ fn clip_multicam_media(
|
||||
/// unchanged). Parameters are the inspector's parameter set (non-hidden,
|
||||
/// non-connection inputs at their standard values; keyframed values are
|
||||
/// not time-resolved on this path).
|
||||
fn clip_effects(g: &oak_node::graph::Graph, block_id: NodeId) -> Vec<oak_render::ticket::MontageEffect> {
|
||||
fn clip_effects(
|
||||
g: &oak_node::graph::Graph,
|
||||
block_id: NodeId,
|
||||
) -> Vec<oak_render::ticket::MontageEffect> {
|
||||
use super::effectchain;
|
||||
effectchain::chain(g, block_id)
|
||||
.into_iter()
|
||||
@@ -483,7 +489,9 @@ pub fn render_multicam_angle_frame(
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<RenderedFrame, String> {
|
||||
render_video(multicam_angle_frame_params(p, seq, track, frame_ts, tb, width, height)?)
|
||||
render_video(multicam_angle_frame_params(
|
||||
p, seq, track, frame_ts, tb, width, height,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// The audio montage over `range`: every audio clip overlapping the
|
||||
@@ -518,8 +526,7 @@ pub fn audio_montage(p: &ProjectRef, seq: NodeId, range: TimeRange) -> Vec<Monta
|
||||
if out <= range.in_() || in_ >= range.out() {
|
||||
continue;
|
||||
}
|
||||
let Some((filename, stream_index)) =
|
||||
clip_preview_media(&g.graph, block_id, false)
|
||||
let Some((filename, stream_index)) = clip_preview_media(&g.graph, block_id, false)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@@ -631,23 +638,24 @@ impl RenderedFrame {
|
||||
let (w, h) = (meta.width.max(0) as u32, meta.height.max(0) as u32);
|
||||
let pixels = f.shm.slot_bytes(f.slot);
|
||||
let data = pixels.get(..meta.data_size.max(0) as usize)?;
|
||||
if meta.format == PIXEL_FORMAT_F32 {
|
||||
// M15 S3: the worker rendered F32 (the 10-bit display
|
||||
// path) — repack the padded rows, transform and hand the
|
||||
// samples back for the RGBA16F texture.
|
||||
let mut samples = repack_f32_rows(meta.width, meta.height, meta.linesize, data)?;
|
||||
// Output node: working space → the project's output
|
||||
// colorspace. The scopes below read the output-colorspace
|
||||
// signal (same convention as the BGRA8 slot); the display
|
||||
// policy then decides whether the display ICC is applied on
|
||||
// top (self-managed) or the OS maps the declared content
|
||||
// colorspace (OS-managed).
|
||||
apply_output_node_f32(&mut samples);
|
||||
let scope = analyze_f32_rgba(w, h, &samples);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64);
|
||||
let image = f32_rgba_to_bgra_image(w, h, &samples);
|
||||
Some((image, scope, Some(samples)))
|
||||
} else {
|
||||
if meta.format == PIXEL_FORMAT_F32 {
|
||||
// M15 S3: the worker rendered F32 (the 10-bit display
|
||||
// path) — repack the padded rows, transform and hand the
|
||||
// samples back for the RGBA16F texture.
|
||||
let mut samples =
|
||||
repack_f32_rows(meta.width, meta.height, meta.linesize, data)?;
|
||||
// Output node: working space → the project's output
|
||||
// colorspace. The scopes below read the output-colorspace
|
||||
// signal (same convention as the BGRA8 slot); the display
|
||||
// policy then decides whether the display ICC is applied on
|
||||
// top (self-managed) or the OS maps the declared content
|
||||
// colorspace (OS-managed).
|
||||
apply_output_node_f32(&mut samples);
|
||||
let scope = analyze_f32_rgba(w, h, &samples);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64);
|
||||
let image = f32_rgba_to_bgra_image(w, h, &samples);
|
||||
Some((image, scope, Some(samples)))
|
||||
} else {
|
||||
// BGRA8 slot: the worker already downconverted — wrap the
|
||||
// bytes directly (no 10-bit path available for them).
|
||||
let scope = analyze_bgra8(w, h, data);
|
||||
@@ -687,11 +695,7 @@ impl RenderedFrame {
|
||||
apply_output_node_f32(&mut samples);
|
||||
let scope = analyze_f32_rgba(w, h, &samples);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64);
|
||||
Some((
|
||||
f32_rgba_to_bgra_image(w, h, &samples),
|
||||
scope,
|
||||
Some(samples),
|
||||
))
|
||||
Some((f32_rgba_to_bgra_image(w, h, &samples), scope, Some(samples)))
|
||||
}
|
||||
RenderedFrame::CpuF32 {
|
||||
width,
|
||||
@@ -706,11 +710,7 @@ impl RenderedFrame {
|
||||
apply_output_node_f32(&mut samples);
|
||||
let scope = analyze_f32_rgba(w, h, &samples);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64);
|
||||
Some((
|
||||
f32_rgba_to_bgra_image(w, h, &samples),
|
||||
scope,
|
||||
Some(samples),
|
||||
))
|
||||
Some((f32_rgba_to_bgra_image(w, h, &samples), scope, Some(samples)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -721,9 +721,9 @@ impl RenderedFrame {
|
||||
/// Pass-through in the legacy sRGB working space.
|
||||
fn apply_output_node_f32(samples: &mut [f32]) {
|
||||
oak_core::colormath::working_to_display_target(
|
||||
samples,
|
||||
oak_core::color::pipeline_working_space(),
|
||||
oak_core::color::pipeline_output_spec(),
|
||||
samples,
|
||||
oak_core::color::pipeline_working_space(),
|
||||
oak_core::color::pipeline_output_spec(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -742,7 +742,7 @@ fn repack_f32_rows(width: i32, height: i32, linesize: i32, data: &[u8]) -> Optio
|
||||
let mut samples = vec![0.0f32; (width * height * 4) as usize];
|
||||
for y in 0..height as usize {
|
||||
let row = &data[y * linesize..y * linesize + row_bytes];
|
||||
for (i, px) in row.chunks_exact(4).enumerate() {
|
||||
for (i, px) in row.as_chunks::<4>().0.iter().enumerate() {
|
||||
let v = f32::from_ne_bytes([px[0], px[1], px[2], px[3]]);
|
||||
samples[y * (width as usize) * 4 + i] = v;
|
||||
}
|
||||
@@ -876,7 +876,8 @@ fn proxy_limits_at(p: &ProjectRef, seq: NodeId, time: Rational) -> Vec<(i32, i32
|
||||
/// variant without copying frame bytes — the shm slot is read zero-copy
|
||||
/// by the caller and released after building the display image).
|
||||
fn render_video(params: VideoTicketParams) -> Result<RenderedFrame, String> {
|
||||
let m = RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?;
|
||||
let m =
|
||||
RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?;
|
||||
let id = m.tickets.next_id();
|
||||
if std::env::var_os("OAK_DEBUG_DISPATCH").is_some() {
|
||||
eprintln!("renderops: sync render submit arena ticket {}", id.0);
|
||||
@@ -959,7 +960,9 @@ pub fn render_sequence_frame(
|
||||
height: i32,
|
||||
format: Option<oak_core::PixelFormat>,
|
||||
) -> Result<RenderedFrame, String> {
|
||||
render_video(sequence_frame_params(p, seq, frame_ts, tb, width, height, format)?)
|
||||
render_video(sequence_frame_params(
|
||||
p, seq, frame_ts, tb, width, height, format,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Build the video ticket params for one single-footage frame (M15 S2:
|
||||
@@ -1025,7 +1028,9 @@ pub fn render_footage_frame(
|
||||
height: i32,
|
||||
format: Option<oak_core::PixelFormat>,
|
||||
) -> Result<RenderedFrame, String> {
|
||||
render_video(footage_frame_params(p, footage, frame_ts, tb, width, height, format)?)
|
||||
render_video(footage_frame_params(
|
||||
p, footage, frame_ts, tb, width, height, format,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Rendered interleaved f32 audio (the module audio ticket payload).
|
||||
@@ -1057,7 +1062,8 @@ pub fn render_audio_range(
|
||||
Rational::new((start_ts + len_ts) * tb.0, tb.1),
|
||||
);
|
||||
let montage = audio_montage(p, seq, range);
|
||||
let m = RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?;
|
||||
let m =
|
||||
RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?;
|
||||
let id = m.tickets.next_id();
|
||||
m.tickets.submit_audio_with_id(
|
||||
id,
|
||||
@@ -1178,21 +1184,23 @@ pub fn encoding_params(
|
||||
// Export range: the work area when enabled, otherwise the whole
|
||||
// sequence. Frames -> seconds rationals in the sequence's frame-rate
|
||||
// timebase (frame duration = rate_den / rate_num).
|
||||
let (has_custom_range, range_in, range_out, length) =
|
||||
match workarea.filter(|(s, e)| e > s) {
|
||||
Some((in_ts, out_ts)) => (
|
||||
true,
|
||||
Rational::new(in_ts * i64::from(rate_den), i64::from(rate_num)),
|
||||
Rational::new(out_ts * i64::from(rate_den), i64::from(rate_num)),
|
||||
Rational::new((out_ts - in_ts) * i64::from(rate_den), i64::from(rate_num)),
|
||||
let (has_custom_range, range_in, range_out, length) = match workarea.filter(|(s, e)| e > s) {
|
||||
Some((in_ts, out_ts)) => (
|
||||
true,
|
||||
Rational::new(in_ts * i64::from(rate_den), i64::from(rate_num)),
|
||||
Rational::new(out_ts * i64::from(rate_den), i64::from(rate_num)),
|
||||
Rational::new((out_ts - in_ts) * i64::from(rate_den), i64::from(rate_num)),
|
||||
),
|
||||
None => (
|
||||
false,
|
||||
Rational::new(0, 1),
|
||||
Rational::new(0, 1),
|
||||
Rational::new(
|
||||
length_frames.max(0) * i64::from(rate_den),
|
||||
i64::from(rate_num),
|
||||
),
|
||||
None => (
|
||||
false,
|
||||
Rational::new(0, 1),
|
||||
Rational::new(0, 1),
|
||||
Rational::new(length_frames.max(0) * i64::from(rate_den), i64::from(rate_num)),
|
||||
),
|
||||
};
|
||||
),
|
||||
};
|
||||
Ok(oak_task::export::EncodingParams {
|
||||
filename: path.to_string_lossy().into_owned(),
|
||||
format,
|
||||
@@ -1245,12 +1253,22 @@ pub fn encoding_params_with_settings(
|
||||
.iter()
|
||||
.find(|c| **c as i32 == settings.video_codec)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("codec {} not supported by {container:?}", settings.video_codec))?;
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"codec {} not supported by {container:?}",
|
||||
settings.video_codec
|
||||
)
|
||||
})?;
|
||||
let audio_codec = oak_codec::exportformat::Format::get_audio_codecs(container)
|
||||
.iter()
|
||||
.find(|c| **c as i32 == settings.audio_codec)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("audio codec {} not supported by {container:?}", settings.audio_codec))?;
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"audio codec {} not supported by {container:?}",
|
||||
settings.audio_codec
|
||||
)
|
||||
})?;
|
||||
let (mut width, mut height, rate) = {
|
||||
let g = lock(p);
|
||||
super::graphops::sequence_video_params(&g.graph, seq)
|
||||
@@ -1281,24 +1299,36 @@ pub fn encoding_params_with_settings(
|
||||
_ => match workarea.filter(|(s, e)| e > s) {
|
||||
Some((in_ts, out_ts)) => (
|
||||
true,
|
||||
Rational::new(in_ts * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new(out_ts * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new((out_ts - in_ts) * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new(
|
||||
in_ts * i64::from(rate_den.max(1)),
|
||||
i64::from(rate_num.max(1)),
|
||||
),
|
||||
Rational::new(
|
||||
out_ts * i64::from(rate_den.max(1)),
|
||||
i64::from(rate_num.max(1)),
|
||||
),
|
||||
Rational::new(
|
||||
(out_ts - in_ts) * i64::from(rate_den.max(1)),
|
||||
i64::from(rate_num.max(1)),
|
||||
),
|
||||
),
|
||||
None => (
|
||||
false,
|
||||
Rational::new(0, 1),
|
||||
Rational::new(0, 1),
|
||||
Rational::new(length_frames.max(0) * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new(
|
||||
length_frames.max(0) * i64::from(rate_den.max(1)),
|
||||
i64::from(rate_num.max(1)),
|
||||
),
|
||||
),
|
||||
},
|
||||
};
|
||||
let bit_depth = settings.bit_depth;
|
||||
let pixel_format = match bit_depth {
|
||||
10 => 1, // PixelFormat::U10 (only when the codec supports it; the
|
||||
// dialog gates 10-bit behind HDR — codecs without 10-bit
|
||||
// stay at the 8-bit default and the dialog disables HDR).
|
||||
_ => 0, // PixelFormat::U8
|
||||
// dialog gates 10-bit behind HDR — codecs without 10-bit
|
||||
// stay at the 8-bit default and the dialog disables HDR).
|
||||
_ => 0, // PixelFormat::U8
|
||||
};
|
||||
Ok(oak_task::export::EncodingParams {
|
||||
filename: path.to_string_lossy().into_owned(),
|
||||
@@ -1394,7 +1424,8 @@ mod tests {
|
||||
fn project_with_clip(media: &std::path::Path) -> (ProjectRef, NodeId, NodeId) {
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Montage Test");
|
||||
let footage = graphops::import_footage(&project, media).expect("import the generated media");
|
||||
let footage =
|
||||
graphops::import_footage(&project, media).expect("import the generated media");
|
||||
graphops::add_track(&project, seq, TrackType::Video).expect("add a video track");
|
||||
graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the clip");
|
||||
@@ -1416,9 +1447,12 @@ mod tests {
|
||||
queue,
|
||||
oak_core::backend::BackendKind::Auto,
|
||||
);
|
||||
let mut frame =
|
||||
oak_render::eval::generate_frame(Rational::new(0, 1), (2, 1), oak_core::PixelFormat::F32)
|
||||
.unwrap();
|
||||
let mut frame = oak_render::eval::generate_frame(
|
||||
Rational::new(0, 1),
|
||||
(2, 1),
|
||||
oak_core::PixelFormat::F32,
|
||||
)
|
||||
.unwrap();
|
||||
for px in frame.data.chunks_exact_mut(16) {
|
||||
for (c, v) in px.chunks_exact_mut(4).zip([0.25f32, 0.5, 0.75, 1.0]) {
|
||||
c.copy_from_slice(&v.to_le_bytes());
|
||||
@@ -1446,7 +1480,13 @@ mod tests {
|
||||
// the display function takes the single explicit readback.
|
||||
let token = base.create_texture(2, 1).unwrap();
|
||||
base.upload(token, &frame).unwrap();
|
||||
let private = RenderedFrame::Gpu(Texture::gpu(base.clone(), token, 2, 1, oak_core::PixelFormat::F32));
|
||||
let private = RenderedFrame::Gpu(Texture::gpu(
|
||||
base.clone(),
|
||||
token,
|
||||
2,
|
||||
1,
|
||||
oak_core::PixelFormat::F32,
|
||||
));
|
||||
oak_core::backend::reset_gpu_transfer_counters();
|
||||
let displayed = private.to_display().expect("fallback display");
|
||||
assert!(displayed.2.is_some(), "the fallback hands CPU samples");
|
||||
@@ -1521,7 +1561,10 @@ mod tests {
|
||||
let montage = video_montage(&project, seq, at(0));
|
||||
assert_eq!(montage.len(), 1, "one clip covers frame 0");
|
||||
assert_eq!(montage[0].filename, media.to_string_lossy());
|
||||
assert!(video_montage(&project, seq, at(9)).len() == 1, "frame 9 is still covered");
|
||||
assert!(
|
||||
video_montage(&project, seq, at(9)).len() == 1,
|
||||
"frame 9 is still covered"
|
||||
);
|
||||
assert!(
|
||||
video_montage(&project, seq, at(10)).is_empty(),
|
||||
"frame 10 is past the clip's out point"
|
||||
@@ -1543,8 +1586,7 @@ mod tests {
|
||||
fn video_montage_stacks_highest_track_on_top() {
|
||||
let _media = media_lock();
|
||||
oak_undo::global::clear().unwrap();
|
||||
let red =
|
||||
std::env::temp_dir().join(format!("oakapp_stack_red_{}.mp4", std::process::id()));
|
||||
let red = std::env::temp_dir().join(format!("oakapp_stack_red_{}.mp4", std::process::id()));
|
||||
let blue =
|
||||
std::env::temp_dir().join(format!("oakapp_stack_blue_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip_solid(&red, 64, 64, 10, 10, [0.9, 0.1, 0.1, 1.0])
|
||||
@@ -1555,7 +1597,8 @@ mod tests {
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Stack Montage");
|
||||
let red_footage = graphops::import_footage(&project, &red).expect("import the red media");
|
||||
let blue_footage = graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
let blue_footage =
|
||||
graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
graphops::place_footage_clip(&project, seq, red_footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the V1 (red) clip");
|
||||
graphops::place_footage_clip(&project, seq, blue_footage, TrackType::Video, 1, 0, 10, 0)
|
||||
@@ -1566,7 +1609,11 @@ mod tests {
|
||||
// Bottom-to-top: V1's (red) clip first, V2's (blue) last.
|
||||
let montage = video_montage(&project, seq, time);
|
||||
assert_eq!(montage.len(), 2, "both clips cover frame 0");
|
||||
assert_eq!(montage[0].filename, red.to_string_lossy(), "V1's clip is the bottom of the stack");
|
||||
assert_eq!(
|
||||
montage[0].filename,
|
||||
red.to_string_lossy(),
|
||||
"V1's clip is the bottom of the stack"
|
||||
);
|
||||
assert_eq!(
|
||||
montage[1].filename,
|
||||
blue.to_string_lossy(),
|
||||
@@ -1595,7 +1642,10 @@ mod tests {
|
||||
let off = (8 * 64 + 8) * 16;
|
||||
let r = f32::from_le_bytes(dst[off..off + 4].try_into().unwrap());
|
||||
let b = f32::from_le_bytes(dst[off + 8..off + 12].try_into().unwrap());
|
||||
assert!(b > 0.5 && r < 0.4, "V2's blue covers V1's red (r={r}, b={b})");
|
||||
assert!(
|
||||
b > 0.5 && r < 0.4,
|
||||
"V2's blue covers V1's red (r={r}, b={b})"
|
||||
);
|
||||
|
||||
oak_undo::global::clear().unwrap();
|
||||
let _ = std::fs::remove_file(&red);
|
||||
@@ -1614,8 +1664,8 @@ mod tests {
|
||||
// legacy sRGB pass-through so the pixel-value assertions hold
|
||||
// regardless of the ACEScg default.
|
||||
oak_core::color::set_pipeline_color_settings(
|
||||
oak_core::colormath::WorkingColorSpace::SrgbLegacy,
|
||||
oak_core::colormath::OutputColorSpec::default(),
|
||||
oak_core::colormath::WorkingColorSpace::SrgbLegacy,
|
||||
oak_core::colormath::OutputColorSpec::default(),
|
||||
);
|
||||
oak_undo::global::clear().unwrap();
|
||||
let media =
|
||||
@@ -1624,10 +1674,12 @@ mod tests {
|
||||
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Effect Montage");
|
||||
let footage = graphops::import_footage(&project, &media).expect("import the generated media");
|
||||
let footage =
|
||||
graphops::import_footage(&project, &media).expect("import the generated media");
|
||||
graphops::add_track(&project, seq, TrackType::Video).expect("add a video track");
|
||||
let block = graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the clip");
|
||||
let block =
|
||||
graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the clip");
|
||||
|
||||
// The effect-library double-click flow: append a 50% Opacity to the
|
||||
// footage-backed clip (the insert rewires footage -> effect -> clip).
|
||||
@@ -1674,7 +1726,9 @@ mod tests {
|
||||
// A left-half pixel of the test pattern (known content, r ~= 0.9).
|
||||
let pixel = |frame: &[u8]| {
|
||||
let off = (8 * 64 + 8) * 16;
|
||||
[0, 1, 2, 3].map(|i| f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap()))
|
||||
[0, 1, 2, 3].map(|i| {
|
||||
f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap())
|
||||
})
|
||||
};
|
||||
|
||||
let with_fx = video_montage(&project, seq, time);
|
||||
@@ -1684,7 +1738,10 @@ mod tests {
|
||||
1,
|
||||
"the montage carries the clip's effect stack (not the footage source node)"
|
||||
);
|
||||
assert_eq!(with_fx[0].effects[0].type_id, "org.olivevideoeditor.Olive.opacity");
|
||||
assert_eq!(
|
||||
with_fx[0].effects[0].type_id,
|
||||
"org.olivevideoeditor.Olive.opacity"
|
||||
);
|
||||
assert!(with_fx[0].effects[0].enabled);
|
||||
|
||||
let plain = render(
|
||||
@@ -1699,7 +1756,11 @@ mod tests {
|
||||
);
|
||||
let effected = render(with_fx);
|
||||
let (p, e) = (pixel(&plain), pixel(&effected));
|
||||
assert!(p[0] > 0.6, "the plain render shows the test pattern (r={})", p[0]);
|
||||
assert!(
|
||||
p[0] > 0.6,
|
||||
"the plain render shows the test pattern (r={})",
|
||||
p[0]
|
||||
);
|
||||
// 50% opacity: the shader halves every channel, then the composite
|
||||
// over transparent black halves it again via the halved alpha.
|
||||
assert!(
|
||||
@@ -1746,7 +1807,8 @@ mod tests {
|
||||
) -> (NodeId, NodeId) {
|
||||
let (tb, in_r) = {
|
||||
let g = lock(p);
|
||||
let tb = graphops::sequence_time_base(&g.graph, seq).expect("the sequence has a timebase");
|
||||
let tb =
|
||||
graphops::sequence_time_base(&g.graph, seq).expect("the sequence has a timebase");
|
||||
(tb, graphops::ts_to_rational(in_ts, tb))
|
||||
};
|
||||
let block = {
|
||||
@@ -1824,7 +1886,8 @@ mod tests {
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Adjustment Montage");
|
||||
let red_footage = graphops::import_footage(&project, &red).expect("import the red media");
|
||||
let blue_footage = graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
let blue_footage =
|
||||
graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
graphops::place_footage_clip(&project, seq, red_footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the V1 (red) clip");
|
||||
graphops::place_footage_clip(&project, seq, blue_footage, TrackType::Video, 1, 0, 10, 0)
|
||||
@@ -1839,36 +1902,38 @@ mod tests {
|
||||
|
||||
let tb = graphops::sequence_time_base(&lock(&project).graph, seq).unwrap();
|
||||
let at = |frame: i64| graphops::ts_to_rational(frame, tb);
|
||||
let render = |montage: Vec<MontageClip>, adjustments: Vec<AdjustmentSpan>, at_time: Rational| {
|
||||
let params = VideoTicketParams {
|
||||
viewer: 0,
|
||||
project: String::new(),
|
||||
time: at_time,
|
||||
force_size: Some((64, 64)),
|
||||
force_format: Some(oak_core::PixelFormat::F32),
|
||||
cache: None,
|
||||
cache_dir: None,
|
||||
cache_id: None,
|
||||
cache_timebase: None,
|
||||
footage: None,
|
||||
montage,
|
||||
adjustments,
|
||||
let render =
|
||||
|montage: Vec<MontageClip>, adjustments: Vec<AdjustmentSpan>, at_time: Rational| {
|
||||
let params = VideoTicketParams {
|
||||
viewer: 0,
|
||||
project: String::new(),
|
||||
time: at_time,
|
||||
force_size: Some((64, 64)),
|
||||
force_format: Some(oak_core::PixelFormat::F32),
|
||||
cache: None,
|
||||
cache_dir: None,
|
||||
cache_id: None,
|
||||
cache_timebase: None,
|
||||
footage: None,
|
||||
montage,
|
||||
adjustments,
|
||||
};
|
||||
let mut dst = vec![0u8; 64 * 64 * 16];
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
params.time,
|
||||
¶ms,
|
||||
(64, 64),
|
||||
&mut dst,
|
||||
64 * 16,
|
||||
)
|
||||
.expect("montage render");
|
||||
dst
|
||||
};
|
||||
let mut dst = vec![0u8; 64 * 64 * 16];
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
params.time,
|
||||
¶ms,
|
||||
(64, 64),
|
||||
&mut dst,
|
||||
64 * 16,
|
||||
)
|
||||
.expect("montage render");
|
||||
dst
|
||||
};
|
||||
let pixel = |frame: &[u8]| {
|
||||
let off = (8 * 64 + 8) * 16;
|
||||
[0, 1, 2, 3]
|
||||
.map(|i| f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap()))
|
||||
[0, 1, 2, 3].map(|i| {
|
||||
f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap())
|
||||
})
|
||||
};
|
||||
|
||||
// Inside the layer's range the span rides the ticket, above the stack.
|
||||
@@ -1880,8 +1945,15 @@ mod tests {
|
||||
montage.len(),
|
||||
"the layer sits above every clip (it grades the whole composite)"
|
||||
);
|
||||
assert_eq!(spans[0].effects.len(), 1, "the layer's stack rides the span");
|
||||
assert_eq!(spans[0].effects[0].type_id, "org.olivevideoeditor.Olive.opacity");
|
||||
assert_eq!(
|
||||
spans[0].effects.len(),
|
||||
1,
|
||||
"the layer's stack rides the span"
|
||||
);
|
||||
assert_eq!(
|
||||
spans[0].effects[0].type_id,
|
||||
"org.olivevideoeditor.Olive.opacity"
|
||||
);
|
||||
|
||||
let baseline = render(montage.clone(), Vec::new(), at(5));
|
||||
let graded = render(montage.clone(), spans, at(5));
|
||||
@@ -1948,7 +2020,8 @@ mod tests {
|
||||
oak_core::colormath::OutputColorSpec::default(),
|
||||
);
|
||||
oak_undo::global::clear().unwrap();
|
||||
let red = std::env::temp_dir().join(format!("oakapp_adjlow_red_{}.mp4", std::process::id()));
|
||||
let red =
|
||||
std::env::temp_dir().join(format!("oakapp_adjlow_red_{}.mp4", std::process::id()));
|
||||
let blue =
|
||||
std::env::temp_dir().join(format!("oakapp_adjlow_blue_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip_solid(&red, 64, 64, 10, 10, [0.9, 0.1, 0.1, 1.0])
|
||||
@@ -1959,7 +2032,8 @@ mod tests {
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Mid Adjustment Montage");
|
||||
let red_footage = graphops::import_footage(&project, &red).expect("import the red media");
|
||||
let blue_footage = graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
let blue_footage =
|
||||
graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
graphops::place_footage_clip(&project, seq, red_footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the V1 (red) clip");
|
||||
graphops::add_track(&project, seq, TrackType::Video).expect("add the adjustment track");
|
||||
@@ -1970,9 +2044,17 @@ mod tests {
|
||||
let (_adj, _fx) = adjustment_layer_with_opacity(&project, seq, adj_track, 0, 5, 0.5);
|
||||
// V3's clip carries its own 50% Opacity so the composite keeps both
|
||||
// contributions visible (an opaque top clip would hide the grading).
|
||||
let blue_clip =
|
||||
graphops::place_footage_clip(&project, seq, blue_footage, TrackType::Video, 2, 0, 10, 0)
|
||||
.expect("place the V3 (blue) clip");
|
||||
let blue_clip = graphops::place_footage_clip(
|
||||
&project,
|
||||
seq,
|
||||
blue_footage,
|
||||
TrackType::Video,
|
||||
2,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
)
|
||||
.expect("place the V3 (blue) clip");
|
||||
let clip_fx = crate::oakui::effectchain::insert(
|
||||
&project,
|
||||
blue_clip,
|
||||
@@ -1990,36 +2072,38 @@ mod tests {
|
||||
|
||||
let tb = graphops::sequence_time_base(&lock(&project).graph, seq).unwrap();
|
||||
let time = graphops::ts_to_rational(0, tb);
|
||||
let render = |montage: Vec<MontageClip>, adjustments: Vec<AdjustmentSpan>, at_time: Rational| {
|
||||
let params = VideoTicketParams {
|
||||
viewer: 0,
|
||||
project: String::new(),
|
||||
time: at_time,
|
||||
force_size: Some((64, 64)),
|
||||
force_format: Some(oak_core::PixelFormat::F32),
|
||||
cache: None,
|
||||
cache_dir: None,
|
||||
cache_id: None,
|
||||
cache_timebase: None,
|
||||
footage: None,
|
||||
montage,
|
||||
adjustments,
|
||||
let render =
|
||||
|montage: Vec<MontageClip>, adjustments: Vec<AdjustmentSpan>, at_time: Rational| {
|
||||
let params = VideoTicketParams {
|
||||
viewer: 0,
|
||||
project: String::new(),
|
||||
time: at_time,
|
||||
force_size: Some((64, 64)),
|
||||
force_format: Some(oak_core::PixelFormat::F32),
|
||||
cache: None,
|
||||
cache_dir: None,
|
||||
cache_id: None,
|
||||
cache_timebase: None,
|
||||
footage: None,
|
||||
montage,
|
||||
adjustments,
|
||||
};
|
||||
let mut dst = vec![0u8; 64 * 64 * 16];
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
at_time,
|
||||
¶ms,
|
||||
(64, 64),
|
||||
&mut dst,
|
||||
64 * 16,
|
||||
)
|
||||
.expect("montage render");
|
||||
dst
|
||||
};
|
||||
let mut dst = vec![0u8; 64 * 64 * 16];
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
at_time,
|
||||
¶ms,
|
||||
(64, 64),
|
||||
&mut dst,
|
||||
64 * 16,
|
||||
)
|
||||
.expect("montage render");
|
||||
dst
|
||||
};
|
||||
let pixel = |frame: &[u8]| {
|
||||
let off = (8 * 64 + 8) * 16;
|
||||
[0, 1, 2, 3]
|
||||
.map(|i| f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap()))
|
||||
[0, 1, 2, 3].map(|i| {
|
||||
f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap())
|
||||
})
|
||||
};
|
||||
|
||||
let (montage, spans) = video_montage_with_adjustments(&project, seq, time);
|
||||
@@ -2027,10 +2111,17 @@ mod tests {
|
||||
// V1's red then V3's blue, and the boundary sits between them.
|
||||
assert_eq!(montage.len(), 2, "the two clips cover frame 0");
|
||||
assert!(montage[0].effects.is_empty(), "the red clip has no stack");
|
||||
assert_eq!(montage[1].effects.len(), 1, "the blue clip's own stack rides it");
|
||||
assert_eq!(
|
||||
montage[1].effects.len(),
|
||||
1,
|
||||
"the blue clip's own stack rides it"
|
||||
);
|
||||
assert_eq!(spans.len(), 1, "the layer emits one span");
|
||||
assert_eq!(spans[0].track_index, 1, "the layer grades only V1's clip");
|
||||
assert!(!spans[0].effects.is_empty(), "the layer carries its opacity");
|
||||
assert!(
|
||||
!spans[0].effects.is_empty(),
|
||||
"the layer carries its opacity"
|
||||
);
|
||||
|
||||
let baseline = pixel(&render(montage.clone(), Vec::new(), time));
|
||||
let graded = pixel(&render(montage.clone(), spans.clone(), time));
|
||||
@@ -2082,8 +2173,7 @@ mod tests {
|
||||
#[test]
|
||||
fn clip_multicam_media_resolves_the_current_angle() {
|
||||
let _media = media_lock();
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oakapp_mcmedia_{}.mp4", std::process::id()));
|
||||
let media = std::env::temp_dir().join(format!("oakapp_mcmedia_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate test media");
|
||||
|
||||
let (project, seq, footage) = project_with_clip(&media);
|
||||
@@ -2114,19 +2204,20 @@ mod tests {
|
||||
-1,
|
||||
oak_node::value::NodeValue::Combo(0),
|
||||
);
|
||||
}
|
||||
}
|
||||
g.graph
|
||||
.connect(source_seq, id, oak_node::nodes::multicamnode::SEQUENCE_INPUT, -1)
|
||||
.connect(
|
||||
source_seq,
|
||||
id,
|
||||
oak_node::nodes::multicamnode::SEQUENCE_INPUT,
|
||||
-1,
|
||||
)
|
||||
.unwrap();
|
||||
// The angle CLIP feeds the source array at element 0 (the
|
||||
// traverser's `active_elements_at_time` keeps only the current
|
||||
// source, so multi `value()` forwards exactly it).
|
||||
g.graph
|
||||
.input_array_insert(
|
||||
id,
|
||||
oak_node::nodes::multicamnode::SOURCES_INPUT,
|
||||
0,
|
||||
)
|
||||
.input_array_insert(id, oak_node::nodes::multicamnode::SOURCES_INPUT, 0)
|
||||
.unwrap();
|
||||
g.graph
|
||||
.connect(
|
||||
@@ -2140,15 +2231,24 @@ mod tests {
|
||||
};
|
||||
// A host clip on the host sequence (default layout V1) fed from the
|
||||
// multicam output; the clip's own input must first be connected.
|
||||
let host_clip = graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the host clip");
|
||||
let host_clip =
|
||||
graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the host clip");
|
||||
{
|
||||
let mut g = graphops::lock(&project);
|
||||
let _ = g
|
||||
.graph
|
||||
.disconnect(footage, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1);
|
||||
g.graph.disconnect(
|
||||
footage,
|
||||
host_clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
-1,
|
||||
);
|
||||
g.graph
|
||||
.connect(mc, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1)
|
||||
.connect(
|
||||
mc,
|
||||
host_clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
-1,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
// The montage at host time 0 resolves to the angle's media.
|
||||
@@ -2263,8 +2363,9 @@ mod tests {
|
||||
.copied()
|
||||
.expect("red clip")
|
||||
};
|
||||
let blue_clip = graphops::place_footage_clip(&project, seq, bfoot, TrackType::Video, 1, 0, 10, 0)
|
||||
.expect("blue angle slot");
|
||||
let blue_clip =
|
||||
graphops::place_footage_clip(&project, seq, bfoot, TrackType::Video, 1, 0, 10, 0)
|
||||
.expect("blue angle slot");
|
||||
let mc = {
|
||||
let mut g = graphops::lock(&project);
|
||||
let (core, behavior) = oak_node::nodes::multicamnode::create();
|
||||
@@ -2280,13 +2381,23 @@ mod tests {
|
||||
.input_array_insert(id, oak_node::nodes::multicamnode::SOURCES_INPUT, 0)
|
||||
.unwrap();
|
||||
g.graph
|
||||
.connect(red_clip, id, oak_node::nodes::multicamnode::SOURCES_INPUT, 0)
|
||||
.connect(
|
||||
red_clip,
|
||||
id,
|
||||
oak_node::nodes::multicamnode::SOURCES_INPUT,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
g.graph
|
||||
.input_array_insert(id, oak_node::nodes::multicamnode::SOURCES_INPUT, 1)
|
||||
.unwrap();
|
||||
g.graph
|
||||
.connect(blue_clip, id, oak_node::nodes::multicamnode::SOURCES_INPUT, 1)
|
||||
.connect(
|
||||
blue_clip,
|
||||
id,
|
||||
oak_node::nodes::multicamnode::SOURCES_INPUT,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
id
|
||||
};
|
||||
@@ -2296,14 +2407,19 @@ mod tests {
|
||||
.expect("host clip");
|
||||
{
|
||||
let mut g = graphops::lock(&project);
|
||||
let _ = g.graph.disconnect(
|
||||
g.graph.disconnect(
|
||||
_rfoot,
|
||||
host_clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
-1,
|
||||
);
|
||||
g.graph
|
||||
.connect(mc, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1)
|
||||
.connect(
|
||||
mc,
|
||||
host_clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
-1,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -2332,7 +2448,10 @@ mod tests {
|
||||
};
|
||||
// Source 0 = red angle.
|
||||
let (r0, b0) = render_rgb(0);
|
||||
assert!(r0 > 0.4 && b0 < 0.4, "source 0 is the red angle (r={r0} b={b0})");
|
||||
assert!(
|
||||
r0 > 0.4 && b0 < 0.4,
|
||||
"source 0 is the red angle (r={r0} b={b0})"
|
||||
);
|
||||
|
||||
// Switch to source 1: current_in drives the element the multi
|
||||
// `value()` forwards — the frame must turn blue.
|
||||
@@ -2490,7 +2609,8 @@ mod tests {
|
||||
#[test]
|
||||
fn audio_montage_overlaps_the_range() {
|
||||
let _media = media_lock();
|
||||
let media = std::env::temp_dir().join(format!("oakapp_montage_a_{}.mp4", std::process::id()));
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oakapp_montage_a_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate test media");
|
||||
|
||||
let (project, seq, footage) = project_with_clip(&media);
|
||||
@@ -2502,9 +2622,15 @@ mod tests {
|
||||
|
||||
let overlapping = audio_montage(&project, seq, TimeRange::new(at(0), at(5)));
|
||||
assert_eq!(overlapping.len(), 1, "the range overlaps the audio clip");
|
||||
assert_eq!(overlapping[0].stream_index, 1, "the audio stream is selected");
|
||||
assert_eq!(
|
||||
overlapping[0].stream_index, 1,
|
||||
"the audio stream is selected"
|
||||
);
|
||||
let disjoint = audio_montage(&project, seq, TimeRange::new(at(10), at(20)));
|
||||
assert!(disjoint.is_empty(), "a range past the clip overlaps nothing");
|
||||
assert!(
|
||||
disjoint.is_empty(),
|
||||
"a range past the clip overlaps nothing"
|
||||
);
|
||||
oak_undo::global::clear().unwrap();
|
||||
let _ = std::fs::remove_file(&media);
|
||||
}
|
||||
@@ -2515,7 +2641,8 @@ mod tests {
|
||||
#[test]
|
||||
fn montages_skip_muted_tracks() {
|
||||
let _media = media_lock();
|
||||
let media = std::env::temp_dir().join(format!("oakapp_montage_m_{}.mp4", std::process::id()));
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oakapp_montage_m_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate test media");
|
||||
|
||||
let (project, seq, footage) = project_with_clip(&media);
|
||||
@@ -2548,8 +2675,16 @@ mod tests {
|
||||
|
||||
oak_undo::global::undo().unwrap();
|
||||
oak_undo::global::undo().unwrap();
|
||||
assert_eq!(video_montage(&project, seq, at(0)).len(), 1, "undo restores the video clip");
|
||||
assert_eq!(audio_montage(&project, seq, range).len(), 1, "undo restores the audio clip");
|
||||
assert_eq!(
|
||||
video_montage(&project, seq, at(0)).len(),
|
||||
1,
|
||||
"undo restores the video clip"
|
||||
);
|
||||
assert_eq!(
|
||||
audio_montage(&project, seq, range).len(),
|
||||
1,
|
||||
"undo restores the audio clip"
|
||||
);
|
||||
oak_undo::global::clear().unwrap();
|
||||
let _ = std::fs::remove_file(&media);
|
||||
}
|
||||
@@ -2569,7 +2704,11 @@ mod tests {
|
||||
// underflows the renderable minimum.
|
||||
assert_eq!(full_res_render_size(0, 0, 1), (2, 2));
|
||||
assert_eq!(full_res_render_size(1, 1, 8), (2, 2));
|
||||
assert_eq!(full_res_render_size(1920, 1080, 0), (1920, 1080), "a zero divider means Full");
|
||||
assert_eq!(
|
||||
full_res_render_size(1920, 1080, 0),
|
||||
(1920, 1080),
|
||||
"a zero divider means Full"
|
||||
);
|
||||
}
|
||||
|
||||
/// Clamping only shrinks: a render that already fits a limit is left
|
||||
@@ -2583,7 +2722,11 @@ mod tests {
|
||||
(960, 540),
|
||||
"a smaller request is not upscaled to the proxy"
|
||||
);
|
||||
assert_eq!(clamp_render_size(1920, 1080, &[]), (1920, 1080), "no limits = no clamping");
|
||||
assert_eq!(
|
||||
clamp_render_size(1920, 1080, &[]),
|
||||
(1920, 1080),
|
||||
"no limits = no clamping"
|
||||
);
|
||||
assert_eq!(
|
||||
clamp_render_size(1920, 1080, &[(640, 640)]),
|
||||
(640, 360),
|
||||
@@ -2609,7 +2752,8 @@ mod tests {
|
||||
std::env::temp_dir().join(format!("oakapp_proxy_src_{}.mp4", std::process::id()));
|
||||
let proxy =
|
||||
std::env::temp_dir().join(format!("oakapp_proxy_32_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&orig, 64, 64, 10, 10).expect("generate the source media");
|
||||
oak_codec::testmedia::write_test_clip(&orig, 64, 64, 10, 10)
|
||||
.expect("generate the source media");
|
||||
oak_codec::testmedia::write_test_clip_solid(&proxy, 32, 32, 10, 10, [0.1, 0.1, 0.9, 1.0])
|
||||
.expect("generate the proxy media");
|
||||
|
||||
@@ -2626,7 +2770,11 @@ mod tests {
|
||||
let f = g
|
||||
.graph
|
||||
.get_mut(footage)
|
||||
.and_then(|e| e.behavior.as_any_mut()?.downcast_mut::<oak_node::footage::FootageBehavior>())
|
||||
.and_then(|e| {
|
||||
e.behavior
|
||||
.as_any_mut()?
|
||||
.downcast_mut::<oak_node::footage::FootageBehavior>()
|
||||
})
|
||||
.expect("the footage behavior");
|
||||
f.proxy = proxy.to_string_lossy().into_owned();
|
||||
f.proxy_enabled = true;
|
||||
@@ -2644,22 +2792,39 @@ mod tests {
|
||||
let f = graphops::footage_behavior(&g.graph, footage).expect("the footage behavior");
|
||||
preview_footage_media(f, true).0
|
||||
};
|
||||
assert_eq!(selected, proxy.to_string_lossy(), "the proxy stands in for the source");
|
||||
assert_eq!(
|
||||
selected,
|
||||
proxy.to_string_lossy(),
|
||||
"the proxy stands in for the source"
|
||||
);
|
||||
|
||||
// …and the sequence frame ticket is clamped to its 32x32 size.
|
||||
let tb = graphops::sequence_time_base(&lock(&project).graph, seq).unwrap();
|
||||
let params =
|
||||
sequence_frame_params(&project, seq, 0, tb, 64, 64, None).expect("sequence frame params");
|
||||
assert_eq!(params.force_size, Some((32, 32)), "the proxy bounds the render size");
|
||||
let params = sequence_frame_params(&project, seq, 0, tb, 64, 64, None)
|
||||
.expect("sequence frame params");
|
||||
assert_eq!(
|
||||
params.force_size,
|
||||
Some((32, 32)),
|
||||
"the proxy bounds the render size"
|
||||
);
|
||||
|
||||
// The decoded pixels are the proxy's (solid blue), not the source's.
|
||||
let mut dst = vec![0u8; 32 * 32 * 16];
|
||||
oak_render::eval::render_montage_frame_into(params.time, ¶ms, (32, 32), &mut dst, 32 * 16)
|
||||
.expect("montage render at the proxy size");
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
params.time,
|
||||
¶ms,
|
||||
(32, 32),
|
||||
&mut dst,
|
||||
32 * 16,
|
||||
)
|
||||
.expect("montage render at the proxy size");
|
||||
let off = (8 * 32 + 8) * 16;
|
||||
let r = f32::from_le_bytes(dst[off..off + 4].try_into().unwrap());
|
||||
let b = f32::from_le_bytes(dst[off + 8..off + 12].try_into().unwrap());
|
||||
assert!(b > 0.5 && r < 0.4, "the proxy's blue covers the frame (r={r}, b={b})");
|
||||
assert!(
|
||||
b > 0.5 && r < 0.4,
|
||||
"the proxy's blue covers the frame (r={r}, b={b})"
|
||||
);
|
||||
|
||||
store.set(None, "UseProxyMedia", &old);
|
||||
oak_undo::global::clear().unwrap();
|
||||
|
||||
@@ -97,7 +97,7 @@ pub(crate) fn analyze_bgra8(width: u32, height: u32, bytes: &[u8]) -> ScopeData
|
||||
let mut luma = Vec::with_capacity(pixels);
|
||||
let mut chroma = Vec::with_capacity(pixels);
|
||||
let src = bytes.get(..pixels * 4).unwrap_or_default();
|
||||
for px in src.chunks_exact(4) {
|
||||
for px in src.as_chunks::<4>().0 {
|
||||
let b = f32::from(px[0]) / 255.0;
|
||||
let g = f32::from(px[1]) / 255.0;
|
||||
let r = f32::from(px[2]) / 255.0;
|
||||
|
||||
@@ -90,7 +90,11 @@ impl WaveformCache {
|
||||
|
||||
/// The cached waveform of `clip`, if extracted.
|
||||
pub fn get(&self, clip: u64) -> Option<Arc<ClipWaveform>> {
|
||||
self.map.lock().unwrap_or_else(|e| e.into_inner()).get(&clip).cloned()
|
||||
self.map
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.get(&clip)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Ensure `clip` (with media `filename`, `duration_frames` long) has a
|
||||
@@ -105,16 +109,17 @@ impl WaveformCache {
|
||||
return;
|
||||
};
|
||||
let mut map = self.map.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !map.contains_key(&clip) {
|
||||
map.insert(clip, Arc::new(waveform));
|
||||
self.version.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = map.entry(clip) {
|
||||
e.insert(Arc::new(waveform));
|
||||
self.version
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a clip's waveform (first channel) via oakaudio's waveform
|
||||
/// extractor.
|
||||
fn extract(filename: &str, duration_frames: i64, _fps: f32) -> Option<ClipWaveform> {
|
||||
fn extract(filename: &str, duration_frames: i64, _fps: f32) -> Option<ClipWaveform> {
|
||||
let cname = std::ffi::CString::new(filename).ok()?;
|
||||
const SAMPLES_PER_POINT: i32 = 256;
|
||||
// Audio-stream index 0 (probe numbering).
|
||||
|
||||
@@ -106,7 +106,7 @@ mod tests {
|
||||
let peaks: Vec<MinMax> = (0..points)
|
||||
.map(|i| {
|
||||
let sample = i64::from(i) * i64::from(SPP);
|
||||
let inside = sample >= 48_000 && sample < 96_000;
|
||||
let inside = (48_000..96_000).contains(&sample);
|
||||
if inside {
|
||||
MinMax { min: -1.0, max: 1.0 }
|
||||
} else {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::oakui::component::text_input;
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::dock::{DockPanel, PanelEvent};
|
||||
use gpui::{
|
||||
@@ -30,7 +31,6 @@ use gpui::{
|
||||
Render, SharedString, Window,
|
||||
};
|
||||
use gpui_elements::editable_text::{EditableTextState, StringStorage, TextChanged};
|
||||
use crate::oakui::component::text_input;
|
||||
|
||||
use crate::i18n;
|
||||
use crate::oakui::effectchain::group_label;
|
||||
@@ -60,7 +60,8 @@ impl<E: AppEngine> EffectLibraryPanel<E> {
|
||||
pub fn new(engine: Entity<E>, _window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
// Re-read the effect table whenever the engine notifies (the table
|
||||
// itself is static, but the selection hint depends on the target).
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach();
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify())
|
||||
.detach();
|
||||
let search = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
cx.subscribe(&search, |_this, _state, _event: &TextChanged, cx| {
|
||||
cx.notify();
|
||||
@@ -212,7 +213,7 @@ impl<E: AppEngine> Render for EffectLibraryPanel<E> {
|
||||
list = list.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("effect-library-{type_id}")))
|
||||
.debug_selector(move || format!("effect-library-row-{row_id}").into())
|
||||
.debug_selector(move || format!("effect-library-row-{row_id}"))
|
||||
.cursor_pointer()
|
||||
.px_2()
|
||||
.py_1()
|
||||
@@ -296,7 +297,9 @@ fn group_header(
|
||||
on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.id(SharedString::from(format!("effect-library-group-{group_key}")))
|
||||
.id(SharedString::from(format!(
|
||||
"effect-library-group-{group_key}"
|
||||
)))
|
||||
.debug_selector({
|
||||
let key = group_key.to_string();
|
||||
move || format!("effect-library-group-row-{key}")
|
||||
@@ -397,7 +400,10 @@ mod tests {
|
||||
let mut collapsed = HashSet::new();
|
||||
|
||||
assert!(!hidden_by_collapse(&color, "", &collapsed));
|
||||
assert!(toggle_collapsed(&mut collapsed, "color"), "first toggle collapses");
|
||||
assert!(
|
||||
toggle_collapsed(&mut collapsed, "color"),
|
||||
"first toggle collapses"
|
||||
);
|
||||
assert!(hidden_by_collapse(&color, "", &collapsed));
|
||||
assert!(
|
||||
!hidden_by_collapse(&color, "blur", &collapsed),
|
||||
@@ -407,7 +413,10 @@ mod tests {
|
||||
!hidden_by_collapse(&color, "", &HashSet::new()),
|
||||
"an unlisted group stays open"
|
||||
);
|
||||
assert!(!toggle_collapsed(&mut collapsed, "color"), "second toggle expands");
|
||||
assert!(
|
||||
!toggle_collapsed(&mut collapsed, "color"),
|
||||
"second toggle expands"
|
||||
);
|
||||
assert!(collapsed.is_empty());
|
||||
}
|
||||
|
||||
@@ -440,9 +449,14 @@ mod tests {
|
||||
#[test]
|
||||
fn collapsed_state_persists_in_the_config() {
|
||||
let _guard = collapsed_config_lock();
|
||||
assert!(load_collapsed().is_empty(), "the lock starts from a clear key");
|
||||
let collapsed: HashSet<String> =
|
||||
["distort", "general"].iter().map(|k| k.to_string()).collect();
|
||||
assert!(
|
||||
load_collapsed().is_empty(),
|
||||
"the lock starts from a clear key"
|
||||
);
|
||||
let collapsed: HashSet<String> = ["distort", "general"]
|
||||
.iter()
|
||||
.map(|k| k.to_string())
|
||||
.collect();
|
||||
store_collapsed(&collapsed);
|
||||
assert_eq!(load_collapsed(), collapsed);
|
||||
store_collapsed(&HashSet::new());
|
||||
@@ -456,7 +470,7 @@ mod tests {
|
||||
let _guard = collapsed_config_lock();
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
EffectLibraryPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
@@ -492,7 +506,7 @@ mod tests {
|
||||
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
let mut panel = EffectLibraryPanel::new(engine, window, cx);
|
||||
// The state the toggle writes (and the config reloads on the
|
||||
// next start), without touching the process-global key.
|
||||
@@ -546,7 +560,7 @@ mod tests {
|
||||
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
EffectLibraryPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -268,7 +268,7 @@ mod tests {
|
||||
) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(320.0), px(400.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
HistoryPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
//! 输出) with add / remove / reorder — no engine, the mock applies the edits
|
||||
//! to its own model.
|
||||
|
||||
use crate::oakui::component::menu::{Menu, MenuItem};
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::dock::{DockPanel, PanelEvent};
|
||||
use gpui::effect_stack::{EffectCardKind, EffectId, EffectStackEvent, EffectStackView};
|
||||
@@ -26,7 +27,6 @@ use gpui::{
|
||||
div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, MouseButton, Render,
|
||||
SharedString, Window,
|
||||
};
|
||||
use crate::oakui::component::menu::{Menu, MenuItem};
|
||||
|
||||
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
|
||||
use crate::oakui::effectchain::group_label;
|
||||
@@ -57,15 +57,13 @@ impl<E: AppEngine> InspectorPanel<E> {
|
||||
pub fn new(engine: Entity<E>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let stack = cx.new(|cx| {
|
||||
let engine_for_params = engine.clone();
|
||||
EffectStackView::new(engine.clone(), cx).params_renderer(
|
||||
move |effect, window, cx| {
|
||||
// The OFX parameter view: auto-generated controls for the
|
||||
// effect's inputs (empty state for effects without a
|
||||
// parameter UI).
|
||||
cx.new(|cx| OfxParamsView::new(*effect, engine_for_params.clone(), window, cx))
|
||||
.into()
|
||||
},
|
||||
)
|
||||
EffectStackView::new(engine.clone(), cx).params_renderer(move |effect, window, cx| {
|
||||
// The OFX parameter view: auto-generated controls for the
|
||||
// effect's inputs (empty state for effects without a
|
||||
// parameter UI).
|
||||
cx.new(|cx| OfxParamsView::new(*effect, engine_for_params.clone(), window, cx))
|
||||
.into()
|
||||
})
|
||||
});
|
||||
// The "edits are requests" loop: forward each request to the engine,
|
||||
// which applies it to its model and notifies. `AddRequested` carries
|
||||
@@ -98,7 +96,8 @@ impl<E: AppEngine> InspectorPanel<E> {
|
||||
// The stack re-renders whenever the engine changes (selection-driven
|
||||
// highlight, effect edits, project drops) — the stack itself does not
|
||||
// subscribe to the engine entity.
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach();
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
let context_menu = ContextMenuHandle::new(Self::on_local_menu_item, window, cx);
|
||||
|
||||
@@ -174,7 +173,7 @@ impl<E: AppEngine> InspectorPanel<E> {
|
||||
list = list.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("add-effect-group-{key}")))
|
||||
.debug_selector(move || format!("add-effect-group-row-{key}").into())
|
||||
.debug_selector(move || format!("add-effect-group-row-{key}"))
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
.px_2()
|
||||
|
||||
@@ -538,7 +538,7 @@ mod tests {
|
||||
async fn panel_renders_the_mock_grid(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(640.0), px(360.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx));
|
||||
let engine = cx.new(crate::oakui::MockEngine::demo);
|
||||
let clock = engine.read(cx).program_clock().clone();
|
||||
MulticamPanel::new(engine, clock, window, cx)
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::oakui::component::menu::{Menu, MenuItem};
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::dock::{DockPanel, PanelEvent};
|
||||
use gpui::node_graph::{
|
||||
@@ -34,13 +35,12 @@ use gpui::node_graph::{
|
||||
NodeVisualState, MAX_ZOOM, MIN_ZOOM,
|
||||
};
|
||||
use gpui::{
|
||||
div, point, prelude::*, px, AnyElement, App, Bounds, ClickEvent, Context, Entity,
|
||||
EventEmitter, MouseButton, Pixels, Point, Render, SharedString, Window,
|
||||
div, point, prelude::*, px, AnyElement, App, Bounds, ClickEvent, Context, Entity, EventEmitter,
|
||||
MouseButton, Pixels, Point, Render, SharedString, Window,
|
||||
};
|
||||
use crate::oakui::component::menu::{Menu, MenuItem};
|
||||
|
||||
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
|
||||
use crate::oakui::component::menu;
|
||||
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
|
||||
use crate::oakui::{AppEngine, NodeLibraryEntry};
|
||||
use crate::panels::commands::PanelCommandHandler;
|
||||
use crate::panels::ids::NODE_EDITOR;
|
||||
@@ -203,8 +203,9 @@ impl<E: AppEngine> NodeEditorPanel<E> {
|
||||
let Some(node) = node else {
|
||||
return;
|
||||
};
|
||||
self.graph
|
||||
.update(cx, |graph, cx| graph.set_selection(BTreeSet::from([NodeId(node)]), cx));
|
||||
self.graph.update(cx, |graph, cx| {
|
||||
graph.set_selection(BTreeSet::from([NodeId(node)]), cx)
|
||||
});
|
||||
}
|
||||
|
||||
/// Handles the node editor's local (non-registry) context-menu items.
|
||||
@@ -221,9 +222,10 @@ impl<E: AppEngine> NodeEditorPanel<E> {
|
||||
.map(|entry| entry.1.clone());
|
||||
if let Some(type_id) = type_id {
|
||||
let position = self.add_node_position.unwrap_or_default();
|
||||
if let Err(err) = self.engine.update(cx, |engine, cx| {
|
||||
engine.add_node_at(&type_id, position, cx)
|
||||
}) {
|
||||
if let Err(err) = self
|
||||
.engine
|
||||
.update(cx, |engine, cx| engine.add_node_at(&type_id, position, cx))
|
||||
{
|
||||
println!("[node editor] add node failed: {err}");
|
||||
}
|
||||
}
|
||||
@@ -394,8 +396,7 @@ impl<E: AppEngine> Render for NodeEditorPanel<E> {
|
||||
.graph_position_at(window.mouse_position());
|
||||
let type_id = payload.type_id.to_string();
|
||||
this.engine.update(cx, |engine, cx| {
|
||||
if let Err(err) = engine.add_node_at(&type_id, graph_position, cx)
|
||||
{
|
||||
if let Err(err) = engine.add_node_at(&type_id, graph_position, cx) {
|
||||
println!("[node editor] add node failed: {err}");
|
||||
}
|
||||
});
|
||||
@@ -508,12 +509,18 @@ pub(crate) fn node_menu(protected: bool) -> Menu {
|
||||
items.push(MenuItem::new(LOCAL_GROUP, tr("node.context.group")));
|
||||
items.push(MenuItem::new(LOCAL_UNGROUP, tr("node.context.ungroup")));
|
||||
items.push(menu::color_label_item(None).separated());
|
||||
items.push(MenuItem::new(LOCAL_OPEN_IN_VIEWER, tr("node.context.open_in_viewer")));
|
||||
items.push(MenuItem::new(
|
||||
LOCAL_OPEN_IN_VIEWER,
|
||||
tr("node.context.open_in_viewer"),
|
||||
));
|
||||
items.push(MenuItem::new(
|
||||
LOCAL_SHOW_IN_PARAM_EDITOR,
|
||||
tr("node.context.show_in_param_editor"),
|
||||
));
|
||||
items.push(MenuItem::new(LOCAL_NODE_PROPERTIES, tr("menu.context.properties")));
|
||||
items.push(MenuItem::new(
|
||||
LOCAL_NODE_PROPERTIES,
|
||||
tr("menu.context.properties"),
|
||||
));
|
||||
Menu::new(items)
|
||||
}
|
||||
|
||||
@@ -538,27 +545,21 @@ pub(crate) fn background_menu(
|
||||
let mut add_items: Vec<MenuItem> = Vec::new();
|
||||
let mut next_id = LOCAL_ADD_NODE_BASE;
|
||||
for (category_key, mut entries) in groups {
|
||||
entries.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
entries.sort_by_key(|a| a.name.to_lowercase());
|
||||
let mut submenu = Vec::with_capacity(entries.len());
|
||||
for entry in entries {
|
||||
submenu.push(MenuItem::new(next_id, entry.name.clone()));
|
||||
add_menu_ids.push((next_id, entry.type_id));
|
||||
next_id += 1;
|
||||
}
|
||||
add_items.push(
|
||||
MenuItem::new(0, tr(category_key)).with_submenu(Menu::new(submenu)),
|
||||
);
|
||||
add_items.push(MenuItem::new(0, tr(category_key)).with_submenu(Menu::new(submenu)));
|
||||
}
|
||||
|
||||
let direction_menu = Menu::new(vec![
|
||||
MenuItem::new(LOCAL_DIR_TOP_BOTTOM, tr("node.context.dir_top_bottom"))
|
||||
.with_checked(true),
|
||||
MenuItem::new(LOCAL_DIR_BOTTOM_TOP, tr("node.context.dir_bottom_top"))
|
||||
.with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_LEFT_RIGHT, tr("node.context.dir_left_right"))
|
||||
.with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_RIGHT_LEFT, tr("node.context.dir_right_left"))
|
||||
.with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_TOP_BOTTOM, tr("node.context.dir_top_bottom")).with_checked(true),
|
||||
MenuItem::new(LOCAL_DIR_BOTTOM_TOP, tr("node.context.dir_bottom_top")).with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_LEFT_RIGHT, tr("node.context.dir_left_right")).with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_RIGHT_LEFT, tr("node.context.dir_right_left")).with_checked(false),
|
||||
]);
|
||||
|
||||
Menu::new(vec![
|
||||
@@ -592,7 +593,7 @@ mod tests {
|
||||
) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(640.0), px(480.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx));
|
||||
let engine = cx.new(crate::oakui::MockEngine::demo);
|
||||
NodeEditorPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
@@ -638,7 +639,9 @@ mod tests {
|
||||
fn node_menu_carries_grouping_and_reveals() {
|
||||
// The color-label lookup below matches on a localized label: pin
|
||||
// en-US under the shared language lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = node_menu(false);
|
||||
let ids: Vec<usize> = menu.items.iter().map(|item| item.id).collect();
|
||||
@@ -761,7 +764,12 @@ mod tests {
|
||||
cx.update(|_window, app| {
|
||||
let engine = panel.read(app).engine.clone();
|
||||
engine.update(app, |engine, cx| {
|
||||
engine.apply_effect_event(&EffectStackEvent::CardSelected { effect: EffectId(1) }, cx);
|
||||
engine.apply_effect_event(
|
||||
&EffectStackEvent::CardSelected {
|
||||
effect: EffectId(1),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -138,45 +138,55 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
// Route every transport request to the engine's program monitor, and
|
||||
// forward the picture's pointer/key events to the active OFX interact
|
||||
// (no-op when none is live).
|
||||
cx.subscribe(&viewer, |this, _viewer, event: &ViewerEvent, cx| match event {
|
||||
ViewerEvent::InteractPointer {
|
||||
kind,
|
||||
position,
|
||||
button,
|
||||
pressed,
|
||||
} => this.forward_interact_pointer(*kind, *position, *button, *pressed, cx),
|
||||
ViewerEvent::InteractKey { down, keystroke } => {
|
||||
this.forward_interact_key(*down, keystroke, cx)
|
||||
}
|
||||
// The widget already toggled its own overlay state; nothing to
|
||||
// forward to the engine.
|
||||
ViewerEvent::ToggleSafeFramesRequested { .. } | ViewerEvent::ToggleZoomRequested { .. } => {}
|
||||
// The loop in/out range is the shell-owned program workarea, so
|
||||
// the panel re-emits the request.
|
||||
ViewerEvent::InPointRequested { .. } => cx.emit(menu::ViewerPanelEvent::SetInPoint),
|
||||
ViewerEvent::OutPointRequested { .. } => cx.emit(menu::ViewerPanelEvent::SetOutPoint),
|
||||
ViewerEvent::ClearRangeRequested { .. } => cx.emit(menu::ViewerPanelEvent::ClearRange),
|
||||
// The OFX color picker armed the eyedropper and the user clicked
|
||||
// the frame: hand the sampled colour back to the engine's mailbox.
|
||||
ViewerEvent::EyedropperPick { color } => {
|
||||
this.engine
|
||||
.update(cx, |engine, cx| engine.eyedropper_picked(*color, cx))
|
||||
}
|
||||
event => {
|
||||
let monitor = Monitor::Program;
|
||||
this.engine.update(cx, |engine, cx| match event {
|
||||
ViewerEvent::PlayRequested { .. } => engine.play(monitor, cx),
|
||||
ViewerEvent::PauseRequested { .. } => engine.pause(monitor, cx),
|
||||
ViewerEvent::StepRequested { delta, .. } => engine.step(monitor, *delta, cx),
|
||||
other => println!("[program viewer] request: {other:?}"),
|
||||
});
|
||||
}
|
||||
})
|
||||
cx.subscribe(
|
||||
&viewer,
|
||||
|this, _viewer, event: &ViewerEvent, cx| match event {
|
||||
ViewerEvent::InteractPointer {
|
||||
kind,
|
||||
position,
|
||||
button,
|
||||
pressed,
|
||||
} => this.forward_interact_pointer(*kind, *position, *button, *pressed, cx),
|
||||
ViewerEvent::InteractKey { down, keystroke } => {
|
||||
this.forward_interact_key(*down, keystroke, cx)
|
||||
}
|
||||
// The widget already toggled its own overlay state; nothing to
|
||||
// forward to the engine.
|
||||
ViewerEvent::ToggleSafeFramesRequested { .. }
|
||||
| ViewerEvent::ToggleZoomRequested { .. } => {}
|
||||
// The loop in/out range is the shell-owned program workarea, so
|
||||
// the panel re-emits the request.
|
||||
ViewerEvent::InPointRequested { .. } => cx.emit(menu::ViewerPanelEvent::SetInPoint),
|
||||
ViewerEvent::OutPointRequested { .. } => {
|
||||
cx.emit(menu::ViewerPanelEvent::SetOutPoint)
|
||||
}
|
||||
ViewerEvent::ClearRangeRequested { .. } => {
|
||||
cx.emit(menu::ViewerPanelEvent::ClearRange)
|
||||
}
|
||||
// The OFX color picker armed the eyedropper and the user clicked
|
||||
// the frame: hand the sampled colour back to the engine's mailbox.
|
||||
ViewerEvent::EyedropperPick { color } => this
|
||||
.engine
|
||||
.update(cx, |engine, cx| engine.eyedropper_picked(*color, cx)),
|
||||
event => {
|
||||
let monitor = Monitor::Program;
|
||||
this.engine.update(cx, |engine, cx| match event {
|
||||
ViewerEvent::PlayRequested { .. } => engine.play(monitor, cx),
|
||||
ViewerEvent::PauseRequested { .. } => engine.pause(monitor, cx),
|
||||
ViewerEvent::StepRequested { delta, .. } => {
|
||||
engine.step(monitor, *delta, cx)
|
||||
}
|
||||
other => println!("[program viewer] request: {other:?}"),
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
// Re-render whenever the engine's global eyedropper arm state flips,
|
||||
// so `sync_frame` mirrors it into the viewer widget.
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach();
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
// A throttled idle pump for the OFX interact: the plugin's UI work
|
||||
// loop is served on the viewer's own timer (the app tick is
|
||||
@@ -198,8 +208,7 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let context_menu =
|
||||
ContextMenuHandle::new(Self::on_local_menu_item, window, cx);
|
||||
let context_menu = ContextMenuHandle::new(Self::on_local_menu_item, window, cx);
|
||||
|
||||
let scope_state = cx.new(|_cx| ScopeState {
|
||||
luma: Vec::new(),
|
||||
@@ -246,11 +255,13 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
match action {
|
||||
menu::ViewerMenuAction::ZoomFit => {
|
||||
let zoom = ViewerZoom::Fit;
|
||||
self.viewer.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx));
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx));
|
||||
}
|
||||
menu::ViewerMenuAction::ZoomLevel(index) => {
|
||||
let zoom = ViewerZoom::Level(index);
|
||||
self.viewer.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx));
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx));
|
||||
}
|
||||
menu::ViewerMenuAction::Resolution(divider) => self
|
||||
.engine
|
||||
@@ -280,7 +291,8 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
}),
|
||||
menu::ViewerMenuAction::ShowFps => {
|
||||
let show = self.viewer.read(cx).show_fps();
|
||||
self.viewer.update(cx, |viewer, cx| viewer.set_show_fps(!show, cx));
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_show_fps(!show, cx));
|
||||
}
|
||||
menu::ViewerMenuAction::SaveFrame => self.save_frame(cx),
|
||||
menu::ViewerMenuAction::FullScreen => {
|
||||
@@ -306,7 +318,11 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
.filter(|dir| std::fs::create_dir_all(dir).is_ok())
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
let path = dir.join(name);
|
||||
match self.engine.read(cx).save_frame(Monitor::Program, path.clone(), cx) {
|
||||
match self
|
||||
.engine
|
||||
.read(cx)
|
||||
.save_frame(Monitor::Program, path.clone(), cx)
|
||||
{
|
||||
Ok(path) => println!("[program viewer] saved frame to {}", path.display()),
|
||||
Err(error) => println!("[program viewer] save frame failed: {error}"),
|
||||
}
|
||||
@@ -335,7 +351,8 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
// cursor (the setter no-ops when the state is unchanged, so a paused
|
||||
// viewer stays inert).
|
||||
let armed = self.engine.read(cx).eyedropper_armed(cx);
|
||||
self.viewer.update(cx, |viewer, cx| viewer.set_eyedropper_armed(armed, cx));
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_eyedropper_armed(armed, cx));
|
||||
|
||||
// Keep the main-process interact in sync with the inspector's
|
||||
// current selection (creates/destroys the interact as the target
|
||||
@@ -380,7 +397,7 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
if let Some(bytes) = displayed.as_bytes(0) {
|
||||
let (w, h) = (sz.width.0 as usize, sz.height.0 as usize);
|
||||
let mut ppm = format!("P6\n{w} {h}\n255\n").into_bytes();
|
||||
for px in bytes[..w * h * 4].chunks_exact(4) {
|
||||
for px in bytes[..w * h * 4].as_chunks::<4>().0 {
|
||||
ppm.extend_from_slice(&[px[2], px[1], px[0]]);
|
||||
}
|
||||
let _ = std::fs::write("/tmp/oak_viewer_frame.ppm", ppm);
|
||||
@@ -406,10 +423,8 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
return frame.clone();
|
||||
};
|
||||
let frame_size = frame.size(0);
|
||||
let viewport = InteractViewport::at_frame_size(
|
||||
frame_size.width.0 as u32,
|
||||
frame_size.height.0 as u32,
|
||||
);
|
||||
let viewport =
|
||||
InteractViewport::at_frame_size(frame_size.width.0 as u32, frame_size.height.0 as u32);
|
||||
let time = self.playhead_seconds(cx);
|
||||
|
||||
// Redraw when the base frame, the target instance, the playhead, or
|
||||
@@ -428,11 +443,7 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
});
|
||||
if stale {
|
||||
if let Some(image) = crate::oakui::ofx::draw_interact_composite(
|
||||
instance,
|
||||
&interact,
|
||||
&viewport,
|
||||
time,
|
||||
frame,
|
||||
instance, &interact, &viewport, time, frame,
|
||||
) {
|
||||
self.overlay = Some(OverlayComposite {
|
||||
frame: frame.clone(),
|
||||
@@ -480,14 +491,9 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
return;
|
||||
};
|
||||
let area = size(f32::from(bounds.size.width), f32::from(bounds.size.height));
|
||||
let frame_size = self
|
||||
.engine
|
||||
.read(cx)
|
||||
.cpu_frame(Monitor::Program, cx)
|
||||
.size(0);
|
||||
let frame_size = self.engine.read(cx).cpu_frame(Monitor::Program, cx).size(0);
|
||||
let frame = size(frame_size.width.0 as f32, frame_size.height.0 as f32);
|
||||
let Some((px, py)) = crate::oakui::ofx::viewport_pixel_to_pen(position, area, frame)
|
||||
else {
|
||||
let Some((px, py)) = crate::oakui::ofx::viewport_pixel_to_pen(position, area, frame) else {
|
||||
// The pointer is in the letterbox (outside the frame rect).
|
||||
return;
|
||||
};
|
||||
@@ -519,7 +525,12 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
/// consumed by any binding. The plugin's return status is deliberately
|
||||
/// not acted on: the app does not steal key repeat or other listeners,
|
||||
/// so the interact is a passive consumer of otherwise-unused keys.
|
||||
fn forward_interact_key(&mut self, down: bool, keystroke: &gpui::Keystroke, cx: &mut Context<Self>) {
|
||||
fn forward_interact_key(
|
||||
&mut self,
|
||||
down: bool,
|
||||
keystroke: &gpui::Keystroke,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some((_, interact, _)) = crate::oakui::ofx::active_interact() else {
|
||||
return;
|
||||
};
|
||||
@@ -790,7 +801,7 @@ mod tests {
|
||||
async fn scopes_tab_renders_from_the_current_frame(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(640.0), px(360.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
let clock = engine.read(cx).program_clock().clone();
|
||||
let meter = cx.new(|cx| AudioLevelMeter::new(30, engine.clone(), window, cx));
|
||||
ProgramViewerPanel::new(engine, clock, meter, window, cx)
|
||||
|
||||
@@ -1546,7 +1546,7 @@ mod tests {
|
||||
) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(width), px(height)), |window, cx| {
|
||||
let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx));
|
||||
let engine = cx.new(crate::oakui::MockEngine::demo);
|
||||
let timeline = cx.new(|cx| TimelineView::new(engine.clone(), window, cx).zoom(2.0));
|
||||
TimelinePanel::new(engine, timeline, window, cx)
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user