Files
pdfcraft/crates/ui-egui/src/app.rs
T
FredAppsandClaude Opus 5.5 29913dcfca Merge current main into #2155
Brings the branch up to date with main (47f42ba0), which moved 37 commits since
the merge base, including page-edge shadows (#2088), full rewrite after page
delete (#4279) and the licence-check stage (#2212). No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:23:43 +01:00

698 lines
30 KiB
Rust

use super::hooks::OsEvent;
use super::state::{
Dialog, DocumentCopyJob, GridFile, LeftPanel, LinkOrigin, Mode, NumberDraft, PasswordPrompt, PendingLink, PropsTab, QuickTool, RecentFile,
RightPanel, UnlockPrompt,
};
#[cfg(not(target_arch = "wasm32"))]
use super::state::{OpenOutcome, OpenRequest, PendingRemoteLink};
use super::*;
mod clipboard;
mod commands;
mod control;
mod files;
mod frame;
mod input;
mod keys;
mod links;
mod prefs;
mod window;
pub(crate) use clipboard::{copy_refused, print_refused};
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use files::same_file;
use frame::migrate_panel_state;
pub(crate) use prefs::{MAX_AUTHOR_CHARS, date_text_for_pdf};
impl Default for PdfCraftApp {
fn default() -> Self {
Self::new()
}
}
impl PdfCraftApp {
/// The window's title and, on the web, the page's, from the active document (#3640). Done in
/// `logic` too, which also runs while the window or browser tab is hidden: a document opened
/// then (a `?file=` download finishing) still names the page an embedding host reads.
fn sync_titles(&mut self, ctx: &egui::Context) {
// The window shows the active document's name (or title, if it asks for that), with •
// while it has unsaved changes, as the other Craft apps write it (#3640). Unsaved means
// what closing would ask about, so a draft not yet applied counts too.
let dirty = self.active.is_some_and(|i| self.has_unsaved_work(i));
let mark = if dirty { " •" } else { "" };
let titled = |name: Option<String>| {
name.or_else(|| self.combine_showing().then(|| tl!("Combine files").to_owned()))
.map_or_else(|| "PdfCraft".to_owned(), |name| format!("{name}{mark} — PdfCraft"))
};
let title = titled(self.active.and_then(|i| crate::canvas::tab_label(self, i)));
// The web page's title is what an embedding host reads to tell which file opened, so it
// names the file even when the document asks to show its own title.
let page_title = titled(self.active.and_then(|i| self.views.get(i)).and_then(|v| self.session.get(v.id)).map(|d| d.name.clone()));
if title != self.window_title {
ctx.send_viewport_cmd(egui::ViewportCommand::Title(title.clone()));
self.window_title = title;
}
if page_title != self.page_title {
// eframe's web runner ignores Title: set the page's own.
#[cfg(target_arch = "wasm32")]
if let Some(document) = web_sys::window().and_then(|w| w.document()) {
document.set_title(&page_title);
}
self.page_title = page_title;
}
}
/// Construct an application with the default session, panels, theme and control state.
pub fn new() -> Self {
Self {
session: Session::new(),
views: Vec::new(),
closed_tabs: Vec::new(),
active: None,
mode: Mode::AllTools,
default_mode: Mode::AllTools,
view_defaults: Default::default(),
units: Default::default(),
date_format_draft: None,
mode_override: None,
left: LeftPanel::AllTools,
side_panels: Default::default(),
left_open: true,
right: None,
last_right: RightPanel::Comments,
right_before_read: None,
comments_panel_closed: false,
right_panel_closed: false,
quick_tool: QuickTool::Select,
space_hand: None,
full_screen_tool: None,
comment_prefs: Default::default(),
theme: ThemeKind::Light,
// A fresh install follows the desktop's appearance (#1248); saved settings override.
theme_preference: ThemePreference::System,
reading_colors: Default::default(),
show_page_edge_shadows: true,
#[cfg(target_os = "linux")]
desktop_theme: None,
#[cfg(target_os = "linux")]
desktop_theme_enabled: false,
scroll_speed: scroll_speed::ScrollSpeed::default(),
animation_effects: motion::AnimationEffects::default(),
motion: motion::Motion::default(),
language: i18n::AUTO.to_string(),
ui_scale: None,
system_text_scale: 1.0,
ui_scale_applied: None,
flatten_fill_sign_on_save: false,
fill_text_size: fill_sign::TEXT_SIZE,
fill_text_color: [0.0; 3],
dialog: None,
updates: updates::Updates::default(),
printer_source: None,
options_source: None,
print_tasks: print_ui::PrintTasks::default(),
palette_open: false,
palette_query: String::new(),
all_tools_expanded: false,
all_tools_full: false,
all_tools_query: String::new(),
all_tools_tab: (None, false),
recent: Vec::new(),
reopen_last_session: false,
restore_last_view: true,
last_views: Vec::new(),
last_session: Default::default(),
quit_session: None,
reload_changed_files: true,
show_secured_notice: true,
always_default_view: false,
#[cfg(not(target_arch = "wasm32"))]
watch: Default::default(),
#[cfg(not(target_arch = "wasm32"))]
open_worker: None,
#[cfg(not(target_arch = "wasm32"))]
pending_opens: Vec::new(),
#[cfg(not(target_arch = "wasm32"))]
next_open_token: 0,
#[cfg(not(target_arch = "wasm32"))]
launch_layout: None,
#[cfg(not(target_arch = "wasm32"))]
launch_display_target: None,
#[cfg(all(test, not(target_arch = "wasm32")))]
open_prepare_gate: None,
pinned: Default::default(),
pinned_tools: Vec::new(),
pin_tool_search: String::new(),
toast: None,
toast_script: None,
script_messages: std::collections::VecDeque::new(),
script_messages_dropped: 0,
toast_dropped: 0,
integrated_titlebar: false,
window_controls: None,
system_title_bar: false,
app_frame_shown: None,
password_prompt: None,
document_copy: None,
unlock_prompt: None,
full_screen: false,
inbox: Default::default(),
startup_inbox: Default::default(),
startup_superseded: false,
failed_inbox: Default::default(),
unsaved_flag: Default::default(),
os_events: None,
host_save: None,
host_dirty: None,
close_request: None,
save_prompt_button: Default::default(),
save_prompt_seen: false,
save_prompt_attention: false,
#[cfg(not(target_arch = "wasm32"))]
pending_overwrite: None,
save_override: None,
props_draft: None,
view_draft: None,
custom_draft: None,
requests: Default::default(),
#[cfg(not(target_arch = "wasm32"))]
pickers: Default::default(),
#[cfg(not(target_arch = "wasm32"))]
pick_override: None,
export_dir_override: None,
split_draft: SplitDraft::default(),
extract_draft: ExtractDraft::default(),
rotate_draft: RotateDraft::default(),
summary_sort: Default::default(),
sign_draft: None,
digital_ids: Vec::new(),
sig_expanded: Vec::new(),
a11y_options: a11y_ui::A11yOptions::default(),
ocr_draft: ocr_ui::OcrDraft::default(),
ocr_run: None,
ocr_batch: None,
run_inline: false,
custom_actions: Vec::new(),
wizard: Default::default(),
action_run: None,
action_files_override: None,
pdfa: Default::default(),
compare_old: None,
compare: None,
js_console: Default::default(),
doc_js: Default::default(),
a11y: a11y_ui::A11yState::default(),
tags_state: Default::default(),
a11y_skipped: Default::default(),
alt_draft: Default::default(),
os_key_store_ids: false,
cert_viewer: None,
space_audit: Vec::new(),
space_audit_of: None,
combine_draft: Vec::new(),
combine_conflicts: Default::default(),
combine_tab: Default::default(),
combine_columns: Default::default(),
combine_view: Default::default(),
combine_zoom: 1.0,
organize_zoom: 1.0,
combine_thumbs: Default::default(),
image_import: None,
custom_stamps: Vec::new(),
stamp_draft: Default::default(),
optimize_draft: OptimizeDraft::default(),
optimize_run: None,
progress_notice: None,
page_clipboard: None,
grid_drop: None,
last_snapshot: None,
last_copied_image: None,
last_copied_file: None,
last_copied_image_rect: None,
system_clipboard: true,
clipboard_override: None,
v_pressed: false,
frame_count: 0,
last_command_frame: None,
attach_override: None,
duplicate_draft: None,
page_props_draft: None,
field_table_draft: None,
form_preview: false,
window_title: String::new(),
page_title: String::new(),
recovery: None,
recoverable: Vec::new(),
recovery_keys: Default::default(),
last_autosave: None,
allow_quit: false,
deferred_commands: Vec::new(),
native_menu: None,
menu_input: Vec::new(),
dialog_seen: None,
dialog_epoch: 0,
ctx: None,
#[cfg(not(target_arch = "wasm32"))]
window_state: Default::default(),
styled: false,
fonts_ready: false,
fonts_cjk: theme::CjkPreference::Default,
control: None,
bookmark_rename: None,
last_opened_url: None,
community_card_closed: false,
pending_link: None,
pending_full_screen: None,
pending_remote_link: None,
protect_draft: Default::default(),
text_style_draft: Default::default(),
boxes_draft: Default::default(),
marks_draft: Default::default(),
unmarked_page: Default::default(),
export_draft: Default::default(),
export_status: None,
signature: None,
initials: None,
signature_draft: Default::default(),
signature_preview: None,
signature_photo: None,
saved_signature_previews: [None, None],
sign_graphic: sign_ui::SignGraphic::Name,
sign_graphic_preview: None,
#[cfg(target_arch = "wasm32")]
signature_images: Default::default(),
comment_props: None,
field_props: None,
bulk_field_props: None,
redact_prefs: RedactPrefs::default(),
redact_defaults: RedactPrefs::default(),
redact_before: None,
redact_pages_draft: RedactPagesDraft::default(),
redact_search: RedactSearchDraft::default(),
hidden_draft: HiddenDraft::default(),
print_draft: PrintDraft::default(),
link_draft: None,
text_style: content_ui::default_style(),
replace_draft: None,
number_draft: NumberDraft { from: 1, to: 1, style: pdfcraft_engine::LabelStyle::Decimal, prefix: String::new(), start: 1 },
word_count_draft: word_count_ui::Draft::default(),
}
}
/// Fields are being edited (Prepare a form or its Calculation order is open, or a field tool
/// is picked), unless the form is being previewed.
pub fn is_preparing(&self) -> bool {
((self.left_open && matches!(self.left, LeftPanel::Tool("form") | LeftPanel::CalculationOrder))
|| matches!(self.quick_tool, QuickTool::Field(_)))
&& !self.form_preview
}
/// A number that changes whenever the open dialog changes (opened, closed or replaced by
/// another), checked every frame. A picker started from a dialog's Browse… button answers
/// only into the same showing of that dialog.
pub(crate) fn dialog_epoch(&mut self) -> u64 {
if self.dialog != self.dialog_seen {
self.dialog_seen = self.dialog;
self.dialog_epoch = self.dialog_epoch.wrapping_add(1);
}
self.dialog_epoch
}
pub fn active_ids(&self) -> Option<(usize, DocId)> {
self.active.and_then(|i| self.views.get(i).map(|v| (i, v.id)))
}
/// A dialog or prompt is open over the window: the keyboard is its own, and nothing may act
/// on the document underneath it.
pub(crate) fn modal_open(&self) -> bool {
self.dialog.is_some()
|| self.close_request.is_some()
|| self.password_prompt.is_some()
|| self.unlock_prompt.is_some()
|| self.pending_link.is_some()
|| self.pending_remote_link.is_some()
|| self.updates.open
}
/// Show `msg` in the toast now, in place of whatever it showed. Script messages still waiting
/// show after it.
pub fn notify(&mut self, msg: impl Into<String>) {
// A left-out count it replaces is said again once the toast is free.
self.script_messages_dropped = self.script_messages_dropped.saturating_add(std::mem::take(&mut self.toast_dropped));
self.toast = Some((msg.into(), 0.0));
self.toast_script = None;
}
/// Notify with a static message in the UI language.
pub fn notify_tr(&mut self, text: &str) {
self.notify(tl!(text).to_string());
}
/// Notify with an error's own text. Engine and OS messages are not translated: matching their
/// English wording would break silently whenever it changes.
pub fn notify_error(&mut self, error: impl std::fmt::Display) {
self.notify(error.to_string());
}
/// Notify with a `{name}`-style template in the UI language (placeholders filled once).
pub fn notify_fmt(&mut self, template: &str, args: &[(&str, &str)]) {
self.notify(i18n::fmt(tl!(template), args));
}
/// `true` while any open document still waits for page renders (used by headless capture).
pub fn render_pending(&self) -> bool {
self.views.iter().any(|v| v.render_pending())
}
/// Check whether a tool group is pinned to Home.
pub fn is_tool_pinned(&self, id: &str) -> bool {
self.pinned_tools.iter().any(|t| t == id)
}
/// Pin a tool group to Home. Returns true if newly pinned.
pub fn pin_tool(&mut self, id: &str) -> bool {
if pdfcraft_engine::catalog::group(id).is_none() {
return false;
}
if !self.is_tool_pinned(id) {
self.pinned_tools.push(id.to_string());
if let Some(g) = pdfcraft_engine::catalog::group(id) {
self.notify_fmt("Pinned {tool} to Home", &[("tool", tl!(g.label))]);
}
return true;
}
false
}
/// Unpin a tool group from Home.
pub fn unpin_tool(&mut self, id: &str) {
if let Some(pos) = self.pinned_tools.iter().position(|t| t == id) {
self.pinned_tools.remove(pos);
if let Some(g) = pdfcraft_engine::catalog::group(id) {
self.notify_fmt("Unpinned {tool} from Home", &[("tool", tl!(g.label))]);
}
}
}
/// Toggle a tool group's pinned state on Home. Returns whether it is now pinned.
pub fn toggle_pin_tool(&mut self, id: &str) -> bool {
if self.is_tool_pinned(id) {
self.unpin_tool(id);
false
} else {
self.pin_tool(id)
}
}
}
impl eframe::App for PdfCraftApp {
/// While files are dragged over the window the system sends no pointer moves, so egui
/// would keep the place the pointer entered at: tell it where the pointer really is, so the
/// page grid can show (and use) the gap under it.
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
// The button's release a window move or resize kept from egui.
window_controls::release_after_desktop_grab(ctx, raw);
self.prepare_raw_input(ctx, raw);
if raw.hovered_files.is_empty() && raw.dropped_files.is_empty() {
return;
}
if let Some(pos) = drag_pointer::in_window(ctx) {
raw.events.push(egui::Event::PointerMoved(pos));
}
ctx.request_repaint();
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
// eframe has just stored the window as it is now: not while it is minimized.
#[cfg(not(target_arch = "wasm32"))]
{
let minimized = self.ctx.as_ref().is_some_and(|c| c.input(|i| i.viewport().minimized == Some(true)));
self.window_state.keep(storage, minimized);
}
storage.set_string("pdfcraft", self.persist());
}
/// Each frame's work before drawing: opens, operating-system and control-channel requests,
/// the quit guard, shortcuts, and the background jobs' results.
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.ctx = Some(ctx.clone());
#[cfg(not(target_arch = "wasm32"))]
self.window_state.repair(ctx);
self.dialog_epoch();
// Notices raised outside `ui` (opened files, OS events, the control channel) translate too.
let lang = i18n::Lang::from_pref(&self.language);
i18n::set_current(lang);
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(worker) = &self.open_worker {
worker.set_context(ctx);
}
self.poll_pending_opens();
}
// Chinese wants its own faces before the Japanese ones (one baseline per line).
let cjk = theme::CjkPreference::for_language(lang.code());
if !self.styled {
egui_extras::install_image_loaders(ctx);
theme::install_fonts_for(ctx, cjk);
self.fonts_cjk = cjk;
theme::apply(ctx, self.theme);
self.styled = true;
} else {
self.fonts_ready = true;
if cjk != self.fonts_cjk {
// Same family names as before, so named fonts stay valid while the new set loads.
theme::install_fonts_for(ctx, cjk);
self.fonts_cjk = cjk;
}
}
self.sync_theme(ctx);
let native = ctx.native_pixels_per_point().unwrap_or(1.0);
ui_scale::sync(ctx, ui_scale::zoom(self.ui_scale, self.system_text_scale, native), &mut self.ui_scale_applied);
// Remember user work even if its tab is subsequently closed or Home is selected.
self.startup_superseded |= !self.views.is_empty() || self.combine_showing() || self.dialog.is_some() || self.password_prompt.is_some();
// Also done in `raw_input_hook`, before input; here for hosts that don't call the hook.
self.scroll_speed.set_line_speed(ctx);
// Motion reads this frame's wheel input, so it begins in `ui`, in a known UI phase:
// `logic` also runs alone while hidden, where the input events and time are stale.
// Before taking this frame's drop: the grid must be drawn once with the pointer where
// the files were let go before the gap is read.
self.finish_grid_drop(ctx);
// Showing a document hides the Combine files tab.
if self.active.is_some() {
self.combine_tab.focused = false;
}
#[cfg(target_arch = "wasm32")]
self.process_signature_images();
let dropped = ctx.input(|i| i.raw.dropped_files.clone());
#[cfg(not(target_arch = "wasm32"))]
let dropped = self.drop_on_signature_pad(dropped);
#[cfg(not(target_arch = "wasm32"))]
let dropped = self.drop_on_grid(dropped, ctx);
for f in dropped {
// Files dropped on the Combine files tab join its list instead of opening.
if self.combine_showing() {
self.drop_into_combine(f, ctx);
} else {
self.open_dropped(f, ctx);
}
}
let arrived: Vec<(String, Vec<u8>)> = self.inbox.lock().map(|mut q| std::mem::take(&mut *q)).unwrap_or_default();
for (name, bytes) in arrived {
if let Err(e) = self.open_bytes(&name, None, bytes) {
self.notify_fmt("Couldn't open {name}: {e}", &[("name", &name), ("e", &e.to_string())]);
}
}
// Don't replace a pending prompt or import. A startup file may need its own password
// prompt, so its bytes stay queued until the current interaction finishes.
#[cfg(not(target_arch = "wasm32"))]
let opening = !self.pending_opens.is_empty();
#[cfg(target_arch = "wasm32")]
let opening = false;
if !opening
&& self.password_prompt.is_none()
&& self.image_import.is_none()
&& self.dialog.is_none()
&& self.close_request.is_none()
&& self.pending_link.is_none()
&& self.pending_remote_link.is_none()
{
let startup = self.startup_inbox.lock().map(|mut q| std::mem::take(&mut *q)).unwrap_or_default();
for (name, bytes) in startup {
let activate = !self.startup_superseded;
if let Err(e) = self.open_bytes_with_focus(&name, None, bytes, activate) {
self.notify_fmt("Couldn't open {name}: {e}", &[("name", &name), ("e", &e)]);
}
}
}
let failed: Vec<(String, String)> = self.failed_inbox.lock().map(|mut q| std::mem::take(&mut *q)).unwrap_or_default();
for (name, e) in failed {
self.notify_fmt("Couldn't open {name}: {e}", &[("name", &name), ("e", &e)]);
}
let os_events = self.os_events.as_mut().map(|poll| poll()).unwrap_or_default();
for e in os_events {
match e {
#[cfg(not(target_arch = "wasm32"))]
OsEvent::Open(paths) => {
paths.iter().for_each(|p| {
self.open_path(p);
});
// Files handed to the running app (Finder, or another launch): bring its
// window to the front, also when minimized or full screen on another Space.
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
}
#[cfg(target_arch = "wasm32")]
OsEvent::Open(_) => {}
// Like closing the window: `guard_quit` asks about unsaved changes.
OsEvent::Quit => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
}
}
if let Some((_, id)) = self.active_ids() {
let out = self.session.take_js_output(id);
self.handle_js(id, out);
}
if let Some(mut control) = self.control.take() {
control.tick(ctx, self);
self.control = Some(control);
}
self.guard_quit(ctx);
// macOS: quit through `terminate:` instead of dropping the window (Touch Bar crash, #1136).
#[cfg(target_os = "macos")]
craft_quit::on_close_request(ctx);
self.autosave_tick(ctx, web_time::Instant::now());
// Reload documents whose files a build rewrote (#431).
#[cfg(not(target_arch = "wasm32"))]
self.watch_files();
if self.host_dirty.is_some() {
let dirty = self.first_dirty().is_some();
if let Some(report) = self.host_dirty.as_mut() {
report(dirty);
}
}
self.poll_updates();
self.poll_print();
// Shortcuts deferred last frame: the text field has taken that frame's typing since.
let deferred = std::mem::take(&mut self.deferred_commands);
self.native_menu_clicks(ctx);
// The keys a menu click stands for (⌘C, Delete…), before anything reads this frame's keys.
if !self.menu_input.is_empty() {
let events = std::mem::take(&mut self.menu_input);
ctx.input_mut(|i| i.events.extend(events));
}
self.shortcuts(ctx);
// Scrolling and group-drag previews are transient: never resume after changing tabs,
// opening a modal/palette, or returning to a window that lost focus.
let blocked = self.modal_open() || self.palette_open || !ctx.input(|i| i.focused);
for (index, view) in self.views.iter_mut().enumerate() {
view.objects.set_input_blocked(blocked || self.active != Some(index));
if blocked || self.active != Some(index) {
view.auto_scroll.cancel();
}
}
// A field that refused its value this frame keeps the shortcut from saving or printing
// behind the user's back; so does a save prompt opened since.
if self.process_pending_edits() && self.close_request.is_none() {
for (id, doc) in deferred {
// Only in the document the shortcut was pressed in.
if self.active_ids().map(|(_, active)| active) == doc {
self.execute(id);
}
}
}
self.poll_export();
self.auto_ocr_scanned_page();
self.poll_ocr();
self.poll_optimize();
self.poll_action();
self.process_file_requests();
#[cfg(not(target_arch = "wasm32"))]
self.process_picked();
// Last, after everything that can open or answer the "save changes?" question.
self.attend_save_prompt(ctx);
self.sync_native_menu(ctx);
self.sync_titles(ctx);
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
i18n::set_current(i18n::Lang::from_pref(&self.language));
// A language change switches stock chrome between LTR and RTL before anything is drawn.
theme::sync_direction(&ctx);
// Fonts registered via set_fonts only take effect next frame; named families would panic now.
if !self.fonts_ready {
ctx.request_repaint();
return;
}
// Before any widget reads this frame's wheel input or draws.
self.motion.begin_pass(&ctx, self.animation_effects);
// One-time 0.36 -> 0.37 panel persistence migration, before any panel stores or reads state.
migrate_panel_state(ui);
for view in &mut self.views {
view.begin_render_frame();
}
self.sync_titles(&ctx);
// The accessible window takes the same name, so a screen reader (AT-SPI on Linux, UIA on
// Windows) can tell it apart (#4614). egui builds the root node afresh every pass.
ctx.accesskit_node_builder(egui::accesskit_root_id(), |root| root.set_label(self.window_title.as_str()));
if self.full_screen && self.active.is_some() {
// Full screen: the page on black, nothing else, as Acrobat's Full Screen Mode (Esc
// or ⌘L to leave).
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(egui::Color32::BLACK)).show(ui, |ui| {
if let Some(i) = self.active {
match compare_ui::side_by_side(self, i) {
Some((old, new)) => compare_ui::compare_area(self, old, new, ui),
None => canvas::document_area(self, i, ui),
}
}
});
dialogs::show(self, &ctx);
// Notices too: a refused field value or a failed save must be seen in full screen.
self.show_progress(&ctx);
widgets::toast(self, &ctx);
self.finish_render_frame(&ctx);
if let Some(mut control) = self.control.take() {
control.finish_frame(&ctx, self);
self.control = Some(control);
}
return;
}
self.sync_window_frame(&ctx);
if self.draws_window_frame() {
window_controls::resize_edges(&ctx);
}
chrome::tab_strip(self, ui);
chrome::mode_bar(self, ui);
// Whichever way another tab came to the front, it shows instead of the expanded view.
if self.all_tools_full && self.all_tools_tab != self.shown_tab() {
self.all_tools_full = false;
}
side_panels::show(self, ui);
let t = theme::Tokens::get(&ctx);
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| {
if self.mode == Mode::AllTools && self.all_tools_full {
all_tools_ui::show(self, ui);
} else {
match self.active {
None if self.combine_showing() => combine_ui::page(self, ui),
None => home::show(self, ui),
Some(index) => match compare_ui::side_by_side(self, index) {
Some((old, new)) => compare_ui::compare_area(self, old, new, ui),
None => canvas::document_area(self, index, ui),
},
}
}
});
self.process_pending_edits();
palette::show(self, &ctx);
dialogs::show(self, &ctx);
self.show_progress(&ctx);
widgets::toast(self, &ctx);
self.finish_render_frame(&ctx);
// Screenshots are issued only after a settled pass that really drew the window.
if let Some(mut control) = self.control.take() {
control.finish_frame(&ctx, self);
self.control = Some(control);
}
}
}