Files
effectcraft/crates/ui-egui/src/prefs_live.rs
T
bflatasticandClaude Opus 5.5 3b6b390f68 M12.8: read the system memory every 30 s instead of 10 s
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>
2026-10-05 07:25:01 +07:00

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;
}
}