From b1ae9efae2915eed7acad157c05c6c86a61a3281 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Mon, 13 Nov 2023 16:28:20 -0800 Subject: [PATCH] ColorScheme[Builder] --- examples/theme.rs | 45 +++++----- src/animation.rs | 16 +++- src/styles.rs | 217 +++++++++++++++++++++++++++++++++++++--------- 3 files changed, 214 insertions(+), 64 deletions(-) diff --git a/examples/theme.rs b/examples/theme.rs index 599bedb..65b8090 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -2,7 +2,9 @@ use std::str::FromStr; use gooey::animation::ZeroToOne; use gooey::styles::components::{TextColor, WidgetBackground}; -use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair}; +use gooey::styles::{ + ColorScheme, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair, +}; use gooey::value::{Dynamic, MapEach}; use gooey::widget::MakeWidget; use gooey::widgets::{Input, Label, ModeSwitch, Scroll, Slider, Stack, Themed}; @@ -10,19 +12,15 @@ use gooey::window::ThemeMode; use gooey::Run; use kludgine::Color; -const PRIMARY_HUE: f32 = 240.; -const SECONDARY_HUE: f32 = 0.; -const TERTIARY_HUE: f32 = 330.; -const ERROR_HUE: f32 = 30.; - fn main() -> gooey::Result { - let (primary, primary_editor) = color_editor(PRIMARY_HUE, 0.8, "Primary"); - let (secondary, secondary_editor) = color_editor(SECONDARY_HUE, 0.3, "Secondary"); - let (tertiary, tertiary_editor) = color_editor(TERTIARY_HUE, 0.3, "Tertiary"); - let (error, error_editor) = color_editor(ERROR_HUE, 0.8, "Error"); - let (neutral, neutral_editor) = color_editor(PRIMARY_HUE, 0.001, "Neutral"); + let scheme = ColorScheme::default(); + let (primary, primary_editor) = color_editor(scheme.primary, "Primary"); + let (secondary, secondary_editor) = color_editor(scheme.secondary, "Secondary"); + let (tertiary, tertiary_editor) = color_editor(scheme.tertiary, "Tertiary"); + let (error, error_editor) = color_editor(scheme.error, "Error"); + let (neutral, neutral_editor) = color_editor(scheme.neutral, "Neutral"); let (neutral_variant, neutral_variant_editor) = - color_editor(PRIMARY_HUE, 0.001, "Neutral Variant"); + color_editor(scheme.neutral_variant, "Neutral Variant"); let (theme_mode, theme_switcher) = dark_mode_slider(); let default_theme = ( @@ -35,14 +33,14 @@ fn main() -> gooey::Result { ) .map_each( |(primary, secondary, tertiary, error, neutral, neutral_variant)| { - ThemePair::from_sources( - *primary, - *secondary, - *tertiary, - *error, - *neutral, - *neutral_variant, - ) + ThemePair::from(ColorScheme { + primary: *primary, + secondary: *secondary, + tertiary: *tertiary, + error: *error, + neutral: *neutral, + neutral_variant: *neutral_variant, + }) }, ); @@ -104,12 +102,11 @@ where } fn color_editor( - initial_hue: f32, - initial_saturation: impl Into, + initial_color: ColorSource, label: &str, ) -> (Dynamic, impl MakeWidget) { - let (hue, hue_text) = create_paired_string(initial_hue); - let (saturation, saturation_text) = create_paired_string(initial_saturation.into()); + let (hue, hue_text) = create_paired_string(initial_color.hue.into_degrees()); + let (saturation, saturation_text) = create_paired_string(initial_color.saturation); let color = (&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation)); diff --git a/src/animation.rs b/src/animation.rs index beb529c..f159181 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -853,6 +853,12 @@ impl ZeroToOne { pub fn into_f32(self) -> f32 { self.0 } + + /// Returns the result of 1.0 - `self`. + #[must_use] + pub fn one_minus(self) -> Self { + Self(1. - self.0) + } } impl Display for ZeroToOne { @@ -952,7 +958,15 @@ impl Div for ZeroToOne { type Output = Self; fn div(self, rhs: Self) -> Self::Output { - Self(self.0 / rhs.0) + self / rhs.0 + } +} + +impl Div for ZeroToOne { + type Output = Self; + + fn div(self, rhs: f32) -> Self::Output { + Self(self.0 / rhs) } } diff --git a/src/styles.rs b/src/styles.rs index 3e66f32..650e9d6 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -915,54 +915,42 @@ pub struct ThemePair { impl ThemePair { /// Returns a new theme generated from the provided color sources. #[must_use] - pub fn from_sources( - primary: ColorSource, - secondary: ColorSource, - tertiary: ColorSource, - error: ColorSource, - neutral: ColorSource, - neutral_variant: ColorSource, - ) -> Self { + pub fn from_scheme(scheme: &ColorScheme) -> Self { Self { light: Theme::light_from_sources( - primary, - secondary, - tertiary, - error, - neutral, - neutral_variant, + scheme.primary, + scheme.secondary, + scheme.tertiary, + scheme.error, + scheme.neutral, + scheme.neutral_variant, ), dark: Theme::dark_from_sources( - primary, - secondary, - tertiary, - error, - neutral, - neutral_variant, + scheme.primary, + scheme.secondary, + scheme.tertiary, + scheme.error, + scheme.neutral, + scheme.neutral_variant, ), - primary_fixed: FixedTheme::from_source(primary), - secondary_fixed: FixedTheme::from_source(secondary), - tertiary_fixed: FixedTheme::from_source(tertiary), - scrim: neutral.color(1), - shadow: neutral.color(1), + primary_fixed: FixedTheme::from_source(scheme.primary), + secondary_fixed: FixedTheme::from_source(scheme.secondary), + tertiary_fixed: FixedTheme::from_source(scheme.tertiary), + scrim: scheme.neutral.color(1), + shadow: scheme.neutral.color(1), } } } +impl From for ThemePair { + fn from(scheme: ColorScheme) -> Self { + Self::from_scheme(&scheme) + } +} + impl Default for ThemePair { fn default() -> Self { - const PRIMARY_HUE: f32 = -120.; - const SECONDARY_HUE: f32 = 0.; - const TERTIARY_HUE: f32 = -30.; - const ERROR_HUE: f32 = 30.; - Self::from_sources( - ColorSource::new(PRIMARY_HUE, 0.8), - ColorSource::new(SECONDARY_HUE, 0.3), - ColorSource::new(TERTIARY_HUE, 0.3), - ColorSource::new(ERROR_HUE, 0.8), - ColorSource::new(0., 0.001), - ColorSource::new(30., 0.), - ) + Self::from(ColorScheme::default()) } } @@ -1177,7 +1165,7 @@ impl FixedTheme { /// /// The goal of this type is to allow various tones of a given hue/saturation to /// be generated easily. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct ColorSource { /// A measurement of hue, in degees, from -180 to 180. /// @@ -1238,7 +1226,7 @@ impl ColorSource { } / 180., ); - saturation_delta * hue_delta + saturation_delta.one_minus() * hue_delta } } @@ -1574,3 +1562,154 @@ impl TryFrom for ContainerLevel { } } } + +/// A builder of [`ColorScheme`]s. +#[derive(Clone, Copy, Debug)] +pub struct ColorSchemeBuilder { + /// The primary color of the scheme. + pub primary: ColorSource, + /// The secondary color of the scheme. If not provided, a complimentary + /// color will be chosen. + pub secondary: Option, + /// The tertiary color of the scheme. If not provided, a complimentary + /// color will be chosen. + pub tertiary: Option, + /// The error color of the scheme. If not provided, red will be used unless + /// it contrasts poorly with any of the other colors. + pub error: Option, + /// The neutral color of the scheme. If not provided, a nearly fully + /// desaturated variation of the primary color will be used. + pub neutral: Option, + /// The neutral variant color of the scheme. If not provided, a mostly + /// desaturated variation of the primary color will be used. + pub neutral_variant: Option, + hue_shift: f32, +} + +impl ColorSchemeBuilder { + /// Returns a builder for the provided hue, in degrees. + #[must_use] + pub fn from_hue(hue: impl Into) -> Self { + Self::new(ColorSource::new(hue, 0.8)) + } + + /// Returns a builder for the provided primary color. + #[must_use] + pub fn new(primary: ColorSource) -> Self { + Self { + primary, + secondary: None, + tertiary: None, + error: None, + neutral: None, + neutral_variant: None, + hue_shift: 30., + } + } + + fn generate_secondary(&self) -> ColorSource { + ColorSource { + hue: self.primary.hue + self.hue_shift, + saturation: self.primary.saturation / 2., + } + } + + fn generate_tertiary(&self, secondary: ColorSource) -> ColorSource { + let hue_shift = (secondary.hue - self.primary.hue).into_degrees().signum() * self.hue_shift; + ColorSource { + hue: self.primary.hue - hue_shift, + saturation: self.primary.saturation / 3., + } + } + + fn generate_error(&self, secondary: ColorSource, tertiary: ColorSource) -> ColorSource { + let mut error = ColorSource::new(30., self.primary.saturation); + while [self.primary, secondary, tertiary] + .iter() + .any(|c| c.contrast_between(error) < 0.10) + { + error.hue -= self.hue_shift; + } + + error + } + + fn generate_neutral(&self) -> ColorSource { + ColorSource { + hue: self.primary.hue, + saturation: ZeroToOne::new(0.01), + } + } + + fn generate_neutral_variant(&self) -> ColorSource { + ColorSource { + hue: self.primary.hue, + saturation: ZeroToOne::new(0.1), + } + } + + /// Builds a color scheme from the provided colors, generating any + /// unspecified colors. + #[must_use] + pub fn build(self) -> ColorScheme { + let secondary = self.secondary.unwrap_or_else(|| self.generate_secondary()); + let tertiary = self + .tertiary + .unwrap_or_else(|| self.generate_tertiary(secondary)); + ColorScheme { + primary: self.primary, + secondary, + tertiary, + error: self + .error + .unwrap_or_else(|| self.generate_error(secondary, tertiary)), + neutral: self.neutral.unwrap_or_else(|| self.generate_neutral()), + neutral_variant: self + .neutral_variant + .unwrap_or_else(|| self.generate_neutral_variant()), + } + } +} + +/// A color scheme for a Gooey application. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ColorScheme { + /// The primary accent color. + pub primary: ColorSource, + /// A secondary accent color. + pub secondary: ColorSource, + /// A tertiary accent color. + pub tertiary: ColorSource, + /// A color used to denote errors. + pub error: ColorSource, + /// A neutral color. + pub neutral: ColorSource, + /// A neutral color with a different tone than `neutral`. + pub neutral_variant: ColorSource, +} + +impl ColorScheme { + /// Returns a generated color scheme based on a `primary` color. + #[must_use] + pub fn from_primary(primary: ColorSource) -> Self { + ColorSchemeBuilder::new(primary).build() + } + + /// Returns a generated color scheme based on a `primary` hue, in degrees. + #[must_use] + pub fn from_primary_hue(hue: impl Into) -> Self { + ColorSchemeBuilder::from_hue(hue).build() + } +} + +impl Default for ColorScheme { + fn default() -> Self { + Self::from_primary_hue(138.5) + } +} + +impl From for ColorScheme { + fn from(primary: ColorSource) -> Self { + ColorScheme::from_primary(primary) + } +}