diff --git a/crates/viewer/re_ui/Cargo.toml b/crates/viewer/re_ui/Cargo.toml index 256b9ef2ce4d..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,10 +51,5 @@ strum_macros.workspace = true sublime_fuzzy.workspace = true -eframe = { workspace = true, default-features = false, features = ["wgpu"] } -egui_tiles.workspace = true -rand.workspace = true - - [dev-dependencies] egui_kittest.workspace = true diff --git a/crates/viewer/re_ui/data/design_tokens.json b/crates/viewer/re_ui/data/design_tokens.json index 9c8974d9a4c5..5c45789ec7ee 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}", @@ -123,7 +27,7 @@ }, "Global": { "Color": { - "Grey": { + "Gray": { "0": { "value": "#000000", "description": "0 - 0", @@ -1158,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 new file mode 100644 index 000000000000..2f8db5a44148 --- /dev/null +++ b/crates/viewer/re_ui/src/color_table.rs @@ -0,0 +1,219 @@ +use std::fmt::{Display, Formatter}; + +use strum::{EnumCount, EnumIter, IntoEnumIterator}; + +/// A hue for a [`ColorToken`]. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, EnumIter, EnumCount)] +pub enum Hue { + Gray, + Green, + Red, + Blue, + Purple, +} + +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::Gray => f.write_str("Gray"), + 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 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 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 [`Scale`] to a [`egui::Color32`]. +#[derive(Debug)] +pub struct ColorTable { + 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_lut[token.hue as usize][token.scale as usize] + } + + #[inline] + pub fn gray(&self, shade: Scale) -> egui::Color32 { + self.get(ColorToken::gray(shade)) + } + + #[inline] + pub fn green(&self, shade: Scale) -> egui::Color32 { + self.get(ColorToken::green(shade)) + } + + #[inline] + pub fn red(&self, shade: Scale) -> egui::Color32 { + self.get(ColorToken::red(shade)) + } + + #[inline] + pub fn blue(&self, shade: Scale) -> egui::Color32 { + self.get(ColorToken::blue(shade)) + } + + #[inline] + pub fn purple(&self, shade: Scale) -> egui::Color32 { + self.get(ColorToken::purple(shade)) + } +} + +/// 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)] +pub struct ColorToken { + pub hue: Hue, + pub scale: Scale, +} + +impl ColorToken { + #[inline] + pub fn new(hue: Hue, shade: Scale) -> Self { + Self { hue, scale: shade } + } + + #[inline] + pub fn gray(shade: Scale) -> Self { + Self::new(Hue::Gray, shade) + } + + #[inline] + pub fn green(shade: Scale) -> Self { + Self::new(Hue::Green, shade) + } + + #[inline] + pub fn red(shade: Scale) -> Self { + Self::new(Hue::Red, shade) + } + + #[inline] + pub fn blue(shade: Scale) -> Self { + Self::new(Hue::Blue, shade) + } + + #[inline] + 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 b2952cdd9401..de05e49c0e87 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::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. /// /// 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.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, 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.gray(S200), + native_frame_stroke: egui::Stroke::new(1.0, color_table.gray(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::gray(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::gray(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::gray(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::gray(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::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; @@ -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::gray(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::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 @@ -244,12 +247,19 @@ 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); } + /// Get the [`egui::Color32`] corresponding to the provided [`ColorToken`]. + #[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 +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 - get_global_color(&self.json, "{Global.Color.Grey.200}") + self.color(ColorToken::gray(S200)) } /// The color we use to mean "loop this selection" @@ -418,23 +428,24 @@ 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::gray(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()) -} +/// 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()) + } -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() + 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 a6e155863e2b..91266b9dd620 100644 --- a/crates/viewer/re_ui/src/lib.rs +++ b/crates/viewer/re_ui/src/lib.rs @@ -1,5 +1,6 @@ //! Rerun GUI theme and helpers, built around [`egui`](https://www.egui.rs/). +mod color_table; mod command; mod command_palette; mod context_ext; @@ -18,6 +19,7 @@ pub mod zoom_pan_area; use egui::NumExt as _; pub use self::{ + color_table::{ColorToken, Hue, Scale}, command::{UICommand, UICommandSender}, command_palette::CommandPalette, context_ext::ContextExt, 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