use egui::Context;
use serde::{Deserialize, Serialize};
pub mod keymap;
pub mod metrics;
pub mod oklch;
pub mod palette;
pub mod policy;
pub mod scroll;
pub mod typography;
pub use keymap::{Action, KeyMap, keymap, publish_keymap};
pub use metrics::Metrics;
pub use oklch::{Oklch, contrast_ratio, relative_luminance};
pub use palette::Palette;
pub use policy::{EffectsPolicy, FocusSpec, Motion, PerfConfig, SurfaceSpec, ThemeMode};
pub use scroll::{ScrollSpec, ScrollVisibility};
pub use typography::{Typography, UiFont};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Theme {
pub name: String,
pub mode: ThemeMode,
pub palette: Palette,
pub typography: Typography,
pub metrics: Metrics,
pub scroll: ScrollSpec,
pub keymap: KeyMap,
pub focus: FocusSpec,
pub surface: SurfaceSpec,
pub motion: Motion,
pub effects: EffectsPolicy,
pub perf: PerfConfig,
}
impl Default for Theme {
fn default() -> Self {
Theme::windows_dark()
}
}
impl Theme {
pub fn windows_dark() -> Self {
Self {
name: "windows-dark".into(),
mode: ThemeMode::Dark,
palette: presets::windows_dark_palette(),
typography: Typography::default().with_font(UiFont::SegoeUi),
metrics: Metrics::windows(),
scroll: ScrollSpec::windows(),
keymap: KeyMap::windows(),
focus: FocusSpec::default(),
surface: SurfaceSpec::Opaque,
motion: Motion::default(),
effects: EffectsPolicy::Full,
perf: PerfConfig::default(),
}
}
pub fn windows_light() -> Self {
Self {
name: "windows-light".into(),
mode: ThemeMode::Light,
palette: presets::windows_light_palette(),
..Theme::windows_dark()
}
}
pub fn windows() -> Self {
Theme::windows_dark()
}
pub fn macos_dark() -> Self {
Self {
name: "macos-dark".into(),
mode: ThemeMode::Dark,
palette: presets::macos_dark_palette(),
typography: Typography::default().with_font(UiFont::SanFrancisco),
metrics: Metrics::macos(),
scroll: ScrollSpec::macos(),
keymap: KeyMap::macos(),
focus: FocusSpec::default(),
surface: SurfaceSpec::Opaque,
motion: Motion::default(),
effects: EffectsPolicy::Full,
perf: PerfConfig::default(),
}
}
pub fn macos_light() -> Self {
Self {
name: "macos-light".into(),
mode: ThemeMode::Light,
palette: presets::macos_light_palette(),
..Theme::macos_dark()
}
}
pub fn macos() -> Self {
Theme::macos_dark()
}
pub fn device() -> Self {
Self {
name: "device".into(),
mode: ThemeMode::Dark,
palette: presets::device_palette(),
typography: Typography::default().with_font(UiFont::System),
metrics: Metrics::device(),
scroll: ScrollSpec::device(),
keymap: KeyMap::device(),
focus: FocusSpec { hints_enabled: false, revolver_enabled: false, ..FocusSpec::default() },
surface: SurfaceSpec::Opaque,
motion: Motion { duration: 0.0, fast: 0.0 }, effects: EffectsPolicy::None,
perf: PerfConfig { prefer_wgpu: true, ..PerfConfig::default() },
}
}
pub fn from_os(os: egui::os::OperatingSystem) -> Self {
use egui::os::OperatingSystem as Os;
match os {
Os::Mac | Os::IOS => Theme::macos_dark(),
Os::Windows => Theme::windows_dark(),
_ => Theme::windows_dark(),
}
}
pub const PRESETS: &'static [fn() -> Theme] = &[
Theme::windows_light,
Theme::windows_dark,
Theme::macos_light,
Theme::macos_dark,
Theme::device,
];
pub fn preset_names() -> Vec<String> {
Self::PRESETS.iter().map(|c| c().name).collect()
}
pub fn by_name(name: &str) -> Option<Theme> {
let norm = |s: &str| s.to_ascii_lowercase().replace([' ', '_'], "-");
let want = norm(name);
Self::PRESETS.iter().map(|c| c()).find(|t| norm(&t.name) == want)
}
pub fn with_effects(mut self, e: EffectsPolicy) -> Self {
self.effects = e;
self
}
pub fn with_keymap(mut self, k: KeyMap) -> Self {
self.keymap = k;
self
}
pub fn with_focus(mut self, f: FocusSpec) -> Self {
self.focus = f;
self
}
pub fn with_surface(mut self, s: SurfaceSpec) -> Self {
self.surface = s;
self
}
pub fn with_name(mut self, n: impl Into<String>) -> Self {
self.name = n.into();
self
}
pub fn is_dark(&self) -> bool {
match self.mode {
ThemeMode::Dark => true,
ThemeMode::Light => false,
ThemeMode::FollowSystem => self.palette.dark,
}
}
pub fn egui_style(&self) -> egui::Style {
let mut style = egui::Style::default();
let mut visuals = self.palette.to_visuals(self.metrics.corner_radius);
visuals.window_corner_radius = egui::CornerRadius::same(self.metrics.window_corner_radius);
visuals.menu_corner_radius = egui::CornerRadius::same(self.metrics.menu_corner_radius);
style.visuals = visuals;
let sp = &mut style.spacing;
sp.item_spacing = self.metrics.item_spacing_vec();
sp.button_padding = self.metrics.button_padding_vec();
sp.window_margin = self.metrics.window_margin_m();
sp.menu_margin = self.metrics.menu_margin_m();
sp.interact_size = self.metrics.interact_size_vec();
sp.indent = self.metrics.indent;
sp.slider_width = self.metrics.slider_width;
sp.icon_width = self.metrics.icon_width;
sp.scroll = self.scroll.to_scroll_style();
style.text_styles = self.typography.text_styles();
style
}
pub fn apply(&self, ctx: &Context) {
let os = if self.name.starts_with("macos") {
egui::os::OperatingSystem::Mac
} else {
egui::os::OperatingSystem::Windows
};
ctx.set_os(os);
ctx.set_global_style(self.egui_style());
crate::theme::publish_palette(ctx, self.to_legacy_palette());
keymap::publish_keymap(ctx, self.keymap.clone());
}
pub fn to_legacy_palette(&self) -> crate::Theme {
let p = &self.palette;
let name: &'static str = match self.name.as_str() {
"windows-dark" => "windows-dark",
"windows-light" => "windows-light",
"macos-dark" => "macos-dark",
"macos-light" => "macos-light",
"device" => "device",
_ => "look",
};
crate::Theme {
name,
bg: p.surface.to_color32(),
node_fill: p.surface_container.to_color32(),
node_stroke: p.outline.to_color32(),
edge: p.outline.with_chroma_scale(0.6).to_color32(),
text: p.on_surface.to_color32(),
text_dim: p.on_surface_dim.to_color32(),
accent: p.accent.to_color32(),
point: p.primary.to_color32(),
panel_bg: p.surface_container.to_color32(),
panel_stroke: p.outline.to_color32(),
glow: p.glow.to_color32(),
}
}
}
mod presets;
#[cfg(test)]
mod tests;