Files
lightcraft/crates/ui-egui/src/theme.rs
T
Brandon ThomasandClaude Opus 5.5 f0ffa11333 M0.1: enforce the asset rule in CI (cargo xtask assets); replace Adobe-published Source Sans 3 with Inter
Every image/icon/font/media/profile file must match an assets/ATTRIBUTION.md path pattern;
Adobe profile/template formats and Adobe-named assets are rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:52:33 -04:00

165 lines
6.4 KiB
Rust

//! Design tokens (colours, sizes, fonts). Values measured from black-box observation of the
//! reference app's dark theme (see plan/lightroom/10-observed-ui.md); everything is our own code.
use std::sync::Arc;
use egui::{Color32, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, Stroke, Visuals};
pub const FONT_SEMIBOLD: &str = "semibold";
#[derive(Clone, Copy, Debug)]
pub struct Tokens {
/// Top bar, side panels, bottom bar, tool strip.
pub chrome: Color32,
/// Photo canvas (detail view) and filmstrip.
pub canvas: Color32,
/// Grid background behind the cells.
pub grid_bg: Color32,
pub cell: Color32,
pub cell_selected: Color32,
pub divider: Color32,
pub inset: Color32,
pub field: Color32,
pub field_border: Color32,
pub button: Color32,
pub button_border: Color32,
pub hover: Color32,
pub pressed: Color32,
pub tool_active: Color32,
pub text: Color32,
pub text_label: Color32,
pub text_dim: Color32,
pub text_disabled: Color32,
/// Icon glyphs in the chrome.
pub icon: Color32,
pub track: Color32,
pub thumb: Color32,
pub thumb_hover: Color32,
pub accent: Color32,
pub star: Color32,
pub pick: Color32,
pub reject: Color32,
pub mask_overlay: Color32,
// metrics (points)
pub top_bar_h: f32,
pub bottom_bar_h: f32,
pub panel_w: f32,
pub strip_w: f32,
pub left_w: f32,
pub slider_row_h: f32,
pub section_h: f32,
pub film_h: f32,
}
impl Default for Tokens {
fn default() -> Self {
Tokens {
chrome: Color32::from_rgb(0x2d, 0x2d, 0x2d),
canvas: Color32::from_rgb(0x1c, 0x1c, 0x1c),
grid_bg: Color32::from_rgb(0x0f, 0x0f, 0x0f),
cell: Color32::from_rgb(0x1c, 0x1c, 0x1c),
cell_selected: Color32::from_rgb(0x2d, 0x2d, 0x2d),
divider: Color32::from_rgb(0x1c, 0x1c, 0x1c),
inset: Color32::from_rgb(0x23, 0x23, 0x23),
field: Color32::from_rgb(0x22, 0x22, 0x22),
field_border: Color32::from_rgb(0x3c, 0x3c, 0x3c),
button: Color32::from_rgb(0x24, 0x24, 0x24),
button_border: Color32::from_rgb(0x4c, 0x4c, 0x4c),
hover: Color32::from_rgb(0x3a, 0x3a, 0x3a),
pressed: Color32::from_rgb(0x3f, 0x3f, 0x3f),
tool_active: Color32::from_rgb(0x3f, 0x3f, 0x3f),
text: Color32::from_rgb(0xe2, 0xe2, 0xe2),
text_label: Color32::from_rgb(0xbc, 0xbc, 0xbc),
text_dim: Color32::from_rgb(0x8e, 0x8e, 0x8e),
text_disabled: Color32::from_rgb(0x5c, 0x5c, 0x5c),
icon: Color32::from_rgb(0x9a, 0x9a, 0x9a),
track: Color32::from_rgb(0x5a, 0x5a, 0x5a),
thumb: Color32::from_rgb(0xa0, 0xa0, 0xa0),
thumb_hover: Color32::from_rgb(0xe0, 0xe0, 0xe0),
accent: Color32::from_rgb(0x01, 0x65, 0xdd),
star: Color32::from_rgb(0xd8, 0xd8, 0xd8),
pick: Color32::from_rgb(0xf0, 0xf0, 0xf0),
reject: Color32::from_rgb(0xe0, 0x4a, 0x4a),
mask_overlay: Color32::from_rgba_unmultiplied(0xe0, 0x20, 0x30, 110),
top_bar_h: 42.0,
bottom_bar_h: 48.0,
panel_w: 270.0,
strip_w: 48.0,
left_w: 268.0,
slider_row_h: 45.0,
section_h: 57.0,
film_h: 142.0,
}
}
}
impl Tokens {
pub fn get(ctx: &egui::Context) -> Tokens {
ctx.data(|d| d.get_temp::<Tokens>(egui::Id::NULL)).unwrap_or_default()
}
pub fn font(&self, size: f32) -> FontId {
FontId::proportional(size)
}
pub fn semibold(&self, size: f32) -> FontId {
FontId::new(size, FontFamily::Name(FONT_SEMIBOLD.into()))
}
}
pub fn install_fonts(ctx: &egui::Context) {
let mut fonts = FontDefinitions::default();
fonts.font_data.insert("Inter".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/Inter-Regular.ttf"))));
fonts.font_data.insert("Inter-SemiBold".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"))));
let fallback: Vec<String> = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default();
let mut prop = vec!["Inter".to_string()];
prop.extend(fallback.clone());
fonts.families.insert(FontFamily::Proportional, prop);
let mut semi = vec!["Inter-SemiBold".to_string()];
semi.extend(fallback);
fonts.families.insert(FontFamily::Name(FONT_SEMIBOLD.into()), semi);
ctx.set_fonts(fonts);
}
pub fn apply(ctx: &egui::Context) {
let t = Tokens::default();
ctx.data_mut(|d| d.insert_temp(egui::Id::NULL, t));
let mut v = Visuals::dark();
v.panel_fill = t.chrome;
v.window_fill = t.chrome;
v.extreme_bg_color = t.field;
v.faint_bg_color = t.inset;
v.window_stroke = Stroke::new(1.0, t.button_border);
v.window_corner_radius = CornerRadius::same(6);
v.menu_corner_radius = CornerRadius::same(6);
v.selection.bg_fill = t.accent.gamma_multiply(0.6);
v.selection.stroke = Stroke::new(1.0, t.accent);
v.override_text_color = Some(t.text_label);
v.popup_shadow = egui::epaint::Shadow { offset: [0, 4], blur: 16, spread: 0, color: Color32::from_black_alpha(140) };
v.window_shadow = egui::epaint::Shadow { offset: [0, 8], blur: 30, spread: 0, color: Color32::from_black_alpha(160) };
let w = &mut v.widgets;
for (wv, fill) in [
(&mut w.noninteractive, t.chrome),
(&mut w.inactive, t.button),
(&mut w.hovered, t.hover),
(&mut w.active, t.pressed),
(&mut w.open, t.hover),
] {
wv.bg_fill = fill;
wv.weak_bg_fill = fill;
wv.corner_radius = CornerRadius::same(4);
wv.fg_stroke = Stroke::new(1.0, t.text_label);
}
w.noninteractive.bg_stroke = Stroke::new(1.0, t.divider);
w.inactive.bg_stroke = Stroke::new(1.0, t.button_border);
ctx.set_visuals(v);
ctx.global_style_mut(|s| {
s.spacing.item_spacing = egui::vec2(6.0, 4.0);
s.spacing.button_padding = egui::vec2(8.0, 3.0);
s.spacing.interact_size.y = 22.0;
s.text_styles.insert(egui::TextStyle::Body, FontId::proportional(13.0));
s.text_styles.insert(egui::TextStyle::Button, FontId::proportional(13.0));
s.text_styles.insert(egui::TextStyle::Small, FontId::proportional(11.0));
s.text_styles.insert(egui::TextStyle::Heading, FontId::new(16.0, FontFamily::Name(FONT_SEMIBOLD.into())));
s.animation_time = 0.08;
});
}