mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 19:47:12 -06:00
refactor(ui-egui): move app constructors and eframe::App to app.rs, clock and hooks to their own files, screenshot and playback state to state.rs
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
use super::*;
|
||||
|
||||
/// The application's monotonic clock, in seconds.
|
||||
///
|
||||
/// Neither `ctx.input(|i| i.time)` nor `RawInput::time` is valid in a logic-only tick:
|
||||
/// `Context::run_logic` copies only the viewport information, so both are the previous full
|
||||
/// pass's values. Playback, auto-save, screenshot deadlines and the control channel measure time
|
||||
/// with this clock instead. Native and web use the same [`web_time::Instant`] source; tests
|
||||
/// inject [`AppClock::manual`] and advance it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppClock {
|
||||
origin: web_time::Instant,
|
||||
manual: Option<f64>,
|
||||
}
|
||||
|
||||
impl AppClock {
|
||||
/// The host's monotonic clock.
|
||||
pub fn real() -> Self {
|
||||
Self { origin: web_time::Instant::now(), manual: None }
|
||||
}
|
||||
|
||||
/// A clock tests advance explicitly with [`AppClock::advance`].
|
||||
pub fn manual() -> Self {
|
||||
Self { origin: web_time::Instant::now(), manual: Some(0.0) }
|
||||
}
|
||||
|
||||
/// Seconds since the clock started (monotonic on native and in the browser).
|
||||
pub fn now(&self) -> f64 {
|
||||
match self.manual {
|
||||
Some(t) => t,
|
||||
None => self.origin.elapsed().as_secs_f64(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Move a manual clock forward; a real clock ignores this.
|
||||
pub fn advance(&mut self, seconds: f64) {
|
||||
if let Some(t) = &mut self.manual {
|
||||
*t = (*t + seconds.max(0.0)).max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the clock is test-driven.
|
||||
pub fn is_manual(&self) -> bool {
|
||||
self.manual.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppClock {
|
||||
/// The real monotonic clock (`Self::real()`).
|
||||
fn default() -> Self {
|
||||
Self::real()
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectcraftApp {
|
||||
/// The app clock's value at the last tick.
|
||||
pub fn now(&self) -> f64 {
|
||||
self.now
|
||||
}
|
||||
|
||||
/// The clock itself (tests advance a manual clock).
|
||||
pub fn clock_mut(&mut self) -> &mut AppClock {
|
||||
&mut self.clock
|
||||
}
|
||||
|
||||
/// Use a test-driven clock from now on.
|
||||
pub fn use_manual_clock(&mut self) {
|
||||
self.clock = AppClock::manual();
|
||||
self.now = 0.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use super::*;
|
||||
|
||||
/// The desktop's light or dark appearance where egui cannot see it (Wayland compositors that send
|
||||
/// winit no theme). `None` = the host does not know.
|
||||
pub type SystemThemeFn = Box<dyn Fn(&egui::Context) -> Option<egui::Theme>>;
|
||||
|
||||
/// A file dialog a host shows without blocking the frame ([`Hooks::file_dialog_async`]).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FileDialog {
|
||||
/// Files to open or import with these extensions; several when `many`.
|
||||
Open { exts: Vec<String>, many: bool },
|
||||
/// An EffectCraft project to open.
|
||||
OpenProject,
|
||||
/// A folder.
|
||||
Folder,
|
||||
/// Where to save `default` (a file name or path) with extension `ext` (empty: no filter);
|
||||
/// `project`: the project save dialog, given the file name.
|
||||
Save { default: String, ext: String, project: bool },
|
||||
}
|
||||
|
||||
impl FileDialog {
|
||||
/// The save dialog for `default`, as [`Hooks::save_dialog`] picks it.
|
||||
pub fn save(default: &str) -> Self {
|
||||
let ext = std::path::Path::new(default).extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default();
|
||||
Self::Save { default: default.to_string(), project: ext.eq_ignore_ascii_case("ecproj"), ext }
|
||||
}
|
||||
}
|
||||
|
||||
/// Called with the paths a [`FileDialog`] picked (empty: cancelled), from any thread.
|
||||
pub type DialogDone = Box<dyn FnOnce(Vec<String>) + Send>;
|
||||
|
||||
/// Host hooks provided by the native app (file pickers etc.).
|
||||
#[derive(Default)]
|
||||
pub struct Hooks {
|
||||
/// The system appearance for Settings ▸ Appearance ▸ Sync with System (the desktop app reads
|
||||
/// the Linux desktop portal); without it, what egui reports.
|
||||
pub system_theme: Option<SystemThemeFn>,
|
||||
pub pick_files: Option<Box<dyn Fn(&[&str]) -> Vec<String>>>,
|
||||
pub pick_save: Option<Box<dyn Fn(&str) -> Option<String>>>,
|
||||
pub pick_open_project: Option<Box<dyn Fn() -> Option<String>>>,
|
||||
/// Opens the audio output for preview playback (the desktop app uses cpal).
|
||||
pub audio_device: Option<audio::AudioDeviceFactory>,
|
||||
/// Lists audio output devices (Settings ▸ Audio).
|
||||
pub audio_devices: Option<Box<dyn Fn() -> Vec<String>>>,
|
||||
/// Picks a folder (Settings paths).
|
||||
pub pick_folder: Option<Box<dyn Fn() -> Option<String>>>,
|
||||
/// Save dialog for other file kinds: (default name or path, extension; empty: no filter). A
|
||||
/// default path opens the dialog in its folder.
|
||||
pub pick_save_file: Option<Box<dyn Fn(&str, &str) -> Option<String>>>,
|
||||
/// The system clipboard's text (native menu Edit ▸ Paste into a text field).
|
||||
pub clipboard_text: Option<Box<dyn Fn() -> Option<String>>>,
|
||||
/// Application actions the OS performs (`app.hide`, `app.hideOthers`, `app.showAll` on
|
||||
/// macOS). Returns false when the host doesn't handle the id.
|
||||
pub app_action: Option<Box<dyn Fn(&str) -> bool>>,
|
||||
/// Shows a command's file dialog without blocking the frame and calls `done` with the
|
||||
/// answer (the host then asks for a repaint). Used in place of the blocking pickers above for
|
||||
/// the commands' dialogs (File ▸ Import, Open, Save As…) where blocking the frame is unsafe or
|
||||
/// leaves the window unresponsive: on macOS a nested modal event loop there aborted the app on
|
||||
/// a drag over the window (#511), and on Linux a blocking desktop-portal dialog never pumps
|
||||
/// events, so the compositor marks the window as not responding (#1275).
|
||||
pub file_dialog_async: Option<Box<dyn Fn(FileDialog, DialogDone)>>,
|
||||
/// Tells the user the graphics device was lost and the window is closing, once. `saved` is
|
||||
/// the auto-save of unsaved changes (`None`: nothing was unsaved), its path or why it failed.
|
||||
pub device_lost: Option<Box<dyn Fn(Option<Result<String, String>>)>>,
|
||||
}
|
||||
|
||||
impl Hooks {
|
||||
/// The host's save dialog for `default` (a file name or path), offering its extension: the
|
||||
/// project dialog for project files, else [`Hooks::pick_save_file`]. The project dialog is
|
||||
/// the fallback for hosts without that (the web, whose dialog only names the file): on macOS
|
||||
/// a dialog filtered to projects appends `.ecproj` to any other name (#293). `None` when the
|
||||
/// host has no save dialog; `Some(None)` when it was cancelled.
|
||||
pub fn save_dialog(&self, default: &str) -> Option<Option<String>> {
|
||||
let path = std::path::Path::new(default);
|
||||
let ext = path.extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default();
|
||||
match (&self.pick_save_file, &self.pick_save) {
|
||||
(Some(pick), _) if !ext.eq_ignore_ascii_case("ecproj") => Some(pick(default, &ext)),
|
||||
(_, Some(pick)) => Some(pick(&path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default())),
|
||||
(Some(pick), None) => Some(pick(default, &ext)),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-2586
File diff suppressed because it is too large
Load Diff
@@ -229,6 +229,7 @@ impl Tool {
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
/// The viewer's Resolution: each composition keeps its own in the project
|
||||
/// ([`effectcraft_engine::project::Comp::resolution`], `EffectcraftApp::viewer_res`).
|
||||
pub use effectcraft_engine::project::Resolution;
|
||||
@@ -746,3 +747,48 @@ impl Default for UiState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a `ui.screenshot` captures.
|
||||
///
|
||||
/// A panel or element is an identity, not a rectangle: it is resolved again when the capture is
|
||||
/// issued, after the settle passes, so a panel that moved or resized while the UI settled is
|
||||
/// captured where it is now (never with the rectangle from request admission).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ShotTarget {
|
||||
/// The whole root surface.
|
||||
Window,
|
||||
/// A fixed rectangle in logical points, relative to the root surface's top-left corner.
|
||||
Rect([f32; 4]),
|
||||
/// One dock panel's automation rectangle (`panel.<id>`).
|
||||
Panel(PanelKind),
|
||||
/// One automation element id's rectangle.
|
||||
Id(String),
|
||||
}
|
||||
|
||||
/// The comp frames a playback pass covers (`first..=last`).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct FrameWindow {
|
||||
pub(crate) first: i64,
|
||||
pub(crate) last: i64,
|
||||
}
|
||||
|
||||
/// The inputs of one playback pass: the viewer series it renders, the preview plan and
|
||||
/// position, and the frame range and clock settings its steps share. `queued` is a snapshot of
|
||||
/// the in-flight render count that each step copies. Built once in `advance_playback`.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct PlaybackPass<'a> {
|
||||
pub(crate) ctx: &'a egui::Context,
|
||||
pub(crate) comp: &'a effectcraft_engine::project::Comp,
|
||||
pub(crate) cid: ItemId,
|
||||
pub(crate) scale: f64,
|
||||
pub(crate) now: f64,
|
||||
pub(crate) frame_rate: effectcraft_engine::time::FrameRate,
|
||||
pub(crate) series: FrameKey,
|
||||
pub(crate) plan: Option<effectcraft_engine::preview::PreviewPlan>,
|
||||
pub(crate) window: FrameWindow,
|
||||
pub(crate) cur: i64,
|
||||
pub(crate) video: bool,
|
||||
pub(crate) looping: bool,
|
||||
pub(crate) ahead: i64,
|
||||
pub(crate) queued: usize,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user