//! Design tokens and egui style (plan/acrobat/02-ui-ux.md ยง1: neutral chrome, white panels, //! one blue accent; Dark Gray keeps pages white). Every custom widget reads `Tokens::get`. use std::sync::Arc; use egui::{Color32, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, Stroke, Visuals}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] pub enum ThemeKind { #[default] Light, Dark, } #[derive(Clone, Copy, Debug)] pub struct Tokens { pub kind: ThemeKind, /// Title/tab strip. pub titlebar: Color32, /// Mode bar, panels. pub chrome: Color32, pub panel: Color32, /// Document area behind pages. pub pasteboard: Color32, pub card: Color32, pub border: Color32, pub divider: Color32, pub text: Color32, pub text_muted: Color32, pub text_faint: Color32, pub icon: Color32, pub hover: Color32, pub pressed: Color32, pub selected: Color32, pub accent: Color32, pub accent_text: Color32, pub accent_soft: Color32, pub field: Color32, pub badge_new: Color32, pub page_shadow: Color32, pub radius: u8, } impl Tokens { pub fn for_kind(kind: ThemeKind) -> Self { match kind { ThemeKind::Light => Self { kind, titlebar: Color32::from_rgb(0xE9, 0xE9, 0xEB), chrome: Color32::from_rgb(0xFF, 0xFF, 0xFF), panel: Color32::from_rgb(0xFF, 0xFF, 0xFF), pasteboard: Color32::from_rgb(0xF1, 0xF1, 0xF3), card: Color32::from_rgb(0xFF, 0xFF, 0xFF), border: Color32::from_rgb(0xDA, 0xDA, 0xDE), divider: Color32::from_rgb(0xE8, 0xE8, 0xEB), text: Color32::from_rgb(0x22, 0x22, 0x26), text_muted: Color32::from_rgb(0x5E, 0x5E, 0x66), text_faint: Color32::from_rgb(0x8E, 0x8E, 0x96), icon: Color32::from_rgb(0x44, 0x44, 0x4B), hover: Color32::from_rgb(0xF0, 0xF0, 0xF3), pressed: Color32::from_rgb(0xE4, 0xE4, 0xE9), selected: Color32::from_rgb(0xE6, 0xEE, 0xFD), accent: Color32::from_rgb(0x1B, 0x63, 0xE0), accent_text: Color32::from_rgb(0x17, 0x55, 0xC4), accent_soft: Color32::from_rgb(0xE3, 0xEC, 0xFD), field: Color32::from_rgb(0xFF, 0xFF, 0xFF), badge_new: Color32::from_rgb(0x1B, 0x63, 0xE0), page_shadow: Color32::from_black_alpha(34), radius: 6, }, ThemeKind::Dark => Self { kind, titlebar: Color32::from_rgb(0x1B, 0x1B, 0x1E), chrome: Color32::from_rgb(0x26, 0x26, 0x2A), panel: Color32::from_rgb(0x26, 0x26, 0x2A), pasteboard: Color32::from_rgb(0x19, 0x19, 0x1C), card: Color32::from_rgb(0x2E, 0x2E, 0x33), border: Color32::from_rgb(0x3C, 0x3C, 0x43), divider: Color32::from_rgb(0x33, 0x33, 0x39), text: Color32::from_rgb(0xEC, 0xEC, 0xEF), text_muted: Color32::from_rgb(0xAE, 0xAE, 0xB6), text_faint: Color32::from_rgb(0x80, 0x80, 0x89), icon: Color32::from_rgb(0xD4, 0xD4, 0xDA), hover: Color32::from_rgb(0x34, 0x34, 0x3A), pressed: Color32::from_rgb(0x3E, 0x3E, 0x45), selected: Color32::from_rgb(0x23, 0x3A, 0x63), accent: Color32::from_rgb(0x4B, 0x8B, 0xF5), accent_text: Color32::from_rgb(0x8C, 0xB6, 0xFA), accent_soft: Color32::from_rgb(0x24, 0x36, 0x57), field: Color32::from_rgb(0x1E, 0x1E, 0x22), badge_new: Color32::from_rgb(0x3D, 0x7D, 0xEE), page_shadow: Color32::from_black_alpha(120), radius: 6, }, } } pub fn get(ctx: &egui::Context) -> Self { ctx.data(|d| d.get_temp::(egui::Id::new("printcraft-theme"))).unwrap_or_else(|| Self::for_kind(ThemeKind::Light)) } pub fn dark(&self) -> bool { self.kind == ThemeKind::Dark } } pub fn install_fonts(ctx: &egui::Context) { ctx.set_fonts(font_definitions()); } /// The interface fonts: Inter (and JetBrains Mono for code) first, then egui's defaults, then /// the Japanese faces of the optional craft-fonts build input (BIZ UDPGothic first) as the last /// fallback in every family. Without craft-fonts there is no Japanese face. pub fn font_definitions() -> FontDefinitions { let mut fonts = FontDefinitions::default(); let add = |fonts: &mut FontDefinitions, name: &str, bytes: &'static [u8]| { fonts.font_data.insert(name.to_owned(), Arc::new(FontData::from_static(bytes))); }; add(&mut fonts, "Inter", include_bytes!("../../../assets/fonts/Inter-Regular.ttf")); add(&mut fonts, "Inter-Medium", include_bytes!("../../../assets/fonts/Inter-Medium.ttf")); add(&mut fonts, "Inter-SemiBold", include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf")); add(&mut fonts, "JetBrainsMono", include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf")); fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "Inter".to_owned()); fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "JetBrainsMono".to_owned()); // The same static bytes printcraft-fonts uses for Japanese text in PDFs: one copy, not two. for face in printcraft_fonts::ui_japanese_fonts() { let name = face.name(); add(&mut fonts, &name, face.bytes); for family in [FontFamily::Proportional, FontFamily::Monospace] { fonts.families.entry(family).or_default().push(name.clone()); } } let fallback: Vec = fonts.families[&FontFamily::Proportional].clone(); for (fam, primary) in [("medium", "Inter-Medium"), ("semibold", "Inter-SemiBold")] { let mut stack = vec![primary.to_owned()]; stack.extend(fallback.iter().cloned()); fonts.families.insert(FontFamily::Name(fam.into()), stack); } fonts } pub fn regular(size: f32) -> FontId { FontId::proportional(size) } pub fn medium(size: f32) -> FontId { FontId::new(size, FontFamily::Name("medium".into())) } pub fn semibold(size: f32) -> FontId { FontId::new(size, FontFamily::Name("semibold".into())) } pub fn apply(ctx: &egui::Context, kind: ThemeKind) { let t = Tokens::for_kind(kind); ctx.data_mut(|d| d.insert_temp(egui::Id::new("printcraft-theme"), t)); let mut v = if t.dark() { Visuals::dark() } else { Visuals::light() }; v.panel_fill = t.panel; v.window_fill = t.card; v.window_stroke = Stroke::new(1.0, t.border); v.extreme_bg_color = t.field; v.faint_bg_color = t.hover; v.selection.bg_fill = t.accent_soft; v.selection.stroke = Stroke::new(1.0, t.accent); v.hyperlink_color = t.accent_text; v.override_text_color = Some(t.text); v.window_corner_radius = CornerRadius::same(10); v.menu_corner_radius = CornerRadius::same(8); v.window_shadow = egui::Shadow { offset: [0, 8], blur: 28, spread: 0, color: Color32::from_black_alpha(if t.dark() { 110 } else { 38 }) }; v.popup_shadow = egui::Shadow { offset: [0, 4], blur: 16, spread: 0, color: Color32::from_black_alpha(if t.dark() { 90 } else { 30 }) }; for w in [&mut v.widgets.noninteractive, &mut v.widgets.inactive, &mut v.widgets.hovered, &mut v.widgets.active, &mut v.widgets.open] { w.corner_radius = CornerRadius::same(t.radius); w.fg_stroke.color = t.text; } v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, t.divider); v.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; v.widgets.inactive.bg_fill = t.field; v.widgets.inactive.bg_stroke = Stroke::NONE; v.widgets.hovered.weak_bg_fill = t.hover; v.widgets.hovered.bg_fill = t.hover; v.widgets.hovered.bg_stroke = Stroke::NONE; v.widgets.active.weak_bg_fill = t.pressed; v.widgets.active.bg_fill = t.pressed; v.widgets.open.weak_bg_fill = t.hover; ctx.set_visuals(v); ctx.global_style_mut(|s| { s.spacing.item_spacing = egui::vec2(8.0, 6.0); s.spacing.button_padding = egui::vec2(10.0, 5.0); s.spacing.menu_margin = egui::Margin::same(6); s.spacing.scroll.bar_width = 8.0; s.spacing.scroll.floating = true; s.text_styles.insert(egui::TextStyle::Body, regular(13.0)); s.text_styles.insert(egui::TextStyle::Button, regular(13.0)); s.text_styles.insert(egui::TextStyle::Small, regular(11.0)); s.text_styles.insert(egui::TextStyle::Heading, semibold(17.0)); s.interaction.tooltip_delay = 0.35; }); }