From eb2b5bb7b3828f88eb595ea119046857ce5040c4 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Thu, 5 Dec 2024 12:14:40 +0100 Subject: [PATCH 1/8] Refactor design tokens to use a proper color table --- Cargo.lock | 1 + crates/viewer/re_ui/Cargo.toml | 1 + crates/viewer/re_ui/src/color_table.rs | 215 +++++++++++++++++++++++ crates/viewer/re_ui/src/design_tokens.rs | 106 ++++++----- crates/viewer/re_ui/src/lib.rs | 1 + 5 files changed, 282 insertions(+), 42 deletions(-) create mode 100644 crates/viewer/re_ui/src/color_table.rs diff --git a/Cargo.lock b/Cargo.lock index b071cb0aacb1..4194fb6a11b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6585,6 +6585,7 @@ dependencies = [ "egui_extras", "egui_kittest", "egui_tiles", + "nohash-hasher", "once_cell", "parking_lot", "rand", diff --git a/crates/viewer/re_ui/Cargo.toml b/crates/viewer/re_ui/Cargo.toml index 256b9ef2ce4d..f20917ec05ca 100644 --- a/crates/viewer/re_ui/Cargo.toml +++ b/crates/viewer/re_ui/Cargo.toml @@ -50,6 +50,7 @@ sublime_fuzzy.workspace = true eframe = { workspace = true, default-features = false, features = ["wgpu"] } egui_tiles.workspace = true +nohash-hasher.workspace = true rand.workspace = true diff --git a/crates/viewer/re_ui/src/color_table.rs b/crates/viewer/re_ui/src/color_table.rs new file mode 100644 index 000000000000..43004b532124 --- /dev/null +++ b/crates/viewer/re_ui/src/color_table.rs @@ -0,0 +1,215 @@ +use std::fmt::{Display, Formatter}; + +/// A hue for a [`ColorToken`]. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum Hue { + Grey, + Green, + Red, + Blue, + Purple, +} + +impl nohash_hasher::IsEnabled for Hue {} + +impl Hue { + pub fn all() -> &'static [Self] { + static ALL: [Hue; 5] = [Hue::Grey, Hue::Green, Hue::Red, Hue::Blue, Hue::Purple]; + &ALL + } +} + +impl Display for Hue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + // these must be as they appear in `design_token.json` + Self::Grey => f.write_str("Grey"), + Self::Green => f.write_str("Green"), + Self::Red => f.write_str("Red"), + Self::Blue => f.write_str("Blue"), + Self::Purple => f.write_str("Purple"), + } + } +} + +/// A shade for a [`ColorToken`]. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum Shade { + S0 = 0, + S25 = 25, + S50 = 50, + S75 = 75, + S100 = 100, + S125 = 125, + S150 = 150, + S175 = 175, + S200 = 200, + S225 = 225, + S250 = 250, + S275 = 275, + S300 = 300, + S325 = 325, + S350 = 350, + S375 = 375, + S400 = 400, + S425 = 425, + S450 = 450, + S475 = 475, + S500 = 500, + S525 = 525, + S550 = 550, + S575 = 575, + S600 = 600, + S625 = 625, + S650 = 650, + S675 = 675, + S700 = 700, + S725 = 725, + S750 = 750, + S775 = 775, + S800 = 800, + S825 = 825, + S850 = 850, + S875 = 875, + S900 = 900, + S925 = 925, + S950 = 950, + S975 = 975, + S1000 = 1000, +} + +impl nohash_hasher::IsEnabled for Shade {} + +impl Shade { + pub fn all() -> &'static [Self] { + static ALL: [Shade; 41] = [ + Shade::S0, + Shade::S25, + Shade::S50, + Shade::S75, + Shade::S100, + Shade::S125, + Shade::S150, + Shade::S175, + Shade::S200, + Shade::S225, + Shade::S250, + Shade::S275, + Shade::S300, + Shade::S325, + Shade::S350, + Shade::S375, + Shade::S400, + Shade::S425, + Shade::S450, + Shade::S475, + Shade::S500, + Shade::S525, + Shade::S550, + Shade::S575, + Shade::S600, + Shade::S625, + Shade::S650, + Shade::S675, + Shade::S700, + Shade::S725, + Shade::S750, + Shade::S775, + Shade::S800, + Shade::S825, + Shade::S850, + Shade::S875, + Shade::S900, + Shade::S925, + Shade::S950, + Shade::S975, + Shade::S1000, + ]; + &ALL + } + + pub fn as_u16(self) -> u16 { + self as u16 + } +} + +/// A table mapping all combination of [`Hue`] and [`Shade`] to a [`egui::Color32`]. +#[derive(Debug)] +pub struct ColorTable { + pub color_table: nohash_hasher::IntMap>, +} + +impl ColorTable { + #[inline] + pub fn get(&self, token: ColorToken) -> egui::Color32 { + *self + .color_table + .get(&token.hue) + .and_then(|m| m.get(&token.shade)) + .expect("The color table must always be complete.") + } + + #[inline] + pub fn grey(&self, shade: Shade) -> egui::Color32 { + self.get(ColorToken::grey(shade)) + } + + #[inline] + pub fn green(&self, shade: Shade) -> egui::Color32 { + self.get(ColorToken::green(shade)) + } + + #[inline] + pub fn red(&self, shade: Shade) -> egui::Color32 { + self.get(ColorToken::red(shade)) + } + + #[inline] + pub fn blue(&self, shade: Shade) -> egui::Color32 { + self.get(ColorToken::blue(shade)) + } + + #[inline] + pub fn purple(&self, shade: Shade) -> egui::Color32 { + self.get(ColorToken::purple(shade)) + } +} + +/// A token representing a color in the [`ColorTable`]. +#[derive(Debug, Clone, Copy, Hash)] +pub struct ColorToken { + hue: Hue, + shade: Shade, +} + +impl ColorToken { + #[inline] + pub fn new(hue: Hue, shade: Shade) -> Self { + Self { hue, shade } + } + + #[inline] + pub fn grey(shade: Shade) -> Self { + Self::new(Hue::Grey, shade) + } + + #[inline] + pub fn green(shade: Shade) -> Self { + Self::new(Hue::Green, shade) + } + + #[inline] + pub fn red(shade: Shade) -> Self { + Self::new(Hue::Red, shade) + } + + #[inline] + pub fn blue(shade: Shade) -> Self { + Self::new(Hue::Blue, shade) + } + + #[inline] + pub fn purple(shade: Shade) -> Self { + Self::new(Hue::Purple, shade) + } +} diff --git a/crates/viewer/re_ui/src/design_tokens.rs b/crates/viewer/re_ui/src/design_tokens.rs index b2952cdd9401..d5f015440b69 100644 --- a/crates/viewer/re_ui/src/design_tokens.rs +++ b/crates/viewer/re_ui/src/design_tokens.rs @@ -1,17 +1,23 @@ -#![allow(clippy::unwrap_used)] // fixed json file +#![allow(clippy::unwrap_used)] +use crate::color_table::Shade::{S100, S1000, S150, S200, S250, S300, S325, S350, S550, S775}; +use crate::color_table::{ColorTable, ColorToken, Hue, Shade}; use crate::{design_tokens, CUSTOM_WINDOW_DECORATIONS}; /// The look and feel of the UI. /// /// Not everything is covered by this. /// A lot of other design tokens are put straight into the [`egui::Style`] -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct DesignTokens { pub json: serde_json::Value, - // TODO(ab): some colors, etc. are defined here, and some others are defined as functions. This - // should be unified, in a way that minimize the number of json->Color32 conversions - // at runtime. + + /// Color table for all colors used in the UI. + /// + /// Loaded at startup from `design_tokens.json`. + color_table: ColorTable, + + // TODO(ab): get rid of these, they should be function calls like the rest. pub top_bar_color: egui::Color32, pub bottom_bar_color: egui::Color32, pub bottom_bar_stroke: egui::Stroke, @@ -28,26 +34,23 @@ impl DesignTokens { serde_json::from_str(include_str!("../data/design_tokens.json")) .expect("Failed to parse data/design_tokens.json"); + let color_table = load_color_table(&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}"), - ), + top_bar_color: color_table.grey(S100), + bottom_bar_color: color_table.grey(S150), + bottom_bar_stroke: egui::Stroke::new(1.0, color_table.grey(S250)), bottom_bar_rounding: egui::Rounding { nw: 6.0, ne: 6.0, sw: 0.0, se: 0.0, }, // copied from figma, should be top only - 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}"), - ), + shadow_gradient_dark_start: egui::Color32::from_black_alpha(77), //TODO(ab): use ColorToken! + tab_bar_color: color_table.grey(S200), + native_frame_stroke: egui::Stroke::new(1.0, color_table.grey(S250)), json, + color_table, } } @@ -128,12 +131,12 @@ impl DesignTokens { .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 panel_bg_color = self.color(ColorToken::grey(S100)); // let floating_color = get_aliased_color(&json, "{Alias.Color.Surface.Floating.value}"); - let floating_color = get_global_color(&self.json, "{Global.Color.Grey.250}"); + let floating_color = self.color(ColorToken::grey(S250)); // For table zebra stripes. - egui_style.visuals.faint_bg_color = get_global_color(&self.json, "{Global.Color.Grey.150}"); + egui_style.visuals.faint_bg_color = self.color(ColorToken::grey(S150)); // Used as the background of text edits, scroll bars and others things // that needs to look different from other interactive stuff. @@ -147,12 +150,11 @@ impl DesignTokens { egui_style.visuals.widgets.inactive.weak_bg_fill = Default::default(); // Buttons have no background color when inactive // Fill of unchecked radio buttons, checkboxes, etc. Must be brighter than the background floating_color. - egui_style.visuals.widgets.inactive.bg_fill = - get_global_color(&self.json, "{Global.Color.Grey.300}"); + egui_style.visuals.widgets.inactive.bg_fill = self.color(ColorToken::grey(S300)); { // Background colors for buttons (menu buttons, blueprint buttons, etc) when hovered or clicked: - let hovered_color = get_global_color(&self.json, "{Global.Color.Grey.325}"); + let hovered_color = self.color(ColorToken::grey(S325)); 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; @@ -175,17 +177,18 @@ impl DesignTokens { 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.bg_fill = self.color(ColorToken::blue(S350)); + + //TODO(ab): use ColorToken! egui_style.visuals.selection.stroke.color = egui::Color32::from_rgb(173, 184, 255); // Brighter version of the above // separator lines, panel lines, etc egui_style.visuals.widgets.noninteractive.bg_stroke.color = - get_global_color(&self.json, "{Global.Color.Grey.250}"); + self.color(ColorToken::grey(S250)); - 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}"); + let subdued = self.color(ColorToken::grey(S550)); + let default = self.color(ColorToken::grey(S775)); + let strong = self.color(ColorToken::grey(S1000)); egui_style.visuals.widgets.noninteractive.fg_stroke.color = subdued; // non-interactive text egui_style.visuals.widgets.inactive.fg_stroke.color = default; // button text @@ -244,12 +247,18 @@ impl DesignTokens { egui_style.visuals.image_loading_spinners = false; + //TODO(ab): use ColorToken! 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 color(&self, token: ColorToken) -> egui::Color32 { + self.color_table.get(token) + } + #[inline] pub fn welcome_screen_h1() -> egui::TextStyle { egui::TextStyle::Name("welcome-screen-h1".into()) @@ -403,7 +412,7 @@ impl DesignTokens { /// The color for the background of [`crate::SectionCollapsingHeader`]. pub fn section_collapsing_header_color(&self) -> egui::Color32 { // same as visuals.widgets.inactive.bg_fill - get_global_color(&self.json, "{Global.Color.Grey.200}") + self.color(ColorToken::grey(S200)) } /// The color we use to mean "loop this selection" @@ -418,23 +427,36 @@ impl DesignTokens { /// Used by the "add view or container" modal. pub fn thumbnail_background_color(&self) -> egui::Color32 { - get_global_color(&self.json, "{Global.Color.Grey.250}") + self.color(ColorToken::grey(S250)) } } // ---------------------------------------------------------------------------- -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() +/// Build the [`ColorTable`] based on the content of `design_token.json` +fn load_color_table(json: &serde_json::Value) -> ColorTable { + fn get_color_from_json(json: &serde_json::Value, global_path: &str) -> egui::Color32 { + parse_color(global_path_value(json, global_path).as_str().unwrap()) + } + + ColorTable { + color_table: Hue::all() + .iter() + .map(|hue| { + let shade_map = Shade::all() + .iter() + .map(|shade| { + let color = get_color_from_json( + json, + &format!("{{Global.Color.{hue}.{}}}", shade.as_u16()), + ); + (*shade, color) + }) + .collect(); + (*hue, shade_map) + }) + .collect(), + } } fn get_alias(json: &serde_json::Value, alias_path: &str) -> T { diff --git a/crates/viewer/re_ui/src/lib.rs b/crates/viewer/re_ui/src/lib.rs index fd1d14c9ec93..7339b738f5d0 100644 --- a/crates/viewer/re_ui/src/lib.rs +++ b/crates/viewer/re_ui/src/lib.rs @@ -5,6 +5,7 @@ mod command_palette; mod design_tokens; mod syntax_highlighting; +mod color_table; mod context_ext; pub mod drag_and_drop; pub mod icons; From c207aa50226499468e73be33ad56ed328b4ead39 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Thu, 5 Dec 2024 12:15:33 +0100 Subject: [PATCH 2/8] Suppress the color aliases section in `design_token.json` --- crates/viewer/re_ui/data/design_tokens.json | 96 --------------------- 1 file changed, 96 deletions(-) diff --git a/crates/viewer/re_ui/data/design_tokens.json b/crates/viewer/re_ui/data/design_tokens.json index 9c8974d9a4c5..7f98e1bfbead 100644 --- a/crates/viewer/re_ui/data/design_tokens.json +++ b/crates/viewer/re_ui/data/design_tokens.json @@ -1,101 +1,5 @@ { "Alias": { - "Color": { - "Surface": { - "Default": { - "description": "Background color for most UI surfaces in Rerun", - "value": "{Global.Color.Grey.100}", - "type": "color" - }, - "Floating": { - "description": "Background color for floating elements like menus, dropdown options, notifications etc.", - "value": "{Global.Color.Grey.175}", - "type": "color" - } - }, - "Action": { - "Default": { - "description": "Background color for UI elements like buttons and selects", - "value": "{Global.Color.Grey.200}", - "type": "color" - }, - "Hovered": { - "description": "Background color for hovered UI elements", - "value": "{Global.Color.Grey.225}", - "type": "color" - }, - "Active": { - "description": "Background color for pressed UI elements", - "value": "{Global.Color.Grey.250}", - "type": "color" - }, - "Pressed": { - "description": "Background color for suppressed UI elements, like a select that is currently showing a menu", - "value": "{Global.Color.Grey.250}", - "type": "color" - } - }, - "NotificationBadge": { - "Unread": { - "description": "Used for unread notification indicators", - "value": "{Global.Color.Blue.500}", - "type": "color" - }, - "Read": { - "description": "Used for read notification indicators", - "value": "{Global.Color.Grey.250}", - "type": "color" - } - }, - "Text": { - "Default": { - "description": "Default text color", - "value": "{Global.Color.Grey.775}", - "type": "color" - }, - "Subdued": { - "description": "Used for less important text", - "value": "{Global.Color.Grey.550}", - "type": "color" - }, - "Strong": { - "description": "Used for highlighted or emphasized items, such as current navigation items", - "value": "{Global.Color.Grey.1000}", - "type": "color" - } - }, - "Border": { - "Default": { - "value": "{Global.Color.OpaqueGrey.Default}", - "description": "Default color for borders", - "type": "color" - } - }, - "Icon": { - "Default": { - "description": "Default icon color", - "value": "{Global.Color.Grey.775}", - "type": "color" - }, - "Subdued": { - "description": "Used together with subdued text", - "value": "{Global.Color.Grey.550}", - "type": "color" - }, - "Strong": { - "description": "Used together width strong text", - "value": "{Global.Color.Grey.1000}", - "type": "color" - } - }, - "Highlight": { - "Default": { - "value": "{Global.Color.Blue.350}", - "description": "Default color for highlighted items, like hovered menu items", - "type": "color" - } - } - }, "Typography": { "Default": { "value": "{Global.Typography.200}", From 9045a6a7a1e51bba00c2d5ea353290f365507b48 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Thu, 5 Dec 2024 12:22:01 +0100 Subject: [PATCH 3/8] Comments & cleanup --- crates/viewer/re_ui/src/color_table.rs | 2 ++ crates/viewer/re_ui/src/design_tokens.rs | 1 + crates/viewer/re_ui/src/lib.rs | 1 + 3 files changed, 4 insertions(+) diff --git a/crates/viewer/re_ui/src/color_table.rs b/crates/viewer/re_ui/src/color_table.rs index 43004b532124..ea10297200b5 100644 --- a/crates/viewer/re_ui/src/color_table.rs +++ b/crates/viewer/re_ui/src/color_table.rs @@ -176,6 +176,8 @@ impl ColorTable { } /// A token representing a color in the [`ColorTable`]. +/// +/// Use [`crate::DesignTokens::color`] to get the color corresponding to a token. #[derive(Debug, Clone, Copy, Hash)] pub struct ColorToken { hue: Hue, diff --git a/crates/viewer/re_ui/src/design_tokens.rs b/crates/viewer/re_ui/src/design_tokens.rs index d5f015440b69..99b27d1d36ed 100644 --- a/crates/viewer/re_ui/src/design_tokens.rs +++ b/crates/viewer/re_ui/src/design_tokens.rs @@ -254,6 +254,7 @@ impl DesignTokens { ctx.set_style(egui_style); } + /// Get the [`egui::Color32`] corresponding to the provided [`ColorToken`]. #[inline] pub fn color(&self, token: ColorToken) -> egui::Color32 { self.color_table.get(token) diff --git a/crates/viewer/re_ui/src/lib.rs b/crates/viewer/re_ui/src/lib.rs index 7339b738f5d0..2f4665495837 100644 --- a/crates/viewer/re_ui/src/lib.rs +++ b/crates/viewer/re_ui/src/lib.rs @@ -20,6 +20,7 @@ pub mod zoom_pan_area; use egui::NumExt as _; pub use self::{ + color_table::{ColorToken, Hue, Shade}, command::{UICommand, UICommandSender}, command_palette::CommandPalette, context_ext::ContextExt, From 7ab64d440a6ae6c978134366f9cdedbd5401e695 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Thu, 5 Dec 2024 12:33:55 +0100 Subject: [PATCH 4/8] okaaay, lets go for US english --- crates/viewer/re_ui/data/design_tokens.json | 4 +-- crates/viewer/re_ui/src/color_table.rs | 14 ++++----- crates/viewer/re_ui/src/design_tokens.rs | 32 ++++++++++----------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/viewer/re_ui/data/design_tokens.json b/crates/viewer/re_ui/data/design_tokens.json index 7f98e1bfbead..5c45789ec7ee 100644 --- a/crates/viewer/re_ui/data/design_tokens.json +++ b/crates/viewer/re_ui/data/design_tokens.json @@ -27,7 +27,7 @@ }, "Global": { "Color": { - "Grey": { + "Gray": { "0": { "value": "#000000", "description": "0 - 0", @@ -1062,7 +1062,7 @@ "type": "color" } }, - "OpaqueGrey": { + "OpaqueGray": { "Default": { "value": "#7c7c7c20", "description": "An opaque grey that picks up some, but not all, of the colors behind it", diff --git a/crates/viewer/re_ui/src/color_table.rs b/crates/viewer/re_ui/src/color_table.rs index ea10297200b5..c0e0d1159df5 100644 --- a/crates/viewer/re_ui/src/color_table.rs +++ b/crates/viewer/re_ui/src/color_table.rs @@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter}; /// A hue for a [`ColorToken`]. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum Hue { - Grey, + Gray, Green, Red, Blue, @@ -14,7 +14,7 @@ impl nohash_hasher::IsEnabled for Hue {} impl Hue { pub fn all() -> &'static [Self] { - static ALL: [Hue; 5] = [Hue::Grey, Hue::Green, Hue::Red, Hue::Blue, Hue::Purple]; + static ALL: [Hue; 5] = [Hue::Gray, Hue::Green, Hue::Red, Hue::Blue, Hue::Purple]; &ALL } } @@ -23,7 +23,7 @@ impl Display for Hue { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { // these must be as they appear in `design_token.json` - Self::Grey => f.write_str("Grey"), + Self::Gray => f.write_str("Gray"), Self::Green => f.write_str("Green"), Self::Red => f.write_str("Red"), Self::Blue => f.write_str("Blue"), @@ -150,8 +150,8 @@ impl ColorTable { } #[inline] - pub fn grey(&self, shade: Shade) -> egui::Color32 { - self.get(ColorToken::grey(shade)) + pub fn gray(&self, shade: Shade) -> egui::Color32 { + self.get(ColorToken::gray(shade)) } #[inline] @@ -191,8 +191,8 @@ impl ColorToken { } #[inline] - pub fn grey(shade: Shade) -> Self { - Self::new(Hue::Grey, shade) + pub fn gray(shade: Shade) -> Self { + Self::new(Hue::Gray, shade) } #[inline] diff --git a/crates/viewer/re_ui/src/design_tokens.rs b/crates/viewer/re_ui/src/design_tokens.rs index 99b27d1d36ed..2ab64ad0f2fb 100644 --- a/crates/viewer/re_ui/src/design_tokens.rs +++ b/crates/viewer/re_ui/src/design_tokens.rs @@ -37,9 +37,9 @@ impl DesignTokens { let color_table = load_color_table(&json); Self { - top_bar_color: color_table.grey(S100), - bottom_bar_color: color_table.grey(S150), - bottom_bar_stroke: egui::Stroke::new(1.0, color_table.grey(S250)), + top_bar_color: color_table.gray(S100), + bottom_bar_color: color_table.gray(S150), + bottom_bar_stroke: egui::Stroke::new(1.0, color_table.gray(S250)), bottom_bar_rounding: egui::Rounding { nw: 6.0, ne: 6.0, @@ -47,8 +47,8 @@ impl DesignTokens { se: 0.0, }, // copied from figma, should be top only shadow_gradient_dark_start: egui::Color32::from_black_alpha(77), //TODO(ab): use ColorToken! - tab_bar_color: color_table.grey(S200), - native_frame_stroke: egui::Stroke::new(1.0, color_table.grey(S250)), + tab_bar_color: color_table.gray(S200), + native_frame_stroke: egui::Stroke::new(1.0, color_table.gray(S250)), json, color_table, } @@ -131,12 +131,12 @@ impl DesignTokens { .text_styles .insert(Self::welcome_screen_tag(), egui::FontId::proportional(10.5)); - let panel_bg_color = self.color(ColorToken::grey(S100)); + let panel_bg_color = self.color(ColorToken::gray(S100)); // let floating_color = get_aliased_color(&json, "{Alias.Color.Surface.Floating.value}"); - let floating_color = self.color(ColorToken::grey(S250)); + let floating_color = self.color(ColorToken::gray(S250)); // For table zebra stripes. - egui_style.visuals.faint_bg_color = self.color(ColorToken::grey(S150)); + egui_style.visuals.faint_bg_color = self.color(ColorToken::gray(S150)); // Used as the background of text edits, scroll bars and others things // that needs to look different from other interactive stuff. @@ -150,11 +150,11 @@ impl DesignTokens { egui_style.visuals.widgets.inactive.weak_bg_fill = Default::default(); // Buttons have no background color when inactive // Fill of unchecked radio buttons, checkboxes, etc. Must be brighter than the background floating_color. - egui_style.visuals.widgets.inactive.bg_fill = self.color(ColorToken::grey(S300)); + egui_style.visuals.widgets.inactive.bg_fill = self.color(ColorToken::gray(S300)); { // Background colors for buttons (menu buttons, blueprint buttons, etc) when hovered or clicked: - let hovered_color = self.color(ColorToken::grey(S325)); + let hovered_color = self.color(ColorToken::gray(S325)); 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; @@ -184,11 +184,11 @@ impl DesignTokens { // separator lines, panel lines, etc egui_style.visuals.widgets.noninteractive.bg_stroke.color = - self.color(ColorToken::grey(S250)); + self.color(ColorToken::gray(S250)); - let subdued = self.color(ColorToken::grey(S550)); - let default = self.color(ColorToken::grey(S775)); - let strong = self.color(ColorToken::grey(S1000)); + let subdued = self.color(ColorToken::gray(S550)); + let default = self.color(ColorToken::gray(S775)); + let strong = self.color(ColorToken::gray(S1000)); egui_style.visuals.widgets.noninteractive.fg_stroke.color = subdued; // non-interactive text egui_style.visuals.widgets.inactive.fg_stroke.color = default; // button text @@ -413,7 +413,7 @@ impl DesignTokens { /// The color for the background of [`crate::SectionCollapsingHeader`]. pub fn section_collapsing_header_color(&self) -> egui::Color32 { // same as visuals.widgets.inactive.bg_fill - self.color(ColorToken::grey(S200)) + self.color(ColorToken::gray(S200)) } /// The color we use to mean "loop this selection" @@ -428,7 +428,7 @@ impl DesignTokens { /// Used by the "add view or container" modal. pub fn thumbnail_background_color(&self) -> egui::Color32 { - self.color(ColorToken::grey(S250)) + self.color(ColorToken::gray(S250)) } } From 854c8c16a06184d592948221d3013d639c3fcf49 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Thu, 5 Dec 2024 13:24:30 +0100 Subject: [PATCH 5/8] fix doclink --- crates/viewer/re_ui/src/color_table.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/viewer/re_ui/src/color_table.rs b/crates/viewer/re_ui/src/color_table.rs index c0e0d1159df5..d27acaf02bbc 100644 --- a/crates/viewer/re_ui/src/color_table.rs +++ b/crates/viewer/re_ui/src/color_table.rs @@ -175,7 +175,7 @@ impl ColorTable { } } -/// A token representing a color in the [`ColorTable`]. +/// A token representing a color in the global color table. /// /// Use [`crate::DesignTokens::color`] to get the color corresponding to a token. #[derive(Debug, Clone, Copy, Hash)] From 32e50c489165e3b918e83213b1af0bcc41a130c5 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Fri, 6 Dec 2024 15:50:29 +0100 Subject: [PATCH 6/8] review comments --- Cargo.lock | 1 - crates/viewer/re_ui/Cargo.toml | 9 +- crates/viewer/re_ui/src/color_table.rs | 256 ++++++++++++----------- crates/viewer/re_ui/src/design_tokens.rs | 28 +-- crates/viewer/re_ui/src/lib.rs | 2 +- 5 files changed, 141 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4194fb6a11b7..b071cb0aacb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6585,7 +6585,6 @@ dependencies = [ "egui_extras", "egui_kittest", "egui_tiles", - "nohash-hasher", "once_cell", "parking_lot", "rand", diff --git a/crates/viewer/re_ui/Cargo.toml b/crates/viewer/re_ui/Cargo.toml index f20917ec05ca..e7c2c7b87d4a 100644 --- a/crates/viewer/re_ui/Cargo.toml +++ b/crates/viewer/re_ui/Cargo.toml @@ -36,11 +36,14 @@ re_log.workspace = true re_log_types.workspace = true # syntax-highlighting for EntityPath re_tracing.workspace = true +eframe = { workspace = true, default-features = false, features = ["wgpu"] } egui.workspace = true egui_commonmark = { workspace = true, features = ["pulldown_cmark"] } egui_extras.workspace = true +egui_tiles.workspace = true once_cell.workspace = true parking_lot.workspace = true +rand.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true strum.workspace = true @@ -48,11 +51,5 @@ strum_macros.workspace = true sublime_fuzzy.workspace = true -eframe = { workspace = true, default-features = false, features = ["wgpu"] } -egui_tiles.workspace = true -nohash-hasher.workspace = true -rand.workspace = true - - [dev-dependencies] egui_kittest.workspace = true diff --git a/crates/viewer/re_ui/src/color_table.rs b/crates/viewer/re_ui/src/color_table.rs index d27acaf02bbc..2f8db5a44148 100644 --- a/crates/viewer/re_ui/src/color_table.rs +++ b/crates/viewer/re_ui/src/color_table.rs @@ -1,7 +1,9 @@ use std::fmt::{Display, Formatter}; +use strum::{EnumCount, EnumIter, IntoEnumIterator}; + /// A hue for a [`ColorToken`]. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, EnumIter, EnumCount)] pub enum Hue { Gray, Green, @@ -10,15 +12,6 @@ pub enum Hue { Purple, } -impl nohash_hasher::IsEnabled for Hue {} - -impl Hue { - pub fn all() -> &'static [Self] { - static ALL: [Hue; 5] = [Hue::Gray, Hue::Green, Hue::Red, Hue::Blue, Hue::Purple]; - &ALL - } -} - impl Display for Hue { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -32,145 +25,154 @@ impl Display for Hue { } } -/// A shade for a [`ColorToken`]. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub enum Shade { - S0 = 0, - S25 = 25, - S50 = 50, - S75 = 75, - S100 = 100, - S125 = 125, - S150 = 150, - S175 = 175, - S200 = 200, - S225 = 225, - S250 = 250, - S275 = 275, - S300 = 300, - S325 = 325, - S350 = 350, - S375 = 375, - S400 = 400, - S425 = 425, - S450 = 450, - S475 = 475, - S500 = 500, - S525 = 525, - S550 = 550, - S575 = 575, - S600 = 600, - S625 = 625, - S650 = 650, - S675 = 675, - S700 = 700, - S725 = 725, - S750 = 750, - S775 = 775, - S800 = 800, - S825 = 825, - S850 = 850, - S875 = 875, - S900 = 900, - S925 = 925, - S950 = 950, - S975 = 975, - S1000 = 1000, +/// A color scale for a [`ColorToken`]. +/// +/// A scale is an arbitrary… well… scale of subjective color "intensity". Both brightness and +/// saturation may vary along the scale. For a dark mode theme, low scales are typically darker and +/// used for backgrounds, whereas high scales are typically brighter and used for text and +/// interactive UI elements. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, EnumIter, EnumCount)] +pub enum Scale { + S0, + S25, + S50, + S75, + S100, + S125, + S150, + S175, + S200, + S225, + S250, + S275, + S300, + S325, + S350, + S375, + S400, + S425, + S450, + S475, + S500, + S525, + S550, + S575, + S600, + S625, + S650, + S675, + S700, + S725, + S750, + S775, + S800, + S825, + S850, + S875, + S900, + S925, + S950, + S975, + S1000, } -impl nohash_hasher::IsEnabled for Shade {} - -impl Shade { - pub fn all() -> &'static [Self] { - static ALL: [Shade; 41] = [ - Shade::S0, - Shade::S25, - Shade::S50, - Shade::S75, - Shade::S100, - Shade::S125, - Shade::S150, - Shade::S175, - Shade::S200, - Shade::S225, - Shade::S250, - Shade::S275, - Shade::S300, - Shade::S325, - Shade::S350, - Shade::S375, - Shade::S400, - Shade::S425, - Shade::S450, - Shade::S475, - Shade::S500, - Shade::S525, - Shade::S550, - Shade::S575, - Shade::S600, - Shade::S625, - Shade::S650, - Shade::S675, - Shade::S700, - Shade::S725, - Shade::S750, - Shade::S775, - Shade::S800, - Shade::S825, - Shade::S850, - Shade::S875, - Shade::S900, - Shade::S925, - Shade::S950, - Shade::S975, - Shade::S1000, - ]; - &ALL - } - - pub fn as_u16(self) -> u16 { - self as u16 +impl Display for Scale { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let txt = match self { + Self::S0 => "0", + Self::S25 => "25", + Self::S50 => "50", + Self::S75 => "75", + Self::S100 => "100", + Self::S125 => "125", + Self::S150 => "150", + Self::S175 => "175", + Self::S200 => "200", + Self::S225 => "225", + Self::S250 => "250", + Self::S275 => "275", + Self::S300 => "300", + Self::S325 => "325", + Self::S350 => "350", + Self::S375 => "375", + Self::S400 => "400", + Self::S425 => "425", + Self::S450 => "450", + Self::S475 => "475", + Self::S500 => "500", + Self::S525 => "525", + Self::S550 => "550", + Self::S575 => "575", + Self::S600 => "600", + Self::S625 => "625", + Self::S650 => "650", + Self::S675 => "675", + Self::S700 => "700", + Self::S725 => "725", + Self::S750 => "750", + Self::S775 => "775", + Self::S800 => "800", + Self::S825 => "825", + Self::S850 => "850", + Self::S875 => "875", + Self::S900 => "900", + Self::S925 => "925", + Self::S950 => "950", + Self::S975 => "975", + Self::S1000 => "1000", + }; + + txt.fmt(f) } } -/// A table mapping all combination of [`Hue`] and [`Shade`] to a [`egui::Color32`]. +/// A table mapping all combination of [`Hue`] and [`Scale`] to a [`egui::Color32`]. #[derive(Debug)] pub struct ColorTable { - pub color_table: nohash_hasher::IntMap>, + color_lut: Vec>, } impl ColorTable { + /// Build a new color table by calling the provided closure with all possible entries. + pub fn new(mut color_lut_fn: impl FnMut(ColorToken) -> egui::Color32) -> Self { + Self { + color_lut: Hue::iter() + .map(|hue| { + Scale::iter() + .map(|scale| color_lut_fn(ColorToken::new(hue, scale))) + .collect() + }) + .collect(), + } + } + #[inline] pub fn get(&self, token: ColorToken) -> egui::Color32 { - *self - .color_table - .get(&token.hue) - .and_then(|m| m.get(&token.shade)) - .expect("The color table must always be complete.") + self.color_lut[token.hue as usize][token.scale as usize] } #[inline] - pub fn gray(&self, shade: Shade) -> egui::Color32 { + pub fn gray(&self, shade: Scale) -> egui::Color32 { self.get(ColorToken::gray(shade)) } #[inline] - pub fn green(&self, shade: Shade) -> egui::Color32 { + pub fn green(&self, shade: Scale) -> egui::Color32 { self.get(ColorToken::green(shade)) } #[inline] - pub fn red(&self, shade: Shade) -> egui::Color32 { + pub fn red(&self, shade: Scale) -> egui::Color32 { self.get(ColorToken::red(shade)) } #[inline] - pub fn blue(&self, shade: Shade) -> egui::Color32 { + pub fn blue(&self, shade: Scale) -> egui::Color32 { self.get(ColorToken::blue(shade)) } #[inline] - pub fn purple(&self, shade: Shade) -> egui::Color32 { + pub fn purple(&self, shade: Scale) -> egui::Color32 { self.get(ColorToken::purple(shade)) } } @@ -180,38 +182,38 @@ impl ColorTable { /// Use [`crate::DesignTokens::color`] to get the color corresponding to a token. #[derive(Debug, Clone, Copy, Hash)] pub struct ColorToken { - hue: Hue, - shade: Shade, + pub hue: Hue, + pub scale: Scale, } impl ColorToken { #[inline] - pub fn new(hue: Hue, shade: Shade) -> Self { - Self { hue, shade } + pub fn new(hue: Hue, shade: Scale) -> Self { + Self { hue, scale: shade } } #[inline] - pub fn gray(shade: Shade) -> Self { + pub fn gray(shade: Scale) -> Self { Self::new(Hue::Gray, shade) } #[inline] - pub fn green(shade: Shade) -> Self { + pub fn green(shade: Scale) -> Self { Self::new(Hue::Green, shade) } #[inline] - pub fn red(shade: Shade) -> Self { + pub fn red(shade: Scale) -> Self { Self::new(Hue::Red, shade) } #[inline] - pub fn blue(shade: Shade) -> Self { + pub fn blue(shade: Scale) -> Self { Self::new(Hue::Blue, shade) } #[inline] - pub fn purple(shade: Shade) -> Self { + pub fn purple(shade: Scale) -> Self { Self::new(Hue::Purple, shade) } } diff --git a/crates/viewer/re_ui/src/design_tokens.rs b/crates/viewer/re_ui/src/design_tokens.rs index 2ab64ad0f2fb..de05e49c0e87 100644 --- a/crates/viewer/re_ui/src/design_tokens.rs +++ b/crates/viewer/re_ui/src/design_tokens.rs @@ -1,7 +1,7 @@ #![allow(clippy::unwrap_used)] -use crate::color_table::Shade::{S100, S1000, S150, S200, S250, S300, S325, S350, S550, S775}; -use crate::color_table::{ColorTable, ColorToken, Hue, Shade}; +use crate::color_table::Scale::{S100, S1000, S150, S200, S250, S300, S325, S350, S550, S775}; +use crate::color_table::{ColorTable, ColorToken}; use crate::{design_tokens, CUSTOM_WINDOW_DECORATIONS}; /// The look and feel of the UI. @@ -440,24 +440,12 @@ fn load_color_table(json: &serde_json::Value) -> ColorTable { parse_color(global_path_value(json, global_path).as_str().unwrap()) } - ColorTable { - color_table: Hue::all() - .iter() - .map(|hue| { - let shade_map = Shade::all() - .iter() - .map(|shade| { - let color = get_color_from_json( - json, - &format!("{{Global.Color.{hue}.{}}}", shade.as_u16()), - ); - (*shade, color) - }) - .collect(); - (*hue, shade_map) - }) - .collect(), - } + ColorTable::new(|color_token| { + get_color_from_json( + json, + &format!("{{Global.Color.{}.{}}}", color_token.hue, color_token.scale), + ) + }) } fn get_alias(json: &serde_json::Value, alias_path: &str) -> T { diff --git a/crates/viewer/re_ui/src/lib.rs b/crates/viewer/re_ui/src/lib.rs index 2f4665495837..50f5063f29a0 100644 --- a/crates/viewer/re_ui/src/lib.rs +++ b/crates/viewer/re_ui/src/lib.rs @@ -20,7 +20,7 @@ pub mod zoom_pan_area; use egui::NumExt as _; pub use self::{ - color_table::{ColorToken, Hue, Shade}, + color_table::{ColorToken, Hue, Scale}, command::{UICommand, UICommandSender}, command_palette::CommandPalette, context_ext::ContextExt, From 53eb1ee0357be300a963821111f6b91b9c1f7428 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Fri, 6 Dec 2024 16:19:29 +0100 Subject: [PATCH 7/8] lint --- crates/viewer/re_ui/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/viewer/re_ui/src/lib.rs b/crates/viewer/re_ui/src/lib.rs index cf3fe0bfc6d9..91266b9dd620 100644 --- a/crates/viewer/re_ui/src/lib.rs +++ b/crates/viewer/re_ui/src/lib.rs @@ -1,10 +1,10 @@ //! Rerun GUI theme and helpers, built around [`egui`](https://www.egui.rs/). +mod color_table; mod command; mod command_palette; -mod design_tokens; -mod color_table; mod context_ext; +mod design_tokens; pub mod drag_and_drop; pub mod icons; pub mod list_item; From a699c83eed2958bfbec14691b1043e9ed006b90e Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Fri, 6 Dec 2024 16:21:06 +0100 Subject: [PATCH 8/8] update failing test snapshot --- crates/viewer/re_ui/tests/snapshots/modal_list_item.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/viewer/re_ui/tests/snapshots/modal_list_item.png b/crates/viewer/re_ui/tests/snapshots/modal_list_item.png index 379ff8545a13..4f33fb2a6cc9 100644 --- a/crates/viewer/re_ui/tests/snapshots/modal_list_item.png +++ b/crates/viewer/re_ui/tests/snapshots/modal_list_item.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18d3acf305b985859b65e1bc2bcdc1b29625aeb28a06dd1d5ff75a5819832ec3 -size 19694 +oid sha256:ad33e16de7e4029c5857459a5e53a407726c6c19e3c36a786b7b2bcbf881eebd +size 19507