mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 05:27:08 -06:00
Each reading starts PowerShell on Windows (in the background since the previous commit); the cache budgets only need to follow memory pressure, not track it closely. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
6.3 KiB
Rust
143 lines
6.3 KiB
Rust
//! Settings the desktop frontend applies every frame: dropped files (Import ▸ Default Drag
|
|
//! Import As), the memory watch (Memory & CPU ▸ RAM Reserved / Reduce Cache Size When System Is
|
|
//! Low on Memory), Startup ▸ Show Home Screen When Opening a Project, and Video ▸ Video Preview
|
|
//! output (a second window showing the composition frame, Mercury Transmit-style).
|
|
|
|
use egui::{Color32, Rect};
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::EffectcraftApp;
|
|
use crate::frames::FrameKey;
|
|
|
|
/// Run once per frame, before the panels draw.
|
|
pub fn frame(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
|
dropped_files(app, ctx);
|
|
memory_watch(app, ctx);
|
|
home_on_open(app, ctx);
|
|
video_preview(app, ctx);
|
|
}
|
|
|
|
/// Files dropped on the window are imported (layered files as Default Drag Import As says).
|
|
fn dropped_files(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
|
let paths: Vec<String> =
|
|
ctx.input(|i| i.raw.dropped_files.iter().map(|f| f.path().to_string_lossy().to_string()).filter(|p| std::path::Path::new(p).is_absolute()).collect());
|
|
if paths.is_empty() {
|
|
return;
|
|
}
|
|
let (proj, rest): (Vec<String>, Vec<String>) = paths.into_iter().partition(|p| {
|
|
let l = p.to_ascii_lowercase();
|
|
l.ends_with(".ecproj") || l.ends_with(".ecprojx")
|
|
});
|
|
if let Some(p) = proj.first()
|
|
&& let Err(e) = crate::menus::invoke(app, ctx, "file.open", json!({"path": p}))
|
|
{
|
|
app.ui.status = e;
|
|
}
|
|
if !rest.is_empty() {
|
|
let params = json!({"paths": rest, "drag": true, "importAs": app.session.prefs.drag_import_as()});
|
|
if let Err(e) = crate::menus::invoke(app, ctx, "file.import", params) {
|
|
app.ui.status = e;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Re-read the system's free memory every 30 s (cache budgets follow it; on Windows each
|
|
/// reading starts PowerShell in the background, so it isn't done more often).
|
|
fn memory_watch(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
|
let now = ctx.input(|i| i.time);
|
|
let id = egui::Id::new("prefs-memory-watch");
|
|
let last = ctx.data(|d| d.get_temp::<f64>(id));
|
|
if last.is_some_and(|t| now - t < 30.0) {
|
|
return;
|
|
}
|
|
ctx.data_mut(|d| d.insert_temp(id, now));
|
|
// The first frame only records the time: tests and short runs don't query the system.
|
|
if last.is_some() {
|
|
app.session.memory_tick();
|
|
}
|
|
}
|
|
|
|
/// Startup ▸ Show Home Screen When Opening a Project.
|
|
fn home_on_open(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
|
let id = egui::Id::new("prefs-last-project");
|
|
let path = app.session.path.clone();
|
|
let last = ctx.data(|d| d.get_temp::<Option<String>>(id));
|
|
ctx.data_mut(|d| d.insert_temp(id, path.clone()));
|
|
let Some(last) = last else { return };
|
|
let opened = app.session.journal.last().is_some_and(|(c, _)| matches!(c.as_str(), "file.open" | "file.openRecent" | "file.revert"));
|
|
if path.is_some() && path != last && opened && app.session.prefs.startup.show_home_on_open_project {
|
|
app.ui.start_screen = true;
|
|
}
|
|
}
|
|
|
|
/// Video ▸ Mirror on Computer Monitor off: during playback the Composition panel leaves the
|
|
/// frame to the Video Preview window.
|
|
pub fn main_viewer_hidden(app: &EffectcraftApp) -> bool {
|
|
let v = &app.session.prefs.video;
|
|
v.enable_output && !v.mirror_on_monitor && app.playback.playing
|
|
}
|
|
|
|
/// Whether the Video Preview window shows now (Disable Video Output When in Background).
|
|
pub fn video_preview_active(app: &EffectcraftApp, focused: bool) -> bool {
|
|
let v = &app.session.prefs.video;
|
|
v.enable_output && (focused || !v.disable_when_background)
|
|
}
|
|
|
|
/// Video ▸ Enable Video Preview Output: a second native window with the current composition
|
|
/// frame, letterboxed on black. Video Device "Full Screen" opens it full screen (move it to the
|
|
/// external display first); Video Output During Playback off holds the last frame while playing.
|
|
fn video_preview(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
|
let focused = ctx.input(|i| i.focused);
|
|
if !video_preview_active(app, focused) {
|
|
return;
|
|
}
|
|
let v = app.session.prefs.video.clone();
|
|
let tex_id = egui::Id::new("video-preview-tex");
|
|
let update = !app.playback.playing || v.output_during_playback;
|
|
let key = app.viewer_shown.as_ref().map(|(_, k)| *k);
|
|
let cur: Option<(FrameKey, egui::TextureHandle)> = ctx.data(|d| d.get_temp(tex_id));
|
|
let tex = match (update, key, cur) {
|
|
(true, Some(k), Some((ck, t))) if ck == k => Some(t),
|
|
(true, Some(k), prev) => match app.viewer_pixels() {
|
|
Some(img) => {
|
|
let opts = crate::panels::viewer::zoom_texture_options(app.session.prefs.viewer_zoom_smooth());
|
|
let t = match prev {
|
|
Some((_, mut t)) => {
|
|
t.set((*img).clone(), opts);
|
|
t
|
|
}
|
|
None => ctx.load_texture("video-preview", (*img).clone(), opts),
|
|
};
|
|
ctx.data_mut(|d| d.insert_temp(tex_id, (k, t.clone())));
|
|
Some(t)
|
|
}
|
|
None => prev.map(|p| p.1),
|
|
},
|
|
(_, _, prev) => prev.map(|p| p.1),
|
|
};
|
|
let title =
|
|
format!("Video Preview — {}", app.session.active_comp_id().and_then(|c| app.session.project.item(c)).map(|i| i.name.clone()).unwrap_or_default());
|
|
let mut builder = egui::ViewportBuilder::default().with_title(title).with_inner_size([960.0, 540.0]);
|
|
if v.device == "fullscreen" {
|
|
builder = builder.with_fullscreen(true);
|
|
}
|
|
let mut closed = false;
|
|
ctx.show_viewport_immediate(egui::ViewportId::from_hash_of("effectcraft-video-preview"), builder, |vctx, _| {
|
|
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(Color32::BLACK)).show(vctx, |ui| {
|
|
let area = ui.max_rect();
|
|
if let Some(t) = &tex {
|
|
let [w, h] = t.size().map(|x| x.max(1) as f32);
|
|
let s = (area.width() / w).min(area.height() / h);
|
|
let r = Rect::from_center_size(area.center(), egui::vec2(w * s, h * s));
|
|
ui.painter().image(t.id(), r, Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), Color32::WHITE);
|
|
}
|
|
});
|
|
if vctx.input(|i| i.viewport().close_requested()) {
|
|
closed = true;
|
|
}
|
|
});
|
|
if closed && let Err(e) = app.set_pref("video.enableOutput", Value::Bool(false)) {
|
|
app.ui.status = e;
|
|
}
|
|
}
|