mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 08:29:14 +00:00
The canvas cache key held the process-wide placed-document bitmap generation, so a bitmap made for a file that only another document holds changed every document's key, and the next frame differed from the frame shown in more than the view. In the web build, which has no render worker, a heavy canvas then rendered in the middle of a pan instead of waiting until the view was still. On desktop the pan dropped its low-resolution fill, and the render worker did not copy pixels from its last frame. Each document now keeps its own value, checked once per generation change, as the plug-in key is. The value moves to the current generation when the document shows a placed document of a file whose bitmap was made. A file kept only for Undo does not count. When more bitmaps were made since the last check than are recorded, the value moves when the document shows a placed document of one of its images. After the value moves, the part of the frame to draw again comes from the bitmaps made since the last check before the move, so bitmaps made earlier for other documents no longer make the whole frame render. The canvas tests that retried or tolerated a new frame when another test made a bitmap now assert without those workarounds.
647 lines
31 KiB
Rust
647 lines
31 KiB
Rust
use super::caches::{CanvasCache, Perf};
|
||
use super::clock::now_ms;
|
||
use super::clock::now_secs;
|
||
use super::services::{Services, browsable_url};
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
use super::state::FONT_INSTALL_POLL;
|
||
use super::state::{
|
||
CaptureEpoch, GeometryStamp, InputGeneration, MAX_PENDING_SCREENSHOTS, MAX_SCREENSHOT_BYTES, MAX_SCREENSHOT_TOTAL_BYTES, PendingScreenshot,
|
||
QueuedScreenshot, SCREENSHOT_SETTLE_FRAMES, SCREENSHOT_TIMEOUT_ERROR, SCREENSHOT_TIMEOUT_MS, SYSTEM_CLIPBOARD_POLL, take_frame_once,
|
||
};
|
||
use super::{
|
||
Arc, AutomationRoots, ControlRequest, Ordering, Receiver, Session, UiState, Value, VectorcraftApp, View, ViewInfo, background, canvas, chrome,
|
||
clipboard_probe, dialogs, export_progress, file_access, fileio, font_menu, graphics, i18n, io, json, menus, native_menu, opening, palette,
|
||
panel_docking, panels, picks, place, placing, plugin_runs, prefs_dialog, recovery, screen_color, scrub, shortcut_editor, shortcuts, theme,
|
||
titlebar, toolbar, tools_float, trace_runs, unsaved, updates_ui, widgets,
|
||
};
|
||
|
||
mod clipboard;
|
||
mod commands;
|
||
mod control;
|
||
mod files;
|
||
mod fonts;
|
||
mod frame;
|
||
mod ime;
|
||
mod input;
|
||
mod links;
|
||
mod prefs;
|
||
mod screenshot;
|
||
mod view;
|
||
|
||
pub use fonts::MAX_RECENT_FONTS;
|
||
|
||
impl VectorcraftApp {
|
||
/// Takes the host's Caps Lock state, which gives the drawing tools the crosshair while it is on
|
||
/// without changing the saved Use Precise Cursors preference.
|
||
pub fn set_caps_lock(&mut self, on: bool) {
|
||
if self.session.caps_lock != on
|
||
&& let Err(err) = self.run("view.capsLock", json!({ "on": on }))
|
||
{
|
||
log::error!("Could not update Caps Lock cursor state: {err}");
|
||
}
|
||
}
|
||
|
||
/// The app around `session`, reaching the platform through `services` (file pickers, the
|
||
/// clipboard, files and the like). It starts cataloguing the installed fonts at once, gives
|
||
/// Data Recovery the host's store and makes a view for each open document.
|
||
pub fn new(mut session: Session, services: Services) -> Self {
|
||
// The font menus and the first file opened need the installed fonts: catalog them now.
|
||
vectorcraft_text::FontDb::global().scan_in_background();
|
||
if let Some(store) = &services.recovery_store {
|
||
session.recovery.set_store(store.clone());
|
||
}
|
||
// The place cursor's clicks and drags place their files through the app
|
||
// ([`place::place_from_cursor`]).
|
||
session.place_cursor_requests = true;
|
||
// A script runs a slice per frame, so the window keeps drawing and Stop can be pressed.
|
||
session.script_frames = true;
|
||
let views = session.documents().iter().map(View::of).collect();
|
||
let view_uids = session.documents().iter().map(|d| d.uid).collect();
|
||
let known = session.documents().iter().map(|d| d.uid).collect();
|
||
Self {
|
||
session,
|
||
ui: UiState::default(),
|
||
views,
|
||
view_uids,
|
||
services,
|
||
canvas: CanvasCache {
|
||
renderer: vectorcraft_render::Renderer::new(),
|
||
texture: None,
|
||
key: None,
|
||
shown: None,
|
||
last_ms: 0.0,
|
||
moved: false,
|
||
from_worker: false,
|
||
reused: None,
|
||
waits_for_still: cfg!(target_arch = "wasm32"),
|
||
still: None,
|
||
fill: None,
|
||
fill_worker: None,
|
||
worker: None,
|
||
worker_started: false,
|
||
slices: None,
|
||
print_tiling: None,
|
||
live_paint_gaps: None,
|
||
hidden_chars: Default::default(),
|
||
spelling: Default::default(),
|
||
selection_marks: Default::default(),
|
||
selection_box: None,
|
||
selection_bounds: None,
|
||
content_bounds: None,
|
||
plugin_keys: Default::default(),
|
||
placed_keys: Default::default(),
|
||
cursors: Default::default(),
|
||
view_moved: None,
|
||
hover_cursor: None,
|
||
presented: None,
|
||
opening_fit: None,
|
||
panes: Default::default(),
|
||
windows: crate::doc_windows::Windows::new(known),
|
||
},
|
||
perf: Perf::default(),
|
||
dock_rt: Default::default(),
|
||
titlebar_inset: 0.0,
|
||
last_effect: None,
|
||
last_pathfinder: None,
|
||
last_warp: None,
|
||
character_focus: None,
|
||
run_count: 0,
|
||
clipboard_out: None,
|
||
clipboard_published: None,
|
||
clipboard_in: None,
|
||
clipboard_publishing: Default::default(),
|
||
pending_url: None,
|
||
control_rx: None,
|
||
deferred_control: None,
|
||
synthetic_dropped_before: 0,
|
||
pending_screenshots: vec![],
|
||
pointer_replies: vec![],
|
||
queued_screenshots: vec![],
|
||
screenshot_token: 0,
|
||
screenshot_reserved: 0,
|
||
synthetic: vec![],
|
||
styled: false,
|
||
fonts_ready: false,
|
||
context: 0,
|
||
ui_ctx: None,
|
||
ui_fonts: Default::default(),
|
||
updates_rt: Default::default(),
|
||
frame_rate: Default::default(),
|
||
canvas_rect: None,
|
||
canvas_ui_zoom: 1.0,
|
||
control_rotate_press: None,
|
||
hover_doc: None,
|
||
titlebar_from_env: false,
|
||
custom_titlebar: false,
|
||
desktop_frame: false,
|
||
last_window_title: String::new(),
|
||
fullscreen_for_mode: false,
|
||
graphics_adapter: None,
|
||
beacon: &crate::hang_beacon::BEACON,
|
||
place: Default::default(),
|
||
system_paste: false,
|
||
clipboard_image_request: None,
|
||
system_paste_at: f64::NEG_INFINITY,
|
||
clipboard_probe: None,
|
||
picks: picks::Picks::default(),
|
||
screen_sampler: None,
|
||
font_check: None,
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
font_install_check_at: f64::NEG_INFINITY,
|
||
paste_chord: Default::default(),
|
||
// Held keys repeat their command (Cmd+= keeps zooming in).
|
||
dispatcher: craft_keymap::egui::Dispatcher::new().with_repeat(true),
|
||
background: Default::default(),
|
||
opening: Default::default(),
|
||
plugin_runs: Default::default(),
|
||
placing: Default::default(),
|
||
trace_runs: Default::default(),
|
||
recovery: Default::default(),
|
||
host_modifiers: Default::default(),
|
||
forwarded: Vec::new(),
|
||
forwarded_modifiers: None,
|
||
forwarded_modifiers_held: false,
|
||
forwarded_frame: false,
|
||
synthetic_modifiers: false,
|
||
ime_marked: None,
|
||
ime_context: Default::default(),
|
||
ime_compositions: Default::default(),
|
||
ime_resets: Default::default(),
|
||
ime_caret_rect: None,
|
||
ime_discard: false,
|
||
prefs_changed: false,
|
||
asset_export_presets: Value::Null,
|
||
touch_screen: false,
|
||
scrub_group: false,
|
||
scrub_scope: None,
|
||
automation_roots: None,
|
||
synthetic_frame: false,
|
||
pending_plugin_install: None,
|
||
pen_end: Default::default(),
|
||
host_iteration: 0,
|
||
#[cfg(test)]
|
||
now_override: None,
|
||
tick_iteration: None,
|
||
input_frames: vec![],
|
||
shortcuts_frames: vec![],
|
||
held_tool: Vec::new(),
|
||
synthetic_injected: false,
|
||
injected_events: vec![],
|
||
injected_generation: InputGeneration::default(),
|
||
injected_settle_hook: false,
|
||
injected_settled: Default::default(),
|
||
geometry_epoch: 0,
|
||
geometry_stamp: None,
|
||
}
|
||
}
|
||
|
||
pub fn with_control(mut self, rx: Receiver<ControlRequest>) -> Self {
|
||
self.control_rx = Some(rx);
|
||
self
|
||
}
|
||
|
||
/// Confine what the control channel does to `roots` (#832): its requests, the input it
|
||
/// injects (in the frames that carry it) and the saves and exports they start read and write
|
||
/// only inside them ([`file_access`]). The host's file services check every path they're given
|
||
/// against the roots in force, and so does the engine; outside those scopes the person at the
|
||
/// keyboard works as usual.
|
||
pub fn with_automation_roots(mut self, roots: Option<Arc<AutomationRoots>>) -> Self {
|
||
let Some(roots) = roots else { return self };
|
||
self.automation_roots = Some(roots);
|
||
let s = &mut self.services;
|
||
if let Some(read) = s.read.take() {
|
||
s.read = Some(Box::new(move |p: &str| file_access::check_read(p).and_then(|()| read(p))));
|
||
}
|
||
if let Some(read_head) = s.read_head.take() {
|
||
s.read_head = Some(Box::new(move |p: &str, max: usize| file_access::check_read(p).and_then(|()| read_head(p, max))));
|
||
}
|
||
if let Some(mut write) = s.write.take() {
|
||
s.write = Some(Box::new(move |p: &str, b: &[u8]| file_access::check_write(p).and_then(|()| write(p, b))));
|
||
}
|
||
if let Some(write) = s.write_shared.take() {
|
||
s.write_shared = Some(Arc::new(move |p: &str, b: &[u8]| file_access::check_write(p).and_then(|()| write(p, b))));
|
||
}
|
||
if let Some(stream) = s.stream_shared.take() {
|
||
s.stream_shared = Some(Arc::new(move |p: &str, fill: &mut dyn FnMut(&mut dyn std::io::Write) -> std::io::Result<()>| {
|
||
file_access::check_write(p).and_then(|()| stream(p, fill))
|
||
}));
|
||
}
|
||
// Edit Original and Show in Folder hand a file to another app: only one automation may read.
|
||
for service in [&mut s.open_file, &mut s.reveal] {
|
||
if let Some(mut open) = service.take() {
|
||
*service = Some(Box::new(move |p: &str| file_access::check_read(p).and_then(|()| open(p))));
|
||
}
|
||
}
|
||
self
|
||
}
|
||
}
|
||
|
||
/// eframe isn't a dependency of this crate (the host owns the event loop); these entry points are
|
||
/// called from the host's `eframe::App` impl.
|
||
impl VectorcraftApp {
|
||
/// Applies the work that finished off the UI thread. It stays pending while a script runs, since
|
||
/// every change to the documents ends in a busy error until the script ends.
|
||
pub(crate) fn poll_finished_work(&mut self) {
|
||
if self.session.script_running() {
|
||
return;
|
||
}
|
||
background::poll(self);
|
||
opening::poll(self);
|
||
plugin_runs::poll(self);
|
||
trace_runs::poll(self);
|
||
placing::poll(self);
|
||
}
|
||
|
||
/// The language the UI is drawn in: the Preferences dialog's choice while it is open (so a
|
||
/// change shows before OK), else the `interfaceLanguage` preference (`auto` = the system's).
|
||
pub fn ui_language(&self) -> i18n::Lang {
|
||
let editing =
|
||
self.ui.dialog.as_ref().filter(|d| d.kind == "preferences").and_then(|d| d.fields.get("interfaceLanguage")).and_then(Value::as_str);
|
||
i18n::Lang::from_pref(editing.unwrap_or(&self.session.prefs.interface_language))
|
||
}
|
||
|
||
/// The host starts one iteration: called from both hosts' `raw_input_hook`, before the host
|
||
/// decides whether a UI pass follows. It counts the iteration; it is not a frame or pass count
|
||
/// and does not promise that `ui` will run (a hidden window gets `tick` only).
|
||
pub fn begin_host_iteration(&mut self) {
|
||
self.host_iteration = self.host_iteration.wrapping_add(1);
|
||
// Frame keys identify input already taken in this host iteration. Each iteration starts
|
||
// fresh; this is what makes a viewport's discarded pass a duplicate, while two viewports'
|
||
// frame 0 both count.
|
||
self.input_frames.clear();
|
||
self.shortcuts_frames.clear();
|
||
}
|
||
|
||
/// Background work, once per host iteration (full or logic-only): control requests, background
|
||
/// saves/exports/opens, plug-ins, recovery, font and clipboard polls, capture completion. It
|
||
/// reads only viewport state from `ctx.input` — never events or `i.time`: a logic-only pass
|
||
/// leaves the last shown frame's input in place.
|
||
///
|
||
/// A bug that panics costs one iteration and shows an error, instead of closing the app with
|
||
/// unsaved work.
|
||
pub fn tick(&mut self, ctx: &egui::Context) {
|
||
let confined = self.synthetic_frame.then(|| self.automation_roots.clone()).flatten();
|
||
if let Err(msg) = file_access::confine(confined.as_ref(), || vectorcraft_engine::guard::catch_panic(|| self.tick_frame(ctx))) {
|
||
self.picks.recover();
|
||
self.session.set_replaying(false);
|
||
self.status(format!("Internal error: {msg} (please report this bug)"));
|
||
}
|
||
}
|
||
|
||
/// [`Self::tick`]'s work, once per host iteration: a second call in the same iteration (a
|
||
/// discarded rerun shares one hook call) does nothing.
|
||
fn tick_frame(&mut self, ctx: &egui::Context) {
|
||
if self.tick_iteration == Some(self.host_iteration) {
|
||
return;
|
||
}
|
||
self.tick_iteration = Some(self.host_iteration);
|
||
let dialog_was_open = self.ui.dialog.is_some();
|
||
let lang = self.ui_language();
|
||
i18n::set_current(lang);
|
||
// The engine gives new type the Japanese defaults while the UI is in Japanese.
|
||
if self.session.ui_language.as_deref() != Some(lang.code()) {
|
||
self.session.ui_language = Some(lang.code().to_string());
|
||
}
|
||
self.install_injected_settle_hook(ctx);
|
||
self.advance_geometry_epoch(ctx);
|
||
// Monotonic host time, never egui's `InputState::time`: a logic-only (hidden) iteration
|
||
// replays the last shown pass's time, freezing recovery, clipboard polling and deadlines.
|
||
// Tests inject a clock to drive idle scheduling deterministically.
|
||
#[cfg(test)]
|
||
let now = self.now_override.unwrap_or_else(now_secs);
|
||
#[cfg(not(test))]
|
||
let now = now_secs();
|
||
self.perf.fps = self.frame_rate.frame(now, self.perf.frame_ms / 1000.0);
|
||
self.sync_views();
|
||
self.refresh_replace_font_warning(false);
|
||
updates_ui::tick(self, ctx);
|
||
// Read the system clipboard only when that alone decides whether Paste is enabled. A
|
||
// background thread reads it where the host installs one (an unresponsive owner must never
|
||
// stall the frame loop); otherwise, or once that thread is gone, it is read here, at most a
|
||
// few times a second (opening it locks it against other apps on some systems).
|
||
let wanted = self.session.clipboard.is_empty() && self.session.active().is_some();
|
||
if let Some(make) = self.services.clipboard_probe.take() {
|
||
self.clipboard_probe = clipboard_probe::Probe::start(make, ctx.clone());
|
||
}
|
||
if let Some(pasteable) = self.clipboard_probe.as_mut().and_then(|p| p.pasteable(wanted)) {
|
||
self.system_paste = pasteable;
|
||
} else if !(0.0..SYSTEM_CLIPBOARD_POLL).contains(&(now - self.system_paste_at)) {
|
||
self.clipboard_probe = None;
|
||
self.system_paste_at = now;
|
||
self.system_paste = wanted && self.system_clipboard_pasteable();
|
||
}
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
if !(0.0..FONT_INSTALL_POLL).contains(&(now - self.font_install_check_at)) {
|
||
self.font_install_check_at = now;
|
||
// Read on the next frame the app draws anyway: an idle app isn't woken for it.
|
||
self.refresh_installed_fonts(false);
|
||
}
|
||
self.poll_font_check(ctx);
|
||
picks::poll(self, ctx);
|
||
// A Paste the host read before this iteration's formats were written must still wait for
|
||
// them, even if the worker below finishes them during this same tick.
|
||
ctx.data_mut(|d| d.insert_temp(egui::Id::unique("clipboard.pending-at-tick"), self.clipboard_publishing.is_pending()));
|
||
screen_color::poll(self, ctx);
|
||
self.poll_finished_work();
|
||
crate::script_runs::poll(self);
|
||
crate::scripts_menu::poll(self, ctx);
|
||
if self.session.script_running() && !self.session.script_waiting() && self.session.script_dialog().is_none() {
|
||
// A script in `$.sleep` goes on when its time is up; until then the app can idle.
|
||
match self.session.script_sleep_left() {
|
||
Some(left) => ctx.request_repaint_after(left),
|
||
None => ctx.request_repaint(),
|
||
}
|
||
}
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
self.tick_link_updates(ctx, now);
|
||
crate::addons::select::link_status::refresh(self, now);
|
||
if !self.background.jobs.is_empty()
|
||
|| !self.opening.pending.is_empty()
|
||
|| !self.plugin_runs.running.is_empty()
|
||
|| !self.placing.pending.is_empty()
|
||
|| !self.trace_runs.running.is_empty()
|
||
{
|
||
// Keep the status bar's progress moving and pick the result up when it arrives.
|
||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||
}
|
||
// The window's close button (or the system quitting the app) asks about unsaved documents,
|
||
// once the saves running in the background are done and a Shape Builder merge being
|
||
// computed is applied. Not while a file dialog's sheet is on the window: what asked for it
|
||
// runs again when it answers.
|
||
if ctx.input(|i| i.viewport().close_requested()) {
|
||
// This native viewport is going away: release its IME context. No request is
|
||
// allocated here: the close prompt or file sheet publishes no custom descriptor, so a
|
||
// token would survive for a later stock field. A close the user then cancels starts a
|
||
// fresh generation on the next pass (never inherits a composition across the prompt).
|
||
crate::ime::close_viewport(self, ctx);
|
||
self.ime_context.end_viewport(ctx.viewport_id());
|
||
unsaved::finish_pending(self);
|
||
if self.session.script_running() {
|
||
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
||
dialogs::script_run::ask_to_close(self);
|
||
} else if self.file_sheet_open() {
|
||
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
||
self.status(picks::busy());
|
||
} else if unsaved::any_dirty(self) {
|
||
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
||
if let Err(e) = unsaved::close_all(self, "quit") {
|
||
self.status(e);
|
||
}
|
||
} else {
|
||
// Quitting with nothing unsaved: no copies to leave behind.
|
||
vectorcraft_engine::cmd::recovery::forget_all(&mut self.session);
|
||
}
|
||
}
|
||
if let Some(wait) = recovery::frame(self, now) {
|
||
// The timer and heartbeat run in an idle window too.
|
||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(wait));
|
||
}
|
||
crate::keymap::note_os(ctx);
|
||
shortcut_editor::sync(&self.ui);
|
||
crate::dialogs::svg_filter::sync(self);
|
||
crate::panels::actions::sync_function_keys(&self.ui);
|
||
prefs_dialog::apply_runtime(self, ctx);
|
||
// The OS title bar (and the taskbar / Alt-Tab entry) follows the active file; with the
|
||
// system title bar this is where the document name lives, as the in-app mark is hidden.
|
||
crate::chrome::sync_window_title(self, ctx);
|
||
crate::chrome::sync_fullscreen(self, ctx);
|
||
self.drain_control(ctx);
|
||
self.answer_pointer_replies(ctx);
|
||
// An egui `PasteImage` with a rich system clipboard: run the paste command now, once. It
|
||
// goes through `run`, whose `edit.paste` branch calls `adopt_system_clipboard` (which clears
|
||
// the request) to read the formats and then pastes them. A text paste in the same UI pass
|
||
// never sets the request, so the two cannot both paste.
|
||
if let Some(held) = self.clipboard_image_request.take() {
|
||
crate::menus::invoke(self, crate::shortcuts::paste_command(held), json!({}));
|
||
}
|
||
self.collect_screenshots(ctx);
|
||
if let Some(u) = self.pending_url.take() {
|
||
ctx.open_url(egui::OpenUrl::new_tab(u));
|
||
}
|
||
self.flush_clipboard_out(ctx);
|
||
self.drain_inbox();
|
||
// An action playing at Step by Step or Pause For runs its next step when it is due, after
|
||
// the control requests and opened files above: a Stop or a document switch they bring
|
||
// comes first.
|
||
if let Some(wait) = crate::panels::actions::tick_playback(self, now) {
|
||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(wait));
|
||
}
|
||
if self.viewport_hidden(ctx) && (self.needs_hidden_paint() || (!dialog_was_open && self.ui.dialog.is_some())) {
|
||
// A plain repaint runs only logic in a hidden window: the ui that applies injected
|
||
// input, shows a queued dialog or issues a capture needs this command. egui-winit
|
||
// handles it; the 0.37 web backend does not (a pending fork fix), so a hidden browser
|
||
// tab cannot be claimed as qualified.
|
||
ctx.send_viewport_cmd(egui::ViewportCommand::RequestPaintWhileHidden);
|
||
}
|
||
if !self.synthetic.is_empty() || self.synthetic_injected || !self.pending_screenshots.is_empty() {
|
||
ctx.request_repaint();
|
||
}
|
||
}
|
||
|
||
/// The 0.36-style entry point, kept for the crate's tests, which call one `logic` inside a
|
||
/// `run_ui` pass and draw a panel afterwards (a UI phase). Production hosts call
|
||
/// `raw_input_hook`, then `tick` in `App::logic` and `ui` in `App::ui`; `tick` alone never
|
||
/// takes input, so a hidden logic-only iteration cannot replay the last shown frame's events.
|
||
///
|
||
/// Outside tests this is just [`Self::tick`] with an iteration boundary.
|
||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||
self.begin_host_iteration();
|
||
self.tick(ctx);
|
||
#[cfg(test)]
|
||
{
|
||
let confined = self.synthetic_frame.then(|| self.automation_roots.clone()).flatten();
|
||
let _ = file_access::confine(confined.as_ref(), || {
|
||
vectorcraft_engine::guard::catch_panic(|| {
|
||
self.ui_setup(ctx);
|
||
self.handle_input_once(ctx);
|
||
})
|
||
});
|
||
}
|
||
self.finish_ime_logic(ctx);
|
||
}
|
||
|
||
/// First UI-pass setup, in a real `run_ui` phase: adopt the context, install the interface
|
||
/// fonts and theme, and the image loaders. 0.37's `set_fonts` applies at once when no text has
|
||
/// been laid out, so the same pass can draw; a hidden logic-only iteration never installs them.
|
||
fn ui_setup(&mut self, ctx: &egui::Context) {
|
||
// Hosts install before their first pass. Installing here covers later passes only: egui
|
||
// has already snapshotted this pass's plugin list, so a direct harness that calls `logic`
|
||
// inside its first `run_ui` must call `install_ime_reset_hooks` before that `run_ui`
|
||
// (as `tests_lifecycle::ime_frame` does). This is not a first-pass bootstrap fallback.
|
||
crate::ime::install_hooks(ctx);
|
||
self.adopt_context(ctx);
|
||
if !self.styled {
|
||
// The saved accent, color scheme and canvas color, not the defaults (#4837).
|
||
prefs_dialog::reapply(self, ctx);
|
||
egui_extras::install_image_loaders(ctx);
|
||
self.styled = true;
|
||
}
|
||
if !self.fonts_ready {
|
||
// `set_fonts` applies at once only while no text was laid out in this pass; when it
|
||
// defers, the next pass applies it and this leaves input and drawing to that pass.
|
||
let fonts = theme::interface_fonts();
|
||
ctx.set_fonts(fonts.clone());
|
||
self.fonts_ready = ctx.fonts(|applied| applied.definitions() == &fonts);
|
||
}
|
||
}
|
||
|
||
/// The canvas panel's body: the document tabs, the canvas (a pane for each document while
|
||
/// Window › Arrange › Tile is on) and the isolation bar over it. While the current document is
|
||
/// in a floating window, the tab strip's document draws as a pane.
|
||
pub(crate) fn canvas_area(&mut self, ui: &mut egui::Ui) {
|
||
let t = theme::Tokens::get(ui.ctx());
|
||
// Tools may already occupy the left side of this panel.
|
||
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, t.pasteboard);
|
||
self.canvas.windows.strip = None;
|
||
if self.ui.screen_mode < 3 {
|
||
chrome::doc_tabs(self, ui);
|
||
}
|
||
self.canvas.windows.area = Some(ui.available_rect_before_wrap());
|
||
let floats = crate::doc_windows::current_floats(self);
|
||
if !canvas::tiled_area(self, ui) {
|
||
if floats {
|
||
let rect = ui.available_rect_before_wrap();
|
||
if let Some(w) = crate::doc_windows::tab_window(self)
|
||
&& canvas::pane(self, ui, w, rect)
|
||
{
|
||
crate::doc_windows::show_window(self, w);
|
||
}
|
||
} else {
|
||
canvas::show(self, ui);
|
||
}
|
||
}
|
||
// Over the canvas inside the rulers, so the rulers and scroll bars stay uncovered.
|
||
if self.ui.screen_mode < 3
|
||
&& !floats
|
||
&& !menus::home_showing(self)
|
||
&& let Some(area) = self.canvas_rect
|
||
{
|
||
chrome::isolation_bar(self, ui, area);
|
||
}
|
||
}
|
||
|
||
/// Lay out the whole window: the first UI pass of a host iteration sets the UI up (fonts,
|
||
/// theme, image loaders) and takes the input; later passes (a `request_discard` rerun) draw
|
||
/// only.
|
||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||
let ctx = ui.ctx().clone();
|
||
crate::ime::begin_ui(self, &ctx);
|
||
let confined = self.synthetic_frame.then(|| self.automation_roots.clone()).flatten();
|
||
if let Err(msg) = file_access::confine(confined.as_ref(), || {
|
||
vectorcraft_engine::guard::catch_panic(|| {
|
||
self.ui_setup(&ctx);
|
||
self.handle_input_once(&ctx);
|
||
self.ui_frame(ui);
|
||
})
|
||
}) {
|
||
self.picks.recover();
|
||
self.session.set_replaying(false);
|
||
self.status(format!("Internal error: {msg} (please report this bug)"));
|
||
}
|
||
crate::ime::finish_ui(self, &ctx);
|
||
}
|
||
|
||
/// [`Self::ui`]'s work, inside its guard: the bars, panels and canvas, then the dialogs and
|
||
/// progress windows over them.
|
||
fn ui_frame(&mut self, ui: &mut egui::Ui) {
|
||
let ctx = ui.ctx().clone();
|
||
if !self.fonts_ready {
|
||
// Do not draw (or run the app's input handling) before the interface fonts are the
|
||
// context's: a deferred `set_fonts` applies at the start of the next pass.
|
||
ctx.request_repaint();
|
||
return;
|
||
}
|
||
crate::state::sync_zoom_stops_display_print_size(self.session.prefs.display_print_size);
|
||
// Whether this pass draws the last step of an action playing at Step by Step.
|
||
let step_held = crate::panels::actions::playback_held(self);
|
||
let t0 = now_ms();
|
||
widgets::set_dialog_open(&ctx, self.ui.dialog.is_some());
|
||
// The canvas records this pass's IME caret geometry; a pass that draws no canvas has none.
|
||
self.ime_caret_rect = None;
|
||
scrub::begin_frame(self, &ctx);
|
||
font_menu::end_stale_preview(self, &ctx);
|
||
tools_float::track(self, &ctx);
|
||
// A selection move released over a panel ends before any panel sees the release (#2826).
|
||
canvas::release_over_panel(self, &ctx);
|
||
let t = theme::Tokens::get(&ctx);
|
||
if self.ui.screen_mode < 2 {
|
||
chrome::app_bar(self, ui);
|
||
if self.ui.control_bar {
|
||
chrome::control_bar(self, ui);
|
||
}
|
||
}
|
||
if self.ui.status_bar && self.ui.screen_mode < 3 {
|
||
chrome::status_bar(self, ui);
|
||
}
|
||
if self.ui.help_bar && self.ui.screen_mode < 3 {
|
||
chrome::hint_bar(self, ui);
|
||
}
|
||
if (self.ui.screen_mode >= 3 || (!self.ui.status_bar && !self.ui.help_bar))
|
||
&& !(self.ui.control_bar && self.ui.control_bar_bottom && self.ui.screen_mode < 2)
|
||
{
|
||
// Keeps the previous frame's value valid for menus drawn above, before the bars. A
|
||
// Control bar docked at the bottom has set it to the bar's top.
|
||
widgets::set_menu_floor(&ctx, ctx.content_rect().bottom());
|
||
}
|
||
if self.ui.toolbar && self.ui.screen_mode < 3 {
|
||
tools_float::set_window_dock_zone(&ctx, ui.available_rect_before_wrap());
|
||
}
|
||
if !self.ui.tools_beside_canvas {
|
||
// A canvas-edge target left from before the setting was turned off docks nothing.
|
||
tools_float::clear_canvas_dock_zone(&ctx);
|
||
}
|
||
// Beside the canvas, docked Tools draws inside the canvas panel (`toolbar::show_beside_canvas`).
|
||
// So does Tools docked on the right, left of the panels.
|
||
if self.ui.toolbar && self.ui.screen_mode < 3 && ((!self.ui.tools_beside_canvas && !self.ui.toolbar_right) || self.ui.toolbar_pos.is_some()) {
|
||
toolbar::show(self, ui);
|
||
} else if !self.ui.toolbar && self.ui.screen_mode < 3 && self.ui.tab_hid.is_none() {
|
||
// Window › Tools hides the Tools panel alone; Tab hides the toolbars made with New
|
||
// Toolbar too.
|
||
toolbar::show_custom(self, ui.ctx());
|
||
}
|
||
panels::library_panel::tidy(self);
|
||
// The canvas and the panels: one docking layout. Tab hides the panels but not the canvas.
|
||
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| panel_docking::show(self, ui));
|
||
crate::doc_windows::show(self, &ctx);
|
||
dialogs::show(self, &ctx);
|
||
updates_ui::show(self, &ctx);
|
||
export_progress::show(self, &ctx);
|
||
plugin_runs::show(self, &ctx);
|
||
trace_runs::show(self, &ctx);
|
||
opening::show(self, &ctx);
|
||
palette::show(self, &ctx);
|
||
// After every panel had its chance to draw in this pass.
|
||
crate::panels::three_d::end_stale_drag(self, &ctx);
|
||
if self.custom_titlebar {
|
||
titlebar::resize_zones(ui);
|
||
}
|
||
self.ui_fonts.frame(&ctx);
|
||
if self.ui.dialog.is_none() && !self.ui.palette_open {
|
||
shortcuts::remember_text_focus(&ctx);
|
||
}
|
||
scrub::end_frame(self, &ctx);
|
||
native_menu::sync(self, &ctx);
|
||
// Observe once, after all of this pass's input, dialogs, floating panels, palette and
|
||
// commands: the latest completed pass is authoritative for owner, text and cause.
|
||
crate::ime::end_of_pass(self, &ctx);
|
||
self.drop_formats_after_egui_copy(&ctx);
|
||
// A command run by a menu, panel or dialog in this pass may have queued its text copy
|
||
// after the UI phase's input flush. Publish it now for the same reason as there.
|
||
self.flush_clipboard_out(&ctx);
|
||
if opening::long_operation_busy(self) {
|
||
ctx.set_cursor_icon(egui::CursorIcon::Progress);
|
||
}
|
||
// A pass egui will discard and rerun is not a presented frame: it must not become the
|
||
// frame-time sample, and it must not issue a capture of what it drew.
|
||
if !ctx.will_discard() {
|
||
self.perf.frame_ms = now_ms() - t0;
|
||
self.issue_screenshots(&ctx);
|
||
// A hidden window's pass shows nothing, so the step waits for a pass the user sees.
|
||
if step_held && !self.viewport_hidden(&ctx) {
|
||
crate::panels::actions::playback_drawn(self);
|
||
}
|
||
}
|
||
let _ = json!(null);
|
||
}
|
||
}
|