Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab3d17e067 | |||
| f864f15d79 | |||
| 402bb38a59 | |||
| e51ad0cd72 |
Generated
+35
-35
@@ -1093,7 +1093,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=5c874d51eeb3de70e735c8ce87a59a7dedd47675#5c874d51eeb3de70e735c8ce87a59a7dedd47675"
|
||||
source = "git+https://git.marcospaulo.dev.br/masi/craft-libs?rev=22544b6d619d4f16779b8bdfa3f0b642a86fc065#22544b6d619d4f16779b8bdfa3f0b642a86fc065"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"serde",
|
||||
@@ -3552,7 +3552,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -3572,7 +3572,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-a11y"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"pdfcraft-content",
|
||||
"pdfcraft-cos",
|
||||
@@ -3583,7 +3583,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-annot"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"hayro-syntax",
|
||||
"pdfcraft-cos",
|
||||
@@ -3594,7 +3594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-audit"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"pdfcraft-content",
|
||||
"pdfcraft-cos",
|
||||
@@ -3605,7 +3605,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-automation"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"image",
|
||||
@@ -3622,7 +3622,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-cli"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"pdfcraft-audit",
|
||||
@@ -3637,18 +3637,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-compare"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-content"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"pdfcraft-cos",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-cos"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"hayro-syntax",
|
||||
"pdfcraft-crypt",
|
||||
@@ -3660,7 +3660,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-create"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"hayro-syntax",
|
||||
@@ -3675,7 +3675,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-crypt"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"md-5",
|
||||
@@ -3687,7 +3687,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-edit"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"hayro-syntax",
|
||||
"pdfcraft-annot",
|
||||
@@ -3701,7 +3701,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-engine"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"icu_properties",
|
||||
@@ -3738,14 +3738,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-export"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-filters"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"proptest",
|
||||
@@ -3755,7 +3755,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-fonts"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"harfrust",
|
||||
"pdfcraft-cos",
|
||||
@@ -3764,7 +3764,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-forms"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"hayro-syntax",
|
||||
"pdfcraft-content",
|
||||
@@ -3776,18 +3776,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-geom"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-js"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"boa_engine",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-measure"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"pdfcraft-annot",
|
||||
"pdfcraft-content",
|
||||
@@ -3799,14 +3799,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-model"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"pdfcraft-cos",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-ocr"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"ocrs",
|
||||
"pdfcraft-fonts",
|
||||
@@ -3816,7 +3816,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-optimize"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"pdfcraft-annot",
|
||||
@@ -3829,7 +3829,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-organize"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"hayro-syntax",
|
||||
"pdfcraft-content",
|
||||
@@ -3839,7 +3839,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-platform"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"futures-lite",
|
||||
@@ -3848,7 +3848,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-preflight"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"pdfcraft-cos",
|
||||
"pdfcraft-model",
|
||||
@@ -3856,7 +3856,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-print"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"pdfcraft-content",
|
||||
"pdfcraft-cos",
|
||||
@@ -3866,7 +3866,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-redact"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"pdfcraft-annot",
|
||||
"pdfcraft-content",
|
||||
@@ -3880,7 +3880,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-render"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"flate2",
|
||||
@@ -3898,7 +3898,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-sign"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aws-lc-rs",
|
||||
@@ -3932,7 +3932,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-ui-egui"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"base64 0.22.1",
|
||||
@@ -3972,7 +3972,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-web"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -3984,7 +3984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-xfa"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"image",
|
||||
@@ -3998,7 +3998,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-xfdf"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"pdfcraft-annot",
|
||||
"pdfcraft-cos",
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ exclude = ["vendor"]
|
||||
default-members = ["crates/*", "apps/pdfcraft", "apps/pdfcraft-cli", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.90"
|
||||
@@ -13,7 +13,7 @@ repository = "https://github.com/storytold/pdfcraft"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Shared frontend components; engines remain independent of egui.
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
|
||||
craft-ui = { git = "https://git.marcospaulo.dev.br/masi/craft-libs", version = "0.1.0", rev = "22544b6d619d4f16779b8bdfa3f0b642a86fc065", features = ["serde"] }
|
||||
# internal
|
||||
pdfcraft-geom = { path = "crates/geom", version = "0.6.0" }
|
||||
pdfcraft-filters = { path = "crates/filters", version = "0.6.0" }
|
||||
|
||||
+50
-10
@@ -64,6 +64,25 @@ fn settings_dir() -> Option<std::path::PathBuf> {
|
||||
eframe::storage_dir("PdfCraft")
|
||||
}
|
||||
|
||||
/// Windows and Linux: no OS title bar; the top row is the title bar
|
||||
/// (`pdfcraft_ui_egui::titlebar`). macOS keeps its traffic lights over the integrated top row.
|
||||
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
||||
|
||||
/// Whether this start draws its own title bar: Windows and Linux do, unless Preferences ▸ Appearance ▸
|
||||
/// Use System Title Bar asks for the system's. Read from the saved settings before the window
|
||||
/// opens; a missing or unreadable file keeps the default.
|
||||
fn custom_titlebar(settings_dir: Option<&std::path::Path>) -> bool {
|
||||
let system_title_bar = settings_dir
|
||||
.and_then(|d| std::fs::read_to_string(d.join("app.ron")).ok())
|
||||
.is_some_and(|s| {
|
||||
s.contains(r#"system_title_bar\":true"#)
|
||||
|| s.contains(r#"system_title_bar\": true"#)
|
||||
|| s.contains(r#""system_title_bar":true"#)
|
||||
|| s.contains(r#""system_title_bar": true"#)
|
||||
});
|
||||
CUSTOM_TITLEBAR && !system_title_bar
|
||||
}
|
||||
|
||||
/// Move the settings and crash-recovery folders of the app's former name, PrintCraft, to the new
|
||||
/// name once, so an upgrade keeps recent files, preferences and unsaved work. Best effort: a
|
||||
/// folder is left alone when the new one already exists or the move fails. A portable copy leaves
|
||||
@@ -188,7 +207,8 @@ fn main() -> eframe::Result {
|
||||
single_instance::Claim::Alone => None,
|
||||
};
|
||||
let choice = renderer_choice(std::env::var("PDFCRAFT_RENDERER").ok().as_deref());
|
||||
let launch = Launch { files, options, control_file, create_images, integrated };
|
||||
let custom_titlebar = custom_titlebar(settings_dir().as_deref());
|
||||
let launch = Launch { files, options, control_file, create_images, integrated, custom_titlebar };
|
||||
// Finder, Open With and the Dock deliver files as Apple events, not arguments; catch the one
|
||||
// that launched us as well as later ones. Lives until the event loop returns.
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -198,7 +218,7 @@ fn main() -> eframe::Result {
|
||||
let first = if choice == RendererChoice::Gl { eframe::Renderer::Glow } else { eframe::Renderer::Wgpu };
|
||||
let result = eframe::run_native(
|
||||
"PdfCraft",
|
||||
native_options(integrated, first),
|
||||
native_options(custom_titlebar, integrated, first),
|
||||
app_creator(
|
||||
launch.clone(),
|
||||
Rc::clone(&started),
|
||||
@@ -216,7 +236,7 @@ fn main() -> eframe::Result {
|
||||
log::error!("the GPU renderer (wgpu) didn't start: {e}. Starting with OpenGL instead; set PDFCRAFT_RENDERER=gl to skip wgpu.");
|
||||
eframe::run_native(
|
||||
"PdfCraft",
|
||||
native_options(integrated, eframe::Renderer::Glow),
|
||||
native_options(custom_titlebar, integrated, eframe::Renderer::Glow),
|
||||
app_creator(
|
||||
launch,
|
||||
started,
|
||||
@@ -256,7 +276,7 @@ fn retry_with_gl(choice: RendererChoice, app_started: bool) -> bool {
|
||||
}
|
||||
|
||||
/// The window and renderer settings for one run.
|
||||
fn native_options(integrated: bool, renderer: eframe::Renderer) -> eframe::NativeOptions {
|
||||
fn native_options(custom_titlebar: bool, integrated: bool, renderer: eframe::Renderer) -> eframe::NativeOptions {
|
||||
let mut viewport = egui::ViewportBuilder::default()
|
||||
.with_title("PdfCraft")
|
||||
.with_inner_size([1440.0, 920.0])
|
||||
@@ -269,7 +289,9 @@ fn native_options(integrated: bool, renderer: eframe::Renderer) -> eframe::Nativ
|
||||
Ok(icon) => viewport = viewport.with_icon(icon),
|
||||
Err(e) => log::warn!("app icon: {e}"),
|
||||
}
|
||||
if integrated {
|
||||
if custom_titlebar {
|
||||
viewport = viewport.with_decorations(false).with_fullsize_content_view(true).with_titlebar_shown(false).with_title_shown(false);
|
||||
} else if integrated {
|
||||
viewport = viewport.with_fullsize_content_view(true).with_titlebar_shown(false).with_title_shown(false);
|
||||
}
|
||||
let persistence_path = settings_dir().map(|d| d.join("app.ron"));
|
||||
@@ -289,6 +311,7 @@ struct Launch {
|
||||
control_file: Option<String>,
|
||||
create_images: bool,
|
||||
integrated: bool,
|
||||
custom_titlebar: bool,
|
||||
}
|
||||
|
||||
fn app_creator<'a>(
|
||||
@@ -297,7 +320,7 @@ fn app_creator<'a>(
|
||||
instance: Option<&'a single_instance::Server>,
|
||||
#[cfg(target_os = "macos")] apple_events: &'a apple_events::AppleEvents,
|
||||
) -> eframe::AppCreator<'a> {
|
||||
let Launch { files, options, control_file, create_images, integrated } = launch;
|
||||
let Launch { files, options, control_file, create_images, integrated, custom_titlebar } = launch;
|
||||
Box::new(move |cc| {
|
||||
started.set(true);
|
||||
let mut app = PdfCraftApp::new();
|
||||
@@ -311,6 +334,7 @@ fn app_creator<'a>(
|
||||
app.restore(&json);
|
||||
}
|
||||
app.integrated_titlebar = integrated;
|
||||
app.custom_titlebar = custom_titlebar;
|
||||
app.system_text_scale = text_scale::system();
|
||||
app.update_source = Some(std::sync::Arc::new(updates::latest_release));
|
||||
app.printer_source = Some(pdfcraft_ui_egui::system_printers());
|
||||
@@ -731,8 +755,8 @@ mod tests {
|
||||
assert!(!retry_with_gl(RendererChoice::Wgpu, false));
|
||||
assert!(!retry_with_gl(RendererChoice::Gl, false));
|
||||
// Each run states its renderer rather than relying on eframe's default.
|
||||
assert_eq!(native_options(false, eframe::Renderer::Wgpu).renderer, eframe::Renderer::Wgpu);
|
||||
assert_eq!(native_options(false, eframe::Renderer::Glow).renderer, eframe::Renderer::Glow);
|
||||
assert_eq!(native_options(false, false, eframe::Renderer::Wgpu).renderer, eframe::Renderer::Wgpu);
|
||||
assert_eq!(native_options(false, false, eframe::Renderer::Glow).renderer, eframe::Renderer::Glow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -878,7 +902,7 @@ mod tests {
|
||||
// and an error wgpu raises before anything reaches the driver: a texture one pixel wider
|
||||
// than the device allows.
|
||||
use eframe::wgpu;
|
||||
let native = super::native_options(false, eframe::Renderer::Wgpu);
|
||||
let native = super::native_options(false, false, eframe::Renderer::Wgpu);
|
||||
let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &native.wgpu_options.wgpu_setup else {
|
||||
panic!("default setup creates its own instance")
|
||||
};
|
||||
@@ -944,7 +968,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pipeline_creation_error_is_returned_and_device_remains_usable() {
|
||||
use eframe::{egui_wgpu, wgpu};
|
||||
let mut native = super::native_options(false, eframe::Renderer::Wgpu);
|
||||
let mut native = super::native_options(false, false, eframe::Renderer::Wgpu);
|
||||
let egui_wgpu::WgpuSetup::CreateNew(setup) = &native.wgpu_options.wgpu_setup else { panic!("default setup creates its own instance") };
|
||||
let instance = pollster::block_on(native.wgpu_options.wgpu_setup.new_instance());
|
||||
let options = wgpu::RequestAdapterOptions { power_preference: setup.power_preference, ..Default::default() };
|
||||
@@ -1219,4 +1243,20 @@ mod tests {
|
||||
let class = entry.lines().find_map(|line| line.strip_prefix("StartupWMClass="));
|
||||
assert_eq!(class, Some(super::APP_ID));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_titlebar_detects_preferences() {
|
||||
let temp = std::env::temp_dir().join(format!("pdfcraft_test_titlebar_{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&temp);
|
||||
assert_eq!(super::custom_titlebar(Some(&temp)), super::CUSTOM_TITLEBAR);
|
||||
|
||||
std::fs::write(temp.join("app.ron"), r#"{"pdfcraft": "{\"system_title_bar\":true}"}"#).unwrap();
|
||||
assert!(!super::custom_titlebar(Some(&temp)));
|
||||
|
||||
std::fs::write(temp.join("app.ron"), r#"{"pdfcraft": "{\"system_title_bar\":false}"}"#).unwrap();
|
||||
assert_eq!(super::custom_titlebar(Some(&temp)), super::CUSTOM_TITLEBAR);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&temp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,26 @@ use crate::PdfCraftApp;
|
||||
use crate::theme::{self, ThemeKind, ThemePreference, Tokens};
|
||||
|
||||
/// The themes each card offers. One of each today; a new theme is one more row here.
|
||||
pub const LIGHT_THEMES: &[(ThemeKind, &str)] = &[(ThemeKind::Light, "Light gray")];
|
||||
pub const DARK_THEMES: &[(ThemeKind, &str)] = &[(ThemeKind::Dark, "Dark gray")];
|
||||
pub const LIGHT_THEMES: &[(ThemeKind, &str)] = &[
|
||||
(ThemeKind::Light, "Light gray"),
|
||||
(ThemeKind::CatppuccinLatte, "Catppuccin Latte"),
|
||||
];
|
||||
pub const DARK_THEMES: &[(ThemeKind, &str)] = &[
|
||||
(ThemeKind::Dark, "Dark gray"),
|
||||
(ThemeKind::CatppuccinFrappe, "Catppuccin Frappé"),
|
||||
(ThemeKind::CatppuccinMacchiato, "Catppuccin Macchiato"),
|
||||
(ThemeKind::CatppuccinMocha, "Catppuccin Mocha"),
|
||||
];
|
||||
|
||||
fn mode_label(preference: ThemePreference) -> &'static str {
|
||||
match preference {
|
||||
ThemePreference::System => "Use system setting",
|
||||
ThemePreference::Light => "Light",
|
||||
ThemePreference::Dark => "Dark",
|
||||
ThemePreference::CatppuccinLatte => "Catppuccin Latte",
|
||||
ThemePreference::CatppuccinFrappe => "Catppuccin Frappé",
|
||||
ThemePreference::CatppuccinMacchiato => "Catppuccin Macchiato",
|
||||
ThemePreference::CatppuccinMocha => "Catppuccin Mocha",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +35,10 @@ fn mode_command(preference: ThemePreference) -> &'static str {
|
||||
ThemePreference::System => "view.theme.system",
|
||||
ThemePreference::Light => "view.theme.light",
|
||||
ThemePreference::Dark => "view.theme.dark",
|
||||
ThemePreference::CatppuccinLatte => "view.theme.catppuccinLatte",
|
||||
ThemePreference::CatppuccinFrappe => "view.theme.catppuccinFrappe",
|
||||
ThemePreference::CatppuccinMacchiato => "view.theme.catppuccinMacchiato",
|
||||
ThemePreference::CatppuccinMocha => "view.theme.catppuccinMocha",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +99,7 @@ fn preview(ui: &mut egui::Ui, kind: ThemeKind, width: f32) {
|
||||
|
||||
/// One theme card: its title, Active when it is what shows now, a preview of the chosen theme
|
||||
/// and a radio row per theme.
|
||||
fn card(ui: &mut egui::Ui, t: &Tokens, title: &str, active: bool, themes: &[(ThemeKind, &str)]) {
|
||||
fn card(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, title: &str, active: bool, themes: &[(ThemeKind, &str)]) {
|
||||
egui::Frame::new()
|
||||
.fill(t.card)
|
||||
.stroke(egui::Stroke::new(1.0, if active { t.accent } else { t.border }))
|
||||
@@ -98,12 +114,22 @@ fn card(ui: &mut egui::Ui, t: &Tokens, title: &str, active: bool, themes: &[(The
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let shown = themes.first().map_or(ThemeKind::Light, |(kind, _)| *kind);
|
||||
let shown = themes.iter().find(|(k, _)| *k == app.theme).map_or(themes[0].0, |(k, _)| *k);
|
||||
preview(ui, shown, ui.available_width());
|
||||
ui.add_space(4.0);
|
||||
for (kind, label) in themes {
|
||||
// One theme per card today, so it is always the chosen one.
|
||||
let _ = ui.radio(*kind == shown, tl!(label));
|
||||
let selected = app.theme == *kind;
|
||||
if ui.radio(selected, tl!(label)).clicked() {
|
||||
let cmd = match kind {
|
||||
ThemeKind::Light => "view.theme.light",
|
||||
ThemeKind::Dark => "view.theme.dark",
|
||||
ThemeKind::CatppuccinLatte => "view.theme.catppuccinLatte",
|
||||
ThemeKind::CatppuccinFrappe => "view.theme.catppuccinFrappe",
|
||||
ThemeKind::CatppuccinMacchiato => "view.theme.catppuccinMacchiato",
|
||||
ThemeKind::CatppuccinMocha => "view.theme.catppuccinMocha",
|
||||
};
|
||||
app.execute(cmd);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -114,7 +140,15 @@ pub fn preferences_section(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Appearance mode"));
|
||||
egui::ComboBox::from_id_salt("appearance-mode").selected_text(tl!(mode_label(app.theme_preference))).show_ui(ui, |ui| {
|
||||
for preference in [ThemePreference::System, ThemePreference::Light, ThemePreference::Dark] {
|
||||
for preference in [
|
||||
ThemePreference::System,
|
||||
ThemePreference::Light,
|
||||
ThemePreference::Dark,
|
||||
ThemePreference::CatppuccinLatte,
|
||||
ThemePreference::CatppuccinFrappe,
|
||||
ThemePreference::CatppuccinMacchiato,
|
||||
ThemePreference::CatppuccinMocha,
|
||||
] {
|
||||
if ui.selectable_label(app.theme_preference == preference, tl!(mode_label(preference))).clicked() {
|
||||
app.execute(mode_command(preference));
|
||||
ui.close();
|
||||
@@ -123,12 +157,21 @@ pub fn preferences_section(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let light_active = app.theme == ThemeKind::Light;
|
||||
let light_active = app.theme.is_light();
|
||||
ui.columns(2, |cols| {
|
||||
let [light, dark] = cols else { return };
|
||||
card(light, t, tl!("Light theme"), light_active, LIGHT_THEMES);
|
||||
card(dark, t, tl!("Dark theme"), !light_active, DARK_THEMES);
|
||||
card(light, app, t, tl!("Light theme"), light_active, LIGHT_THEMES);
|
||||
card(dark, app, t, tl!("Dark theme"), !light_active, DARK_THEMES);
|
||||
});
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
ui.add_space(6.0);
|
||||
let mut sys = app.system_title_bar;
|
||||
let label = "Use System Title Bar on Windows and Linux";
|
||||
if ui.checkbox(&mut sys, crate::i18n::t(label)).changed() {
|
||||
app.system_title_bar = sys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -31,20 +31,26 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
// macOS: the strip is the 28 pt title bar, so the traffic lights are centred in it; tabs and
|
||||
// buttons shrink to keep a margin above and below.
|
||||
let mac = cfg!(target_os = "macos") && app.integrated_titlebar;
|
||||
let custom = app.custom_titlebar;
|
||||
let left = if mac { MAC_CONTENT_LEFT as i8 } else { 8 };
|
||||
let right = if custom { (crate::titlebar::WIDTH + 8.0) as i8 } else { 10 };
|
||||
let (tab_h, button) = if mac { (MAC_TITLE_BAR - 2.0, 24.0) } else { (TAB_HEIGHT, STRIP_BUTTON) };
|
||||
egui::Panel::top("tab_strip")
|
||||
.exact_size(if mac { MAC_TITLE_BAR } else { TAB_STRIP })
|
||||
.frame(egui::Frame::NONE.fill(t.titlebar).inner_margin(egui::Margin { left, right: 10, top: 0, bottom: 0 }))
|
||||
.frame(egui::Frame::NONE.fill(t.titlebar).inner_margin(egui::Margin { left, right, top: 0, bottom: 0 }))
|
||||
.show(ui, |ui| {
|
||||
let full = ui.max_rect();
|
||||
let drag = ui.interact(full, ui.id().with("titledrag"), Sense::click_and_drag());
|
||||
if drag.drag_started() {
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
|
||||
}
|
||||
if drag.double_clicked() {
|
||||
let max = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false));
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Maximized(!max));
|
||||
if custom || mac {
|
||||
crate::titlebar::drag_area(ui, full);
|
||||
} else {
|
||||
let drag = ui.interact(full, ui.id().with("titledrag"), Sense::click_and_drag());
|
||||
if drag.drag_started() {
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
|
||||
}
|
||||
if drag.double_clicked() {
|
||||
let max = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false));
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Maximized(!max));
|
||||
}
|
||||
}
|
||||
ui.horizontal_centered(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
@@ -175,15 +181,23 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
ThemePreference::System => ("monitor", tl!("Use system setting")),
|
||||
ThemePreference::Light => ("sun", tl!("Light gray")),
|
||||
ThemePreference::Dark => ("moon", tl!("Dark gray")),
|
||||
ThemePreference::CatppuccinLatte => ("sun", "Catppuccin Latte"),
|
||||
ThemePreference::CatppuccinFrappe => ("moon", "Catppuccin Frappé"),
|
||||
ThemePreference::CatppuccinMacchiato => ("moon", "Catppuccin Macchiato"),
|
||||
ThemePreference::CatppuccinMocha => ("moon", "Catppuccin Mocha"),
|
||||
};
|
||||
let tip = format!("{}: {label}", tl!("Display theme"));
|
||||
let response = icons::button(ui, icon, button, false, &tip);
|
||||
// A click cycles System, Light, Dark; a right-click opens the menu.
|
||||
// A click cycles System, Light, Dark, Catppuccin; a right-click opens the menu.
|
||||
if response.clicked() {
|
||||
app.execute(match app.theme_preference {
|
||||
ThemePreference::System => "view.theme.light",
|
||||
ThemePreference::Light => "view.theme.dark",
|
||||
ThemePreference::Dark => "view.theme.system",
|
||||
ThemePreference::Dark => "view.theme.catppuccinLatte",
|
||||
ThemePreference::CatppuccinLatte => "view.theme.catppuccinFrappe",
|
||||
ThemePreference::CatppuccinFrappe => "view.theme.catppuccinMacchiato",
|
||||
ThemePreference::CatppuccinMacchiato => "view.theme.catppuccinMocha",
|
||||
ThemePreference::CatppuccinMocha => "view.theme.system",
|
||||
});
|
||||
}
|
||||
egui::Popup::context_menu(&response).id(response.id.with("theme_menu")).show(|ui| appearance_menu(app, ui));
|
||||
@@ -192,6 +206,9 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
});
|
||||
});
|
||||
if custom {
|
||||
crate::titlebar::caption_buttons(app, ui, full);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,6 +218,10 @@ fn theme_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
(ThemePreference::System, "view.theme.system", "Use system setting"),
|
||||
(ThemePreference::Light, "view.theme.light", "Light gray"),
|
||||
(ThemePreference::Dark, "view.theme.dark", "Dark gray"),
|
||||
(ThemePreference::CatppuccinLatte, "view.theme.catppuccinLatte", "Catppuccin Latte"),
|
||||
(ThemePreference::CatppuccinFrappe, "view.theme.catppuccinFrappe", "Catppuccin Frappé"),
|
||||
(ThemePreference::CatppuccinMacchiato, "view.theme.catppuccinMacchiato", "Catppuccin Macchiato"),
|
||||
(ThemePreference::CatppuccinMocha, "view.theme.catppuccinMocha", "Catppuccin Mocha"),
|
||||
] {
|
||||
if ui.radio(app.theme_preference == preference, tl!(label)).clicked() {
|
||||
app.execute(command);
|
||||
|
||||
@@ -9,7 +9,7 @@ use pdfcraft_engine::commands::{self, COMMANDS, CommandSpec, Shortcut};
|
||||
|
||||
use crate::{
|
||||
Dialog, Mode, PdfCraftApp, PropsTab, RightPanel, SaveTarget,
|
||||
theme::{ThemeKind, ThemePreference},
|
||||
theme::ThemePreference,
|
||||
widgets,
|
||||
};
|
||||
|
||||
@@ -261,13 +261,17 @@ impl PdfCraftApp {
|
||||
}
|
||||
"view.read_mode" => self.switch_mode(if self.mode == Mode::Read { Mode::AllTools } else { Mode::Read }, false),
|
||||
"view.theme" => {
|
||||
let next = if self.theme == ThemeKind::Light { ThemePreference::Dark } else { ThemePreference::Light };
|
||||
let next = if self.theme.is_light() { ThemePreference::Dark } else { ThemePreference::Light };
|
||||
self.set_theme_preference(next);
|
||||
}
|
||||
command if command.starts_with("measure.") => crate::measure_ui::command(self, command),
|
||||
"view.theme.system" => self.set_theme_preference(ThemePreference::System),
|
||||
"view.theme.light" => self.set_theme_preference(ThemePreference::Light),
|
||||
"view.theme.dark" => self.set_theme_preference(ThemePreference::Dark),
|
||||
"view.theme.catppuccinlatte" | "view.theme.catppuccinLatte" => self.set_theme_preference(ThemePreference::CatppuccinLatte),
|
||||
"view.theme.catppuccinfrappe" | "view.theme.catppuccinFrappe" => self.set_theme_preference(ThemePreference::CatppuccinFrappe),
|
||||
"view.theme.catppuccinmacchiato" | "view.theme.catppuccinMacchiato" => self.set_theme_preference(ThemePreference::CatppuccinMacchiato),
|
||||
"view.theme.catppuccinmocha" | "view.theme.catppuccinMocha" => self.set_theme_preference(ThemePreference::CatppuccinMocha),
|
||||
"comment.list" => self.choose_right_panel(Some(RightPanel::Comments)),
|
||||
tool if crate::comments::CommentTool::from_command(tool).is_some() => {
|
||||
let Some(tool) = crate::comments::CommentTool::from_command(tool) else { return false };
|
||||
|
||||
@@ -119,6 +119,7 @@ pub mod scroll_speed;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod system_fonts;
|
||||
pub mod theme;
|
||||
pub mod titlebar;
|
||||
pub mod ui_scale;
|
||||
pub mod updates;
|
||||
mod wheel_pager;
|
||||
@@ -507,6 +508,8 @@ pub struct PdfCraftApp {
|
||||
pub toast: Option<(String, f64)>,
|
||||
/// Whether the macOS title bar is drawn by us (traffic lights over our tab strip).
|
||||
pub integrated_titlebar: bool,
|
||||
pub custom_titlebar: bool,
|
||||
pub system_title_bar: bool,
|
||||
pub password_prompt: Option<PasswordPrompt>,
|
||||
pub unlock_prompt: Option<UnlockPrompt>,
|
||||
pub full_screen: bool,
|
||||
@@ -809,6 +812,8 @@ impl PdfCraftApp {
|
||||
pinned: Default::default(),
|
||||
toast: None,
|
||||
integrated_titlebar: false,
|
||||
custom_titlebar: false,
|
||||
system_title_bar: false,
|
||||
password_prompt: None,
|
||||
unlock_prompt: None,
|
||||
full_screen: false,
|
||||
@@ -1620,6 +1625,7 @@ impl PdfCraftApp {
|
||||
"reload_changed_files": self.reload_changed_files,
|
||||
"pinned_folders": self.pinned.folders,
|
||||
"theme": self.theme_preference,
|
||||
"system_title_bar": self.system_title_bar,
|
||||
"scroll_speed": self.scroll_speed,
|
||||
"default_mode": self.default_mode,
|
||||
"default_layout": canvas::display_token(self.view_defaults.layout, self.view_defaults.continuous),
|
||||
@@ -1703,6 +1709,9 @@ impl PdfCraftApp {
|
||||
if let Ok(preference) = serde_json::from_value::<ThemePreference>(v["theme"].clone()) {
|
||||
self.set_theme_preference(preference);
|
||||
}
|
||||
if let Some(on) = v["system_title_bar"].as_bool() {
|
||||
self.system_title_bar = on;
|
||||
}
|
||||
if let Ok(speed) = serde_json::from_value::<scroll_speed::ScrollSpeed>(v["scroll_speed"].clone()) {
|
||||
self.scroll_speed = speed;
|
||||
}
|
||||
@@ -1825,7 +1834,11 @@ impl PdfCraftApp {
|
||||
"system" => ThemePreference::System,
|
||||
"light" => ThemePreference::Light,
|
||||
"dark" => ThemePreference::Dark,
|
||||
_ => return Err("theme must be light, dark, or system".into()),
|
||||
"catppuccinlatte" | "catppuccin_latte" | "catppuccinLatte" => ThemePreference::CatppuccinLatte,
|
||||
"catppuccinfrappe" | "catppuccin_frappe" | "catppuccinFrappe" => ThemePreference::CatppuccinFrappe,
|
||||
"catppuccinmacchiato" | "catppuccin_macchiato" | "catppuccinMacchiato" => ThemePreference::CatppuccinMacchiato,
|
||||
"catppuccinmocha" | "catppuccin_mocha" | "catppuccinMocha" => ThemePreference::CatppuccinMocha,
|
||||
_ => return Err("theme must be light, dark, catppuccinLatte, catppuccinFrappe, catppuccinMacchiato, catppuccinMocha, or system".into()),
|
||||
};
|
||||
self.set_theme_preference(preference);
|
||||
}
|
||||
@@ -2441,6 +2454,9 @@ impl eframe::App for PdfCraftApp {
|
||||
dialogs::show(self, &ctx);
|
||||
self.show_progress(&ctx);
|
||||
widgets::toast(self, &ctx);
|
||||
if self.custom_titlebar {
|
||||
titlebar::resize_zones(ui);
|
||||
}
|
||||
self.finish_render_frame(&ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use craft_ui::theme::catppuccin::Flavor;
|
||||
use egui::{Color32, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, Stroke, Visuals};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
@@ -10,6 +11,46 @@ pub enum ThemeKind {
|
||||
#[default]
|
||||
Light,
|
||||
Dark,
|
||||
CatppuccinLatte,
|
||||
CatppuccinFrappe,
|
||||
CatppuccinMacchiato,
|
||||
CatppuccinMocha,
|
||||
}
|
||||
|
||||
impl ThemeKind {
|
||||
pub const ALL: [ThemeKind; 6] = [
|
||||
ThemeKind::Light,
|
||||
ThemeKind::Dark,
|
||||
ThemeKind::CatppuccinLatte,
|
||||
ThemeKind::CatppuccinFrappe,
|
||||
ThemeKind::CatppuccinMacchiato,
|
||||
ThemeKind::CatppuccinMocha,
|
||||
];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
ThemeKind::Light => "light",
|
||||
ThemeKind::Dark => "dark",
|
||||
ThemeKind::CatppuccinLatte => "catppuccinLatte",
|
||||
ThemeKind::CatppuccinFrappe => "catppuccinFrappe",
|
||||
ThemeKind::CatppuccinMacchiato => "catppuccinMacchiato",
|
||||
ThemeKind::CatppuccinMocha => "catppuccinMocha",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flavor(self) -> Option<Flavor> {
|
||||
match self {
|
||||
ThemeKind::CatppuccinLatte => Some(Flavor::Latte),
|
||||
ThemeKind::CatppuccinFrappe => Some(Flavor::Frappe),
|
||||
ThemeKind::CatppuccinMacchiato => Some(Flavor::Macchiato),
|
||||
ThemeKind::CatppuccinMocha => Some(Flavor::Mocha),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_light(self) -> bool {
|
||||
matches!(self, ThemeKind::Light | ThemeKind::CatppuccinLatte)
|
||||
}
|
||||
}
|
||||
|
||||
/// The saved user choice, independent of the light/dark colours currently displayed.
|
||||
@@ -19,6 +60,10 @@ pub enum ThemePreference {
|
||||
#[default]
|
||||
Light,
|
||||
Dark,
|
||||
CatppuccinLatte,
|
||||
CatppuccinFrappe,
|
||||
CatppuccinMacchiato,
|
||||
CatppuccinMocha,
|
||||
}
|
||||
|
||||
impl ThemePreference {
|
||||
@@ -28,6 +73,10 @@ impl ThemePreference {
|
||||
match self {
|
||||
Self::Light => ThemeKind::Light,
|
||||
Self::Dark => ThemeKind::Dark,
|
||||
Self::CatppuccinLatte => ThemeKind::CatppuccinLatte,
|
||||
Self::CatppuccinFrappe => ThemeKind::CatppuccinFrappe,
|
||||
Self::CatppuccinMacchiato => ThemeKind::CatppuccinMacchiato,
|
||||
Self::CatppuccinMocha => ThemeKind::CatppuccinMocha,
|
||||
Self::System => match system {
|
||||
Some(true) => ThemeKind::Dark,
|
||||
Some(false) => ThemeKind::Light,
|
||||
@@ -66,8 +115,40 @@ pub struct Tokens {
|
||||
pub radius: u8,
|
||||
}
|
||||
|
||||
fn catppuccin(kind: ThemeKind, f: Flavor) -> Tokens {
|
||||
let p = f.palette();
|
||||
let dark = f.is_dark();
|
||||
Tokens {
|
||||
kind,
|
||||
titlebar: p.mantle,
|
||||
chrome: p.base,
|
||||
panel: p.base,
|
||||
pasteboard: p.crust,
|
||||
card: p.surface0,
|
||||
border: p.surface1,
|
||||
divider: p.surface0,
|
||||
text: p.text,
|
||||
text_muted: p.subtext0,
|
||||
text_faint: p.overlay1,
|
||||
icon: p.subtext1,
|
||||
hover: p.surface0,
|
||||
pressed: p.surface1,
|
||||
selected: Color32::from_rgba_unmultiplied(p.mauve.r(), p.mauve.g(), p.mauve.b(), if dark { 55 } else { 40 }),
|
||||
accent: p.mauve,
|
||||
accent_text: if dark { p.lavender } else { p.mauve },
|
||||
accent_soft: Color32::from_rgba_unmultiplied(p.mauve.r(), p.mauve.g(), p.mauve.b(), if dark { 40 } else { 30 }),
|
||||
field: p.mantle,
|
||||
badge_new: p.mauve,
|
||||
page_shadow: if dark { Color32::from_black_alpha(120) } else { Color32::from_black_alpha(34) },
|
||||
radius: 6,
|
||||
}
|
||||
}
|
||||
|
||||
impl Tokens {
|
||||
pub fn for_kind(kind: ThemeKind) -> Self {
|
||||
if let Some(f) = kind.flavor() {
|
||||
return catppuccin(kind, f);
|
||||
}
|
||||
match kind {
|
||||
ThemeKind::Light => Self {
|
||||
kind,
|
||||
@@ -93,7 +174,7 @@ impl Tokens {
|
||||
page_shadow: Color32::from_black_alpha(34),
|
||||
radius: 6,
|
||||
},
|
||||
ThemeKind::Dark => Self {
|
||||
ThemeKind::Dark | ThemeKind::CatppuccinLatte | ThemeKind::CatppuccinFrappe | ThemeKind::CatppuccinMacchiato | ThemeKind::CatppuccinMocha => Self {
|
||||
kind,
|
||||
titlebar: Color32::from_rgb(0x1B, 0x1B, 0x1E),
|
||||
chrome: Color32::from_rgb(0x26, 0x26, 0x2A),
|
||||
@@ -125,7 +206,7 @@ impl Tokens {
|
||||
}
|
||||
|
||||
pub fn dark(&self) -> bool {
|
||||
self.kind == ThemeKind::Dark
|
||||
!self.kind.is_light()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,4 +460,20 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catppuccin_flavors_resolve_and_set_correct_kind() {
|
||||
for kind in [
|
||||
ThemeKind::CatppuccinLatte,
|
||||
ThemeKind::CatppuccinFrappe,
|
||||
ThemeKind::CatppuccinMacchiato,
|
||||
ThemeKind::CatppuccinMocha,
|
||||
] {
|
||||
assert!(kind.flavor().is_some());
|
||||
let t = Tokens::for_kind(kind);
|
||||
assert_eq!(t.kind, kind);
|
||||
assert_eq!(t.dark(), !kind.is_light());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
//! The window's own title bar on Windows and Linux, where the window has no OS decorations
|
||||
//! ([`PdfCraftApp::custom_titlebar`]): the tab strip row is the title bar. Empty space in it
|
||||
//! drags the window and a double-click there maximizes it, caption buttons at the far right
|
||||
//! minimize, maximize/restore and close, and thin invisible zones along the window's edges resize
|
||||
//! it. On macOS the traffic lights sit over the same row and only the drag works.
|
||||
|
||||
use egui::{CursorIcon, Id, LayerId, Order, PointerButton, Rect, ResizeDirection, Sense, Stroke, Ui, ViewportCommand, pos2, vec2};
|
||||
|
||||
use crate::PdfCraftApp;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// One caption button: as wide as the native Windows ones.
|
||||
pub const BUTTON_WIDTH: f32 = 46.0;
|
||||
/// The three caption buttons together.
|
||||
pub const WIDTH: f32 = 3.0 * BUTTON_WIDTH;
|
||||
/// Height of the title row (tab strip) when it carries the caption buttons.
|
||||
pub const HEIGHT: f32 = 38.0;
|
||||
/// Resize zone thickness along an edge, and the side of a corner zone.
|
||||
const EDGE: f32 = 6.0;
|
||||
const CORNER: f32 = 12.0;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Caption {
|
||||
Minimize,
|
||||
Maximize,
|
||||
Close,
|
||||
}
|
||||
|
||||
const CAPTIONS: [Caption; 3] = [Caption::Minimize, Caption::Maximize, Caption::Close];
|
||||
|
||||
fn caption_id(c: Caption) -> Id {
|
||||
Id::new(("titlebar-caption", c as u8))
|
||||
}
|
||||
|
||||
fn maximized(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|i| i.viewport().maximized.unwrap_or(false))
|
||||
}
|
||||
|
||||
fn toggle_maximize(ctx: &egui::Context) {
|
||||
ctx.send_viewport_cmd(ViewportCommand::Maximized(!maximized(ctx)));
|
||||
}
|
||||
|
||||
pub fn drag_area(ui: &Ui, rect: Rect) {
|
||||
let ctx = ui.ctx();
|
||||
let resp = ui.interact(rect, Id::new("titlebar-drag"), Sense::click_and_drag());
|
||||
if ctx.input(|i| i.pointer.any_pressed()) {
|
||||
let bare = ctx.viewport(|v| v.hits.click.is_some_and(|w| w.id == resp.id));
|
||||
ctx.data_mut(|d| d.insert_temp(resp.id, bare));
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
toggle_maximize(ctx);
|
||||
} else if resp.drag_started_by(PointerButton::Primary) && ctx.data(|d| d.get_temp(resp.id).unwrap_or(false)) {
|
||||
ctx.send_viewport_cmd(ViewportCommand::StartDrag);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn caption_buttons(app: &mut PdfCraftApp, ui: &mut Ui, bar: Rect) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let max = maximized(ui.ctx());
|
||||
let mut clicked = None;
|
||||
for (i, c) in CAPTIONS.into_iter().enumerate() {
|
||||
let r = Rect::from_min_size(pos2(bar.right() - WIDTH + i as f32 * BUTTON_WIDTH, bar.top()), vec2(BUTTON_WIDTH, bar.height()));
|
||||
let resp = ui.interact(r, caption_id(c), Sense::click());
|
||||
let close = c == Caption::Close;
|
||||
let fill = match (resp.is_pointer_button_down_on(), resp.hovered()) {
|
||||
(true, _) if close => egui::Color32::from_rgb(0xc4, 0x2b, 0x1c),
|
||||
(_, true) if close => egui::Color32::from_rgb(0xe8, 0x11, 0x23),
|
||||
(true, _) => t.pressed,
|
||||
(_, true) => t.hover,
|
||||
_ => egui::Color32::TRANSPARENT,
|
||||
};
|
||||
ui.painter().rect_filled(r, 0.0, fill);
|
||||
let ink = if close && resp.hovered() { egui::Color32::WHITE } else { t.icon };
|
||||
paint_glyph(ui, c, max, r.center(), Stroke::new(1.0, ink));
|
||||
let tip = caption_tip(c, max);
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tip));
|
||||
if resp.on_hover_text(tip).clicked() {
|
||||
clicked = Some(c);
|
||||
}
|
||||
}
|
||||
match clicked {
|
||||
Some(Caption::Minimize) => ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true)),
|
||||
Some(Caption::Maximize) => toggle_maximize(ui.ctx()),
|
||||
Some(Caption::Close) => {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.request_quit(&ctx);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn caption_tip(c: Caption, max: bool) -> &'static str {
|
||||
match c {
|
||||
Caption::Minimize => crate::i18n::t("Minimize\0".trim_end_matches('\0')),
|
||||
Caption::Maximize if max => crate::i18n::t("Restore\0".trim_end_matches('\0')),
|
||||
Caption::Maximize => crate::i18n::t("Maximize\0".trim_end_matches('\0')),
|
||||
Caption::Close => tl!("Close"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn paint_glyph(ui: &Ui, c: Caption, maximized: bool, at: egui::Pos2, s: Stroke) {
|
||||
let p = ui.painter();
|
||||
let g = Rect::from_center_size(at, vec2(10.0, 10.0));
|
||||
match c {
|
||||
Caption::Minimize => {
|
||||
p.line_segment([g.left_center(), g.right_center()], s);
|
||||
}
|
||||
Caption::Maximize if maximized => {
|
||||
let front = Rect::from_min_max(g.min + vec2(0.0, 2.0), g.max - vec2(2.0, 0.0));
|
||||
p.rect_stroke(front, 0.0, s, egui::StrokeKind::Middle);
|
||||
p.line(vec![g.min + vec2(2.0, 2.0), g.min + vec2(2.0, 0.0), g.right_top(), g.max - vec2(0.0, 2.0), g.max - vec2(2.0, 2.0)], s);
|
||||
}
|
||||
Caption::Maximize => {
|
||||
p.rect_stroke(g, 0.0, s, egui::StrokeKind::Middle);
|
||||
}
|
||||
Caption::Close => {
|
||||
p.line_segment([g.left_top(), g.right_bottom()], s);
|
||||
p.line_segment([g.right_top(), g.left_bottom()], s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn zones(w: Rect) -> [(Rect, ResizeDirection, CursorIcon); 8] {
|
||||
use ResizeDirection::*;
|
||||
let (e, c) = (EDGE, CORNER);
|
||||
[
|
||||
(Rect::from_min_max(w.left_top(), pos2(w.right(), w.top() + e)), North, CursorIcon::ResizeVertical),
|
||||
(Rect::from_min_max(pos2(w.left(), w.bottom() - e), w.right_bottom()), South, CursorIcon::ResizeVertical),
|
||||
(Rect::from_min_max(w.left_top(), pos2(w.left() + e, w.bottom())), West, CursorIcon::ResizeHorizontal),
|
||||
(Rect::from_min_max(pos2(w.right() - e, w.top()), w.right_bottom()), East, CursorIcon::ResizeHorizontal),
|
||||
(Rect::from_min_size(w.left_top(), vec2(c, c)), NorthWest, CursorIcon::ResizeNwSe),
|
||||
(Rect::from_min_size(w.right_top() - vec2(c, 0.0), vec2(c, c)), NorthEast, CursorIcon::ResizeNeSw),
|
||||
(Rect::from_min_size(w.left_bottom() - vec2(0.0, c), vec2(c, c)), SouthWest, CursorIcon::ResizeNeSw),
|
||||
(Rect::from_min_size(w.right_bottom() - vec2(c, c), vec2(c, c)), SouthEast, CursorIcon::ResizeNwSe),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn resize_zones(ui: &mut Ui) {
|
||||
let ctx = ui.ctx().clone();
|
||||
if maximized(&ctx) || ctx.input(|i| i.viewport().fullscreen.unwrap_or(false)) {
|
||||
return;
|
||||
}
|
||||
let w = ctx.content_rect();
|
||||
let layer = LayerId::new(Order::Foreground, Id::new("titlebar-resize"));
|
||||
let zui = ui.new_child(egui::UiBuilder::new().layer_id(layer).max_rect(w));
|
||||
for (i, (r, dir, cursor)) in zones(w).into_iter().enumerate() {
|
||||
let resp = zui.interact(r, Id::new(("titlebar-resize", i)), Sense::drag()).on_hover_cursor(cursor);
|
||||
if resp.drag_started_by(PointerButton::Primary) {
|
||||
ctx.send_viewport_cmd(ViewportCommand::BeginResize(dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use egui::{Event, Pos2, RawInput, ViewportId};
|
||||
|
||||
struct Win {
|
||||
ctx: egui::Context,
|
||||
app: PdfCraftApp,
|
||||
width: f32,
|
||||
time: f64,
|
||||
maximized: bool,
|
||||
}
|
||||
|
||||
impl Win {
|
||||
fn new(width: f32) -> Self {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.custom_titlebar = true;
|
||||
crate::theme::apply(&ctx, app.theme);
|
||||
Self { ctx, app, width, time: 0.0, maximized: false }
|
||||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<Event>) -> egui::FullOutput {
|
||||
self.time += 0.05;
|
||||
let mut input =
|
||||
RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(self.width, 600.0))), time: Some(self.time), events, ..Default::default() };
|
||||
if let Some(v) = input.viewports.get_mut(&ViewportId::ROOT) {
|
||||
v.maximized = Some(self.maximized);
|
||||
}
|
||||
let app = &mut self.app;
|
||||
let width = self.width;
|
||||
let mut out = self.ctx.run_ui(input, |ui| {
|
||||
let bar = Rect::from_min_size(Pos2::ZERO, vec2(width, HEIGHT));
|
||||
drag_area(ui, bar);
|
||||
caption_buttons(app, ui, bar);
|
||||
resize_zones(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
out
|
||||
}
|
||||
|
||||
fn gesture(&mut self, path: &[Pos2]) -> Vec<ViewportCommand> {
|
||||
let button = |p: Pos2, pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
let mut events = vec![vec![Event::PointerMoved(path[0])], vec![button(path[0], true)]];
|
||||
events.extend(path[1..].iter().map(|p| vec![Event::PointerMoved(*p)]));
|
||||
events.push(vec![button(*path.last().unwrap_or(&path[0]), false)]);
|
||||
events.into_iter().flat_map(|e| self.frame(e).viewport_output.remove(&ViewportId::ROOT).map(|v| v.commands).unwrap_or_default()).collect()
|
||||
}
|
||||
|
||||
fn click(&mut self, p: Pos2) -> Vec<ViewportCommand> {
|
||||
self.gesture(&[p])
|
||||
}
|
||||
|
||||
fn caption(&self, c: Caption) -> Rect {
|
||||
self.ctx.read_response(caption_id(c)).map(|r| r.rect).unwrap_or(Rect::NOTHING)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caption_buttons_take_the_right_end_of_the_row() {
|
||||
for width in [800.0, 1440.0] {
|
||||
let mut w = Win::new(width);
|
||||
w.frame(vec![]);
|
||||
w.frame(vec![]);
|
||||
let [min, max, close] = CAPTIONS.map(|c| w.caption(c));
|
||||
assert_eq!((close.right(), close.top()), (width, 0.0), "Close sits in the window's corner at {width}");
|
||||
assert!(close.height() >= 30.0 && [min, max, close].iter().all(|r| r.width() == BUTTON_WIDTH));
|
||||
assert_eq!((min.right(), max.right()), (max.left(), close.left()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimize_maximize_and_close_follow_the_window_state() {
|
||||
let mut w = Win::new(1000.0);
|
||||
w.frame(vec![]);
|
||||
let (min, max, close) = (w.caption(Caption::Minimize).center(), w.caption(Caption::Maximize).center(), w.caption(Caption::Close).center());
|
||||
assert!(w.click(min).contains(&ViewportCommand::Minimized(true)));
|
||||
assert!(w.click(max).contains(&ViewportCommand::Maximized(true)));
|
||||
w.maximized = true;
|
||||
assert!(w.click(max).contains(&ViewportCommand::Maximized(false)), "Restore");
|
||||
assert!(w.click(close).contains(&ViewportCommand::Close));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_row_space_drags_and_double_click_maximizes() {
|
||||
let mut w = Win::new(1440.0);
|
||||
w.frame(vec![]);
|
||||
let empty = pos2(700.0, 16.0);
|
||||
let cmds = w.gesture(&[empty, empty + vec2(12.0, 4.0), empty + vec2(30.0, 8.0)]);
|
||||
assert_eq!(cmds.iter().filter(|c| matches!(c, ViewportCommand::StartDrag)).count(), 1, "{cmds:?}");
|
||||
let mut cmds = w.click(empty);
|
||||
cmds.extend(w.click(empty));
|
||||
assert!(cmds.contains(&ViewportCommand::Maximized(true)), "{cmds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_zones_resize_unless_maximized() {
|
||||
use ResizeDirection::*;
|
||||
let mut w = Win::new(1000.0);
|
||||
w.frame(vec![]);
|
||||
for (p, dir) in [
|
||||
(pos2(1.0, 300.0), West),
|
||||
(pos2(998.0, 300.0), East),
|
||||
(pos2(500.0, 1.0), North),
|
||||
(pos2(500.0, 598.0), South),
|
||||
(pos2(2.0, 2.0), NorthWest),
|
||||
(pos2(997.0, 3.0), NorthEast),
|
||||
(pos2(3.0, 597.0), SouthWest),
|
||||
(pos2(998.0, 598.0), SouthEast),
|
||||
] {
|
||||
let cmds = w.gesture(&[p, p + vec2(6.0, 6.0)]);
|
||||
assert_eq!(cmds, vec![ViewportCommand::BeginResize(dir)], "at {p:?}");
|
||||
}
|
||||
w.maximized = true;
|
||||
let cmds = w.gesture(&[pos2(1.0, 300.0), pos2(8.0, 300.0)]);
|
||||
assert!(!cmds.iter().any(|c| matches!(c, ViewportCommand::BeginResize(_))), "{cmds:?}");
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
Produces, in $env:DIST (default: dist/release):
|
||||
pdfcraft-<version>-windows-<arch>.msi per-machine installer (WiX v5)
|
||||
pdfcraft-<version>-windows-<arch>-portable.zip pdfcraft.exe + pdfcraft-cli.exe + models\ + portable.txt
|
||||
pdfcraft-cli-<version>-windows-<arch>.zip pdfcraft-cli.exe standalone
|
||||
|
||||
The binaries link the C runtime statically (+crt-static), so neither the MSI nor the portable
|
||||
zip needs the Visual C++ redistributable. Signing is delegated to sign.ps1 (skipped with a
|
||||
@@ -25,6 +26,7 @@ param(
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
Set-Location $Root
|
||||
|
||||
function Invoke-Native([string] $What, [scriptblock] $Block) {
|
||||
Write-Output "==> $What"
|
||||
@@ -56,6 +58,17 @@ if (-not $env:PDFCRAFT_BUILD_DATE) { $env:PDFCRAFT_BUILD_DATE = (Get-Date).ToUni
|
||||
Write-Output "PdfCraft $Version for Windows $Arch ($Target)"
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
# Locate rc.exe if not in PATH
|
||||
if (-not (Get-Command rc.exe -ErrorAction SilentlyContinue)) {
|
||||
$kits = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
|
||||
$foundRc = Get-ChildItem -Path $kits -Recurse -Filter rc.exe -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -match '\\x64\\rc\.exe$' } |
|
||||
Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if ($foundRc) {
|
||||
$env:PATH = "$($foundRc.DirectoryName);$env:PATH"
|
||||
}
|
||||
}
|
||||
|
||||
# Static CRT: no VC++ redistributable needed. Scoped to the target so host build scripts and
|
||||
# proc-macros are unaffected.
|
||||
$flagVar = 'CARGO_TARGET_' + ($Target.ToUpper() -replace '-', '_') + '_RUSTFLAGS'
|
||||
@@ -152,6 +165,19 @@ $Zip = Join-Path $Dist "pdfcraft-$Version-windows-$Arch-portable.zip"
|
||||
Remove-Item -Force $Zip -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path $Portable -DestinationPath $Zip
|
||||
|
||||
# ---- cli zip -----------------------------------------------------------------------------------
|
||||
$CliPortable = Join-Path $TargetDir "windows-package\pdfcraft-cli-$Version-windows-$Arch"
|
||||
Remove-Item -Recurse -Force $CliPortable -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $CliPortable | Out-Null
|
||||
Copy-Item (Join-Path $Stage 'pdfcraft-cli.exe') $CliPortable
|
||||
foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') {
|
||||
$p = Join-Path $Root $f
|
||||
if (Test-Path $p) { Copy-Item $p $CliPortable }
|
||||
}
|
||||
$CliZip = Join-Path $Dist "pdfcraft-cli-$Version-windows-$Arch.zip"
|
||||
Remove-Item -Force $CliZip -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path $CliPortable -DestinationPath $CliZip
|
||||
|
||||
# Smoke-test the CLI when this machine can run it. An ARM64 build made on an x64 runner can't run
|
||||
# here; .github/workflows/windows-arm64.yml installs and runs it on ARM64 instead.
|
||||
$HostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
|
||||
@@ -160,4 +186,4 @@ if ($Arch -ne 'arm64' -or $HostArch -eq 'arm64') {
|
||||
} else {
|
||||
Write-Output "skipping pdfcraft-cli --version: an $Arch build doesn't run on this $HostArch machine"
|
||||
}
|
||||
Get-Item $Msi, $Zip | Format-Table Name, Length
|
||||
Get-Item $Msi, $Zip, $CliZip | Format-Table Name, Length
|
||||
|
||||
Reference in New Issue
Block a user