#![allow(clippy::unwrap_used)] use crate::{design_tokens, CUSTOM_WINDOW_DECORATIONS};
#[derive(Clone, Debug)]
pub struct DesignTokens {
pub json: serde_json::Value,
pub top_bar_color: egui::Color32,
pub bottom_bar_color: egui::Color32,
pub bottom_bar_stroke: egui::Stroke,
pub bottom_bar_rounding: egui::Rounding,
pub shadow_gradient_dark_start: egui::Color32,
pub tab_bar_color: egui::Color32,
pub native_frame_stroke: egui::Stroke,
}
impl DesignTokens {
pub fn load() -> Self {
let json: serde_json::Value =
serde_json::from_str(include_str!("../data/design_tokens.json"))
.expect("Failed to parse data/design_tokens.json");
Self {
top_bar_color: get_aliased_color(&json, "{Alias.Color.Surface.Default.value}"),
bottom_bar_color: get_global_color(&json, "{Global.Color.Grey.150}"),
bottom_bar_stroke: egui::Stroke::new(
1.0,
get_global_color(&json, "{Global.Color.Grey.250}"),
),
bottom_bar_rounding: egui::Rounding {
nw: 6.0,
ne: 6.0,
sw: 0.0,
se: 0.0,
}, shadow_gradient_dark_start: egui::Color32::from_black_alpha(77),
tab_bar_color: get_global_color(&json, "{Global.Color.Grey.200}"),
native_frame_stroke: egui::Stroke::new(
1.0,
get_global_color(&json, "{Global.Color.Grey.250}"),
),
json,
}
}
pub(crate) fn apply(&self, ctx: &egui::Context) {
let apply_font = true;
let apply_font_size = true;
let typography_default: Typography =
get_alias(&self.json, "{Alias.Typography.Default.value}");
if apply_font {
assert_eq!(typography_default.fontFamily, "Inter");
assert_eq!(typography_default.fontWeight, "Medium");
let mut font_definitions = egui::FontDefinitions::default();
font_definitions.font_data.insert(
"Inter-Medium".into(),
egui::FontData::from_static(include_bytes!("../data/Inter-Medium.otf")),
);
font_definitions
.families
.get_mut(&egui::FontFamily::Proportional)
.unwrap()
.insert(0, "Inter-Medium".into());
ctx.set_fonts(font_definitions);
}
let mut egui_style = egui::Style {
visuals: egui::Visuals::dark(),
..Default::default()
};
if apply_font_size {
let font_size = parse_px(&typography_default.fontSize);
for text_style in [
egui::TextStyle::Body,
egui::TextStyle::Monospace,
egui::TextStyle::Button,
] {
egui_style.text_styles.get_mut(&text_style).unwrap().size = font_size;
}
egui_style
.text_styles
.get_mut(&egui::TextStyle::Heading)
.unwrap()
.size = 16.0;
egui_style.spacing.interact_size.y = 15.0;
}
egui_style
.text_styles
.insert(Self::welcome_screen_h1(), egui::FontId::proportional(41.0));
egui_style
.text_styles
.insert(Self::welcome_screen_h2(), egui::FontId::proportional(27.0));
egui_style.text_styles.insert(
Self::welcome_screen_example_title(),
egui::FontId::proportional(13.0),
);
egui_style.text_styles.insert(
Self::welcome_screen_body(),
egui::FontId::proportional(15.0),
);
egui_style
.text_styles
.insert(Self::welcome_screen_tag(), egui::FontId::proportional(10.5));
let panel_bg_color = get_aliased_color(&self.json, "{Alias.Color.Surface.Default.value}");
let floating_color = get_global_color(&self.json, "{Global.Color.Grey.250}");
egui_style.visuals.faint_bg_color = get_global_color(&self.json, "{Global.Color.Grey.150}");
egui_style.visuals.extreme_bg_color = egui::Color32::BLACK;
egui_style.visuals.widgets.noninteractive.weak_bg_fill = panel_bg_color;
egui_style.visuals.widgets.noninteractive.bg_fill = panel_bg_color;
egui_style.visuals.button_frame = true;
egui_style.visuals.widgets.inactive.weak_bg_fill = Default::default(); egui_style.visuals.widgets.inactive.bg_fill =
get_global_color(&self.json, "{Global.Color.Grey.300}");
{
let hovered_color = get_global_color(&self.json, "{Global.Color.Grey.325}");
egui_style.visuals.widgets.hovered.weak_bg_fill = hovered_color;
egui_style.visuals.widgets.hovered.bg_fill = hovered_color;
egui_style.visuals.widgets.active.weak_bg_fill = hovered_color;
egui_style.visuals.widgets.active.bg_fill = hovered_color;
egui_style.visuals.widgets.open.weak_bg_fill = hovered_color;
egui_style.visuals.widgets.open.bg_fill = hovered_color;
}
{
egui_style.visuals.widgets.inactive.bg_stroke = Default::default();
egui_style.visuals.widgets.hovered.bg_stroke = Default::default();
egui_style.visuals.widgets.active.bg_stroke = Default::default();
egui_style.visuals.widgets.open.bg_stroke = Default::default();
}
{
egui_style.visuals.widgets.hovered.expansion = 2.0;
egui_style.visuals.widgets.active.expansion = 2.0;
egui_style.visuals.widgets.open.expansion = 2.0;
}
egui_style.visuals.selection.bg_fill =
get_aliased_color(&self.json, "{Alias.Color.Highlight.Default.value}");
egui_style.visuals.selection.stroke.color = egui::Color32::from_rgb(173, 184, 255); egui_style.visuals.widgets.noninteractive.bg_stroke.color =
get_global_color(&self.json, "{Global.Color.Grey.250}");
let subdued = get_aliased_color(&self.json, "{Alias.Color.Text.Subdued.value}");
let default = get_aliased_color(&self.json, "{Alias.Color.Text.Default.value}");
let strong = get_aliased_color(&self.json, "{Alias.Color.Text.Strong.value}");
egui_style.visuals.widgets.noninteractive.fg_stroke.color = subdued; egui_style.visuals.widgets.inactive.fg_stroke.color = default; egui_style.visuals.widgets.active.fg_stroke.color = strong; let wide_stroke_width = 2.0; egui_style.visuals.widgets.active.fg_stroke.width = wide_stroke_width;
egui_style.visuals.selection.stroke.width = wide_stroke_width;
let shadow = egui::epaint::Shadow {
offset: egui::vec2(0.0, 15.0),
blur: 50.0,
spread: 0.0,
color: egui::Color32::from_black_alpha(128),
};
egui_style.visuals.popup_shadow = shadow;
egui_style.visuals.window_shadow = shadow;
egui_style.visuals.window_fill = floating_color; egui_style.visuals.window_stroke = egui::Stroke::NONE;
egui_style.visuals.panel_fill = panel_bg_color;
egui_style.visuals.window_rounding = Self::window_rounding().into();
egui_style.visuals.menu_rounding = Self::window_rounding().into();
let small_rounding = Self::small_rounding().into();
egui_style.visuals.widgets.noninteractive.rounding = small_rounding;
egui_style.visuals.widgets.inactive.rounding = small_rounding;
egui_style.visuals.widgets.hovered.rounding = small_rounding;
egui_style.visuals.widgets.active.rounding = small_rounding;
egui_style.visuals.widgets.open.rounding = small_rounding;
egui_style.spacing.item_spacing = egui::vec2(8.0, 8.0);
egui_style.spacing.menu_margin = Self::view_padding().into();
egui_style.spacing.menu_spacing = 1.0;
egui_style.visuals.clip_rect_margin = 0.0;
egui_style.visuals.striped = false;
egui_style.visuals.indent_has_left_vline = false;
egui_style.spacing.button_padding = egui::Vec2::new(1.0, 0.0); egui_style.spacing.indent = 14.0; egui_style.spacing.combo_width = 8.0; egui_style.spacing.scroll.bar_inner_margin = 2.0;
egui_style.spacing.scroll.bar_width = 6.0;
egui_style.spacing.scroll.bar_outer_margin = 2.0;
egui_style.spacing.tooltip_width = 720.0;
egui_style.visuals.hyperlink_color = default;
egui_style.visuals.image_loading_spinners = false;
egui_style.visuals.error_fg_color = egui::Color32::from_rgb(0xAB, 0x01, 0x16);
egui_style.visuals.warn_fg_color = egui::Color32::from_rgb(0xFF, 0x7A, 0x0C);
ctx.set_style(egui_style);
}
#[inline]
pub fn welcome_screen_h1() -> egui::TextStyle {
egui::TextStyle::Name("welcome-screen-h1".into())
}
#[inline]
pub fn welcome_screen_h2() -> egui::TextStyle {
egui::TextStyle::Name("welcome-screen-h2".into())
}
#[inline]
pub fn welcome_screen_example_title() -> egui::TextStyle {
egui::TextStyle::Name("welcome-screen-example-title".into())
}
#[inline]
pub fn welcome_screen_body() -> egui::TextStyle {
egui::TextStyle::Name("welcome-screen-body".into())
}
#[inline]
pub fn welcome_screen_tag() -> egui::TextStyle {
egui::TextStyle::Name("welcome-screen-tag".into())
}
pub fn view_padding() -> f32 {
12.0
}
pub fn panel_margin() -> egui::Margin {
egui::Margin::symmetric(Self::view_padding(), 0.0)
}
pub fn window_rounding() -> f32 {
12.0
}
pub fn normal_rounding() -> f32 {
6.0
}
pub fn small_rounding() -> f32 {
4.0
}
pub fn table_line_height() -> f32 {
20.0 }
pub fn table_header_height() -> f32 {
20.0
}
pub fn top_bar_margin() -> egui::Margin {
egui::Margin::symmetric(8.0, 2.0)
}
pub fn text_to_icon_padding() -> f32 {
4.0
}
pub fn top_bar_height() -> f32 {
28.0 }
pub fn title_bar_height() -> f32 {
24.0 }
pub fn list_item_height() -> f32 {
24.0
}
pub fn native_window_rounding() -> f32 {
10.0
}
pub fn top_panel_frame() -> egui::Frame {
let mut frame = egui::Frame {
inner_margin: Self::top_bar_margin(),
fill: design_tokens().top_bar_color,
..Default::default()
};
if CUSTOM_WINDOW_DECORATIONS {
frame.rounding.nw = Self::native_window_rounding();
frame.rounding.ne = Self::native_window_rounding();
}
frame
}
pub fn bottom_panel_margin() -> egui::Margin {
Self::top_bar_margin()
}
pub fn bottom_panel_frame() -> egui::Frame {
let margin_offset = design_tokens().bottom_bar_stroke.width * 0.5;
let margin = Self::bottom_panel_margin();
let design_tokens = design_tokens();
let mut frame = egui::Frame {
fill: design_tokens.bottom_bar_color,
inner_margin: margin + margin_offset,
outer_margin: egui::Margin {
left: -margin_offset,
right: -margin_offset,
top: design_tokens.bottom_bar_stroke.width,
bottom: -margin_offset,
},
stroke: design_tokens.bottom_bar_stroke,
rounding: design_tokens.bottom_bar_rounding,
..Default::default()
};
if CUSTOM_WINDOW_DECORATIONS {
frame.rounding.sw = Self::native_window_rounding();
frame.rounding.se = Self::native_window_rounding();
}
frame
}
pub fn small_icon_size() -> egui::Vec2 {
egui::Vec2::splat(14.0)
}
pub fn setup_table_header(_header: &mut egui_extras::TableRow<'_, '_>) {}
pub fn setup_table_body(body: &mut egui_extras::TableBody<'_>) {
body.ui_mut().spacing_mut().interact_size.y = Self::table_line_height();
}
pub fn collapsing_triangle_area() -> egui::Vec2 {
Self::small_icon_size()
}
pub fn section_collapsing_header_color(&self) -> egui::Color32 {
get_global_color(&self.json, "{Global.Color.Grey.200}")
}
pub fn loop_selection_color() -> egui::Color32 {
egui::Color32::from_rgb(1, 37, 105) }
pub fn loop_everything_color() -> egui::Color32 {
egui::Color32::from_rgb(2, 80, 45) }
pub fn thumbnail_background_color(&self) -> egui::Color32 {
get_global_color(&self.json, "{Global.Color.Grey.250}")
}
}
fn get_aliased_color(json: &serde_json::Value, alias_path: &str) -> egui::Color32 {
parse_color(get_alias_str(json, alias_path))
}
fn get_global_color(json: &serde_json::Value, global_path: &str) -> egui::Color32 {
parse_color(global_path_value(json, global_path).as_str().unwrap())
}
fn get_alias_str<'json>(json: &'json serde_json::Value, alias_path: &str) -> &'json str {
let global_path = follow_path_or_panic(json, alias_path).as_str().unwrap();
global_path_value(json, global_path).as_str().unwrap()
}
fn get_alias<T: serde::de::DeserializeOwned>(json: &serde_json::Value, alias_path: &str) -> T {
let global_path = follow_path_or_panic(json, alias_path).as_str().unwrap();
let global_value = global_path_value(json, global_path);
serde_json::from_value(global_value.clone()).unwrap_or_else(|err| {
panic!(
"Failed to convert {global_path:?} to {}: {err}. Json: {json:?}",
std::any::type_name::<T>()
)
})
}
fn global_path_value<'json>(
value: &'json serde_json::Value,
global_path: &str,
) -> &'json serde_json::Value {
follow_path_or_panic(value, global_path)
.get("value")
.unwrap()
}
fn follow_path_or_panic<'json>(
json: &'json serde_json::Value,
json_path: &str,
) -> &'json serde_json::Value {
follow_path(json, json_path).unwrap_or_else(|| panic!("Failed to find {json_path:?}"))
}
fn follow_path<'json>(
mut value: &'json serde_json::Value,
path: &str,
) -> Option<&'json serde_json::Value> {
let path = path.strip_prefix('{')?;
let path = path.strip_suffix('}')?;
for component in path.split('.') {
value = value.get(component)?;
}
Some(value)
}
#[allow(non_snake_case)]
#[derive(serde::Deserialize)]
struct Typography {
fontSize: String,
fontWeight: String,
fontFamily: String,
}
fn parse_px(pixels: &str) -> f32 {
pixels.strip_suffix("px").unwrap().parse().unwrap()
}
fn parse_color(color: &str) -> egui::Color32 {
#![allow(clippy::identity_op)]
let color = color.strip_prefix('#').unwrap();
if color.len() == 6 {
let color = u32::from_str_radix(color, 16).unwrap();
egui::Color32::from_rgb(
((color >> 16) & 0xff) as u8,
((color >> 8) & 0xff) as u8,
((color >> 0) & 0xff) as u8,
)
} else if color.len() == 8 {
let color = u32::from_str_radix(color, 16).unwrap();
egui::Color32::from_rgba_unmultiplied(
((color >> 24) & 0xff) as u8,
((color >> 16) & 0xff) as u8,
((color >> 8) & 0xff) as u8,
((color >> 0) & 0xff) as u8,
)
} else {
panic!()
}
}
#[test]
fn test_design_tokens() {
let ctx = egui::Context::default();
crate::apply_style_and_install_loaders(&ctx);
let _ = ctx.run(Default::default(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
ui.label("Hello Test!");
});
});
}