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:
2026-09-15 19:29:32 +08:00
parent 7e87ec135e
commit ee7ea18d94
146 changed files with 5235 additions and 4046 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+77 -77
View File
@@ -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();
}
}
+62 -22
View File
@@ -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");
}
}
+7 -6
View File
@@ -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")))]
{
+4 -4
View File
@@ -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 {
+64 -27
View File
@@ -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),
+33 -30
View File
@@ -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
View File
@@ -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");
File diff suppressed because it is too large Load Diff
+351 -186
View File
@@ -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,
&params,
(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,
&params,
(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,
&params,
(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,
&params,
(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, &params, (32, 32), &mut dst, 32 * 16)
.expect("montage render at the proxy size");
oak_render::eval::render_montage_frame_into(
params.time,
&params,
(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();
+1 -1
View File
@@ -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;
+10 -5
View File
@@ -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).
+1 -1
View File
@@ -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 {
+26 -12
View File
@@ -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();
+1 -1
View File
@@ -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();
+11 -12
View File
@@ -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()
+1 -1
View File
@@ -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)
});
+36 -28
View File
@@ -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
+72 -61
View File
@@ -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)
+1 -1
View File
@@ -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)
});