diff --git a/crates/waybetter-host/src/app.rs b/crates/waybetter-host/src/app.rs index e953f80..9fe0d40 100644 --- a/crates/waybetter-host/src/app.rs +++ b/crates/waybetter-host/src/app.rs @@ -86,6 +86,9 @@ pub struct AppConfig { pub store: Option<&'static crate::store::StoreConfig>, /// The shell's media panel (gallery folders, the clipboard history): serves [`commands::MEDIA`]. pub media: bool, + /// Google Fonts (install, remove, preview; the cache in app data): serves [`commands::GFONTS`] + /// and calls [`SiteHooks::fonts_changed`]. Needs the `net` feature. + pub fonts: bool, } impl AppConfig { @@ -109,9 +112,16 @@ impl AppConfig { home_url: None, store: None, media: false, + fonts: false, } } + /// Serves the Google Fonts commands ([`commands::GFONTS`]). + pub const fn with_fonts(mut self) -> Self { + self.fonts = true; + self + } + /// Serves the media panel's commands ([`commands::MEDIA`]). pub const fn with_media(mut self) -> Self { self.media = true; @@ -202,6 +212,10 @@ pub trait SiteHooks: Send + Sync + 'static { /// A tab webview was created. fn tab_created(&self, _app: &AppHandle) {} + /// A Google font was installed or removed ([`commands::GFONTS`]): give the site tabs their + /// theme, and the font bytes, again. + fn fonts_changed(&self, _app: &AppHandle) {} + /// `tab` was laid out for `item` (existing or new), before it is placed. fn tab_placed(&self, _app: &AppHandle, _tab: &Webview, _item: &TabLayout) {} @@ -248,6 +262,11 @@ fn host_state(app: &AppHandle) -> tauri::State<'_, Host> { app.state::() } +/// The app's site hooks ([`run_app`] must be running). +pub(crate) fn hooks(app: &AppHandle) -> Arc { + host_state(app).hooks.clone() +} + /// The app's config ([`run_app`] must be running). pub fn config(app: &AppHandle) -> AppConfig { host_state(app).config @@ -1115,6 +1134,17 @@ fn store_handler() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + ' |_| false } +/// The Google Fonts commands ([`commands::GFONTS`]): with the `net` feature (downloads) only. +#[cfg(feature = "net")] +fn fonts_handler() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { + crate::fonts_commands::handler() +} + +#[cfg(not(feature = "net"))] +fn fonts_handler() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { + |_| false +} + /// The media panel's commands ([`commands::MEDIA`]). fn media_handler() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { crate::media_commands::handler() @@ -1152,6 +1182,7 @@ impl App { let host_handler = host_handler(); let store_handler = store_handler(); let media_handler = media_handler(); + let fonts_handler = fonts_handler(); let builder = tauri::Builder::::default() // Must be first: a second launch focuses the running window instead of fighting over // the app's data and WebView profile (and hands over its link). @@ -1232,6 +1263,8 @@ impl App { #[cfg(feature = "net")] crate::store_commands::init(&handle, &config); crate::media_commands::init(&handle); + #[cfg(feature = "net")] + crate::fonts_commands::init(&handle); // Safe mode for this run: the launch argument, or Shift held while starting (Windows). crate::safe::set(crate::safe::at_launch(std::env::args(), host::shift_held())); // Focus and layering diagnostics for this run: `--diag` or WAYBETTER_DIAG=1 (app log folder, diag.log). @@ -1250,6 +1283,8 @@ impl App { store_handler(invoke) } else if commands::MEDIA.contains(&invoke.message.command()) { media_handler(invoke) + } else if commands::GFONTS.contains(&invoke.message.command()) { + fonts_handler(invoke) } else if commands::is_host(invoke.message.command()) { host_handler(invoke) } else { diff --git a/crates/waybetter-host/src/commands.rs b/crates/waybetter-host/src/commands.rs index d91278d..48d0547 100644 --- a/crates/waybetter-host/src/commands.rs +++ b/crates/waybetter-host/src/commands.rs @@ -82,9 +82,13 @@ pub const MEDIA: &[&str] = &[ "clip_remove", ]; +/// Shell-only, optional: Google Fonts (what is installed, install, remove, the faces of a family and +/// a preview), served when the app sets `AppConfig::fonts`; needs the `net` feature. +pub const GFONTS: &[&str] = &["gfonts_installed", "gfonts_install", "gfonts_remove", "gfonts_faces", "gfonts_preview"]; + /// Whether `command` is one of the host's (dispatched to the host, never to the app's handler). pub fn is_host(command: &str) -> bool { - SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) + SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) || GFONTS.contains(&command) } /// The capability permission tauri-build generates for `command` (`tabs_sync` → `allow-tabs-sync`). @@ -99,8 +103,8 @@ pub fn permission(command: &str) -> String { /// tauri_build::AppManifest::new().commands(waybetter_host::commands::manifest(&["draft_list"])) /// ``` pub fn manifest(app: &[&'static str]) -> &'static [&'static str] { - let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len()); - for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA) { + let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len() + GFONTS.len()); + for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS) { if !all.contains(command) { all.push(command); } @@ -117,7 +121,7 @@ mod tests { let all = manifest(&["draft_list", "tabs_sync"]); assert_eq!(all[0], "draft_list"); assert_eq!(all.iter().filter(|c| **c == "tabs_sync").count(), 1); - for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA) { + for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS) { assert!(all.contains(command), "{command}"); assert!(is_host(command)); } @@ -127,7 +131,7 @@ mod tests { #[test] fn shell_and_tab_commands_do_not_overlap() { - assert!(!SHELL.iter().any(|command| TAB.contains(command) || SITE.contains(command) || STORE.contains(command) || MEDIA.contains(command))); + assert!(!SHELL.iter().any(|command| TAB.contains(command) || SITE.contains(command) || STORE.contains(command) || MEDIA.contains(command) || GFONTS.contains(command))); assert!(!TAB.iter().any(|command| SITE.contains(command))); } } diff --git a/crates/waybetter-host/src/fonts_commands.rs b/crates/waybetter-host/src/fonts_commands.rs new file mode 100644 index 0000000..c1423db --- /dev/null +++ b/crates/waybetter-host/src/fonts_commands.rs @@ -0,0 +1,66 @@ +//! Google Fonts, native side, for every app that sets [`crate::app::AppConfig::fonts`]: the +//! shell-only `gfonts_*` commands ([`crate::commands::GFONTS`]) over [`crate::fonts`]'s downloads +//! and cache (`fonts.googleapis.com` and `fonts.gstatic.com` only, app-data `fonts/`). Site tabs +//! never request fonts and gain no command: native code hands them cached bytes +//! ([`crate::fonts::page_script_for`]). +//! +//! When a font is installed or removed the app's [`crate::app::SiteHooks::fonts_changed`] runs, so +//! it can give every site tab its theme (and the font bytes) again. + +use crate::fonts::{self, InstalledFont}; +use crate::gfonts::Weights; +use crate::host::shell_only; +use tauri::{AppHandle, Manager, Webview}; + +/// Opens the font cache in app data. Does nothing unless the app set [`crate::app::AppConfig::fonts`]. +pub(crate) fn init(app: &AppHandle) { + if !crate::app::config(app).fonts { + return; + } + if let Ok(dir) = app.path().app_data_dir() { + fonts::init(&dir); + } +} + +fn changed(app: &AppHandle) { + crate::app::hooks(app).fonts_changed(app); +} + +#[tauri::command] +pub(crate) async fn gfonts_installed(webview: Webview) -> Result, String> { + shell_only(&webview)?; + fonts::installed() +} + +#[tauri::command] +pub(crate) async fn gfonts_install(webview: Webview, app: AppHandle, family: String, weights: Weights) -> Result { + shell_only(&webview)?; + let installed = fonts::install(family, weights).await?; + changed(&app); + Ok(installed) +} + +#[tauri::command] +pub(crate) async fn gfonts_remove(webview: Webview, app: AppHandle, family: String) -> Result<(), String> { + shell_only(&webview)?; + fonts::remove(&family)?; + changed(&app); + Ok(()) +} + +#[tauri::command] +pub(crate) async fn gfonts_faces(webview: Webview, family: String) -> Result { + shell_only(&webview)?; + fonts::faces(&family) +} + +#[tauri::command] +pub(crate) async fn gfonts_preview(webview: Webview, family: String, weights: Weights, text: String) -> Result, String> { + shell_only(&webview)?; + fonts::preview(family, weights, text).await +} + +/// The fonts commands ([`crate::commands::GFONTS`]). +pub(crate) fn handler() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { + tauri::generate_handler![gfonts_installed, gfonts_install, gfonts_remove, gfonts_faces, gfonts_preview] +} diff --git a/crates/waybetter-host/src/lib.rs b/crates/waybetter-host/src/lib.rs index e53d9b6..736bca9 100644 --- a/crates/waybetter-host/src/lib.rs +++ b/crates/waybetter-host/src/lib.rs @@ -85,6 +85,8 @@ pub mod app; mod store_commands; #[cfg(feature = "app")] pub mod media_commands; +#[cfg(all(feature = "app", feature = "net"))] +mod fonts_commands; #[cfg(all(feature = "app", debug_assertions))] pub mod devloop; #[cfg(feature = "tauri")]