mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
161 lines
8.9 KiB
Rust
161 lines
8.9 KiB
Rust
use super::{Arc, ClipboardProbeFactory, Mutex, SystemClipboard, background, native_menu, picks, place, print, screen_color, stylus};
|
|
|
|
/// A file type a dialog filters by: its label, which shows translated, and its extensions without
|
|
/// the dot.
|
|
pub type FileFilter = (String, Vec<String>);
|
|
|
|
/// What a file dialog shows: a suggested file name (save dialogs), the folder to start in and the
|
|
/// file-type filters.
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct FilePick {
|
|
pub name: String,
|
|
pub folder: Option<String>,
|
|
/// The default type first; empty = every file.
|
|
pub filters: Vec<FileFilter>,
|
|
}
|
|
|
|
impl FilePick {
|
|
/// A save dialog suggesting `name`.
|
|
pub fn named(name: &str) -> Self {
|
|
Self { name: name.to_string(), ..Default::default() }
|
|
}
|
|
|
|
/// The file type `label` with `extensions`.
|
|
pub fn filter(label: &str, extensions: &[&str]) -> FileFilter {
|
|
(label.to_owned(), extensions.iter().map(|e| (*e).to_owned()).collect())
|
|
}
|
|
|
|
/// The file types of `list`, as the engine lists them: `(label, extensions)`.
|
|
pub fn filters<'a>(list: impl IntoIterator<Item = (&'a str, &'a [&'a str])>) -> Vec<FileFilter> {
|
|
list.into_iter().map(|(label, extensions)| Self::filter(label, extensions)).collect()
|
|
}
|
|
}
|
|
|
|
pub type PickOpen = Box<dyn FnMut(&FilePick) -> Option<String>>;
|
|
|
|
pub type PickSave = Box<dyn FnMut(&FilePick) -> Option<String>>;
|
|
|
|
pub type ReadFn = Box<dyn Fn(&str) -> Result<Vec<u8>, String>>;
|
|
|
|
pub type WriteFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
|
|
|
|
pub type ExistsFn = Box<dyn Fn(&str) -> bool>;
|
|
|
|
pub type Inbox = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
|
|
|
|
/// Starts a browser download of the bytes under a file name; an error when it couldn't be started
|
|
/// (the save or export then fails as a refused write does: #1693).
|
|
pub type DownloadFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
|
|
|
|
/// Opens a URL in the system browser.
|
|
pub type OpenUrlFn = Box<dyn FnMut(&str)>;
|
|
|
|
/// Whether a URL from a document may open in the browser: an `http:`, `https:` or `mailto:` URL
|
|
/// without control characters. The system hands other schemes (`file:`, application schemes) to
|
|
/// other programs.
|
|
pub(crate) fn browsable_url(url: &str) -> Result<(), String> {
|
|
let scheme = url.split_once(':').map_or("", |(s, _)| s);
|
|
if ["http", "https", "mailto"].iter().any(|k| scheme.eq_ignore_ascii_case(k)) && !url.chars().any(char::is_control) {
|
|
Ok(())
|
|
} else {
|
|
Err(format!("only http, https and mailto URLs open in the browser, not “{url}”"))
|
|
}
|
|
}
|
|
|
|
/// Shows a file in the system file manager, or opens it in its app.
|
|
pub type RevealFn = Box<dyn FnMut(&str) -> Result<(), String>>;
|
|
|
|
/// Platform services injected by the host app (desktop or web).
|
|
#[derive(Default)]
|
|
pub struct Services {
|
|
/// Show an open dialog; returns a path.
|
|
pub pick_open: Option<PickOpen>,
|
|
/// Show a save dialog (suggested name, folder, file types); returns a path.
|
|
pub pick_save: Option<PickSave>,
|
|
pub read: Option<ReadFn>,
|
|
pub write: Option<WriteFn>,
|
|
/// Is there a file or folder at a path (desktop)? The app asks before replacing a file that a
|
|
/// save panel's name names once the app gives it an extension ([`picks::ask_to_replace`]).
|
|
/// Without it, no file counts as there.
|
|
pub exists: Option<ExistsFn>,
|
|
/// Files that arrived asynchronously (web open / drops): (name, bytes).
|
|
pub inbox: Option<Inbox>,
|
|
/// Web: trigger a browser download instead of writing a path.
|
|
pub download: Option<DownloadFn>,
|
|
/// Web: start an async open (bytes arrive via `inbox`).
|
|
pub open_async: Option<Box<dyn FnMut()>>,
|
|
/// Read the system clipboard's text (desktop). Without it, pasted text only arrives with
|
|
/// egui's Paste event (web, and keyboard paste everywhere).
|
|
pub clipboard_read: Option<Box<dyn FnMut() -> Option<String>>>,
|
|
/// Open a URL in the system browser (desktop). Without it, egui opens it (a new tab on the web).
|
|
pub open_url: Option<OpenUrlFn>,
|
|
/// Show an open dialog for several files (File → Place…); returns their paths (none: cancelled).
|
|
pub pick_open_multi: Option<Box<dyn FnMut() -> Vec<String>>>,
|
|
/// Web: start an async pick of files to place (they arrive via `place_inbox`).
|
|
pub place_async: Option<Box<dyn FnMut()>>,
|
|
/// Files that arrived asynchronously to be placed (web: picked for Place, or dropped on the
|
|
/// canvas).
|
|
pub place_inbox: Option<place::PlaceInbox>,
|
|
/// The system clipboard with all its formats (desktop): Copy offers text, SVG, PDF and PNG,
|
|
/// Paste takes SVG, PDF, text and bitmaps from other apps. Without it, SVG text only (through
|
|
/// egui and `clipboard_read`).
|
|
pub system_clipboard: Option<Box<dyn SystemClipboard>>,
|
|
/// A second system-clipboard handle, for checking whether Paste has something to take on a
|
|
/// background thread (Linux: an X11 clipboard owner that never answers would freeze the UI).
|
|
/// Taken on the first frame. Without it the check runs on the UI thread.
|
|
pub clipboard_probe: Option<ClipboardProbeFactory>,
|
|
/// File → Show in Folder: select a file in the system file manager (desktop).
|
|
pub reveal: Option<RevealFn>,
|
|
/// Write a file from any thread (desktop): lets Background Save and Export write off the UI
|
|
/// thread ([`background`]). Without it they run at once.
|
|
pub write_shared: Option<background::SharedWriteFn>,
|
|
/// Write a file from any thread with what a filler writes into it (desktop): native saves
|
|
/// stream into the file as they are encoded instead of being held whole in memory first.
|
|
/// Without it they are encoded, then written with `write` / `write_shared`.
|
|
pub stream_shared: Option<background::SharedStreamFn>,
|
|
/// Open a file (or a folder) in the system's default app for it (desktop: Edit Original, Show
|
|
/// Package). Without it (the web) those answer with an error.
|
|
pub open_file: Option<RevealFn>,
|
|
/// Show a folder picker; returns its path (desktop: Relink to Folder, Package).
|
|
pub pick_folder: Option<Box<dyn FnMut() -> Option<String>>>,
|
|
/// Show the system's color sampler, which picks a color anywhere on the screen (macOS, and
|
|
/// browsers with an EyeDropper): Sample Color from Screen ([`screen_color`]). Without it that
|
|
/// command answers with an error and the Color Picker's eyedropper button is gray.
|
|
pub screen_color: Option<screen_color::StartSampler>,
|
|
/// Where Data Recovery keeps its copies when not in a folder (the web's browser storage;
|
|
/// tests): installed into the session ([`vectorcraft_engine::cmd::recovery`]).
|
|
pub recovery_store: Option<std::sync::Arc<dyn vectorcraft_engine::cmd::recovery::RecoveryStore>>,
|
|
/// File → Print: the system's printers and print queue (desktop), the browser's print dialog
|
|
/// (web). Without it Print saves the job as a PDF.
|
|
pub print: Option<Box<dyn print::PrintService>>,
|
|
/// The macOS menu bar, when the desktop app installed one: the in-window menus are hidden then.
|
|
pub native_menu: Option<native_menu::NativeMenu>,
|
|
/// Show file dialogs off the UI thread (desktop Linux, where a dialog in line holds the window
|
|
/// and the compositor finds it not answering, #592) or as sheets on the window (macOS, where a
|
|
/// dialog in line aborts the app, #867): what asked runs again with the answer ([`picks`]).
|
|
/// Without it they are shown in line.
|
|
pub start_pick: Option<picks::StartPick>,
|
|
/// [`Self::start_pick`]'s dialogs are sheets on the window (macOS): while one is open the app
|
|
/// neither quits nor closes documents, as under the modal dialog before.
|
|
pub dialogs_are_sheets: bool,
|
|
/// The window is on Wayland: the tool cursors are painted into it instead of handed to the
|
|
/// compositor as bitmaps ([`cursors::os_cursors`]).
|
|
pub wayland: bool,
|
|
/// On Wayland, no cursor theme can be loaded, so winit sets no pointer and the compositor's
|
|
/// default one would never come back once the canvas hid it: the canvas keeps the system
|
|
/// pointer visible next to the tool glyph ([`cursors::hide_system_pointer`], #2450).
|
|
pub no_cursor_theme: bool,
|
|
/// The pen pressure and pen end the host reads from the system itself (desktop Linux under
|
|
/// X11 and XWayland, macOS and Windows). The canvas uses the pressure when a frame's events carry no touch force,
|
|
/// and the end to switch to the Eraser tool and back. Without it, pen pressure comes from
|
|
/// touch events only ([`stylus`]).
|
|
pub stylus: Option<stylus::StylusFeed>,
|
|
/// The host's rewrites of a window's raw input (pen barrel button, closing the macOS menu
|
|
/// bar's open menu) for floating panels in OS windows of their own: eframe runs the host's
|
|
/// `raw_input_hook` for the main window only.
|
|
pub native_input_hook: Option<NativeInputHook>,
|
|
}
|
|
|
|
/// Rewrites one window's raw input before egui reads it ([`Services::native_input_hook`]).
|
|
pub type NativeInputHook = std::sync::Arc<dyn Fn(&mut egui::RawInput) + Send + Sync>;
|