diff --git a/Cargo.toml b/Cargo.toml index 147c939..6f89ed9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,22 @@ debug = ["iced/debug"] freedesktop-icons = "0.2.1" apply = "0.3.0" derive_setters = "0.1.5" +lazy_static = "1.4.0" +palette = "0.6.1" + +[dependencies.cosmic-theme] +git = "https://github.com/pop-os/cosmic-theme.git" [dependencies.iced] git = "https://github.com/pop-os/iced.git" branch = "cosmic-design-system" # path = "../iced" -features = ["cosmic-theme", "image", "svg"] +features = ["image", "svg"] + +[dependencies.iced_core] +git = "https://github.com/pop-os/iced.git" +branch = "cosmic-design-system" +# path = "../iced/core" [dependencies.iced_lazy] git = "https://github.com/pop-os/iced.git" @@ -30,19 +40,22 @@ branch = "cosmic-design-system" git = "https://github.com/pop-os/iced.git" branch = "cosmic-design-system" # path = "../iced/native" -features = ["cosmic-theme"] [dependencies.iced_style] git = "https://github.com/pop-os/iced.git" branch = "cosmic-design-system" # path = "../iced/style" -features = ["cosmic-theme"] [dependencies.iced_winit] git = "https://github.com/pop-os/iced.git" branch = "cosmic-design-system" # path = "../iced/winit" +[dependencies.iced_wgpu] +git = "https://github.com/pop-os/iced.git" +branch = "cosmic-design-system" +# path = "../iced/wgpu" + [workspace] members = [ "examples/*", diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index ecf070b..1a077de 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -4,11 +4,13 @@ use cosmic::{ checkbox, column, container, horizontal_space, pick_list, progress_bar, radio, row, slider, text, }, - iced::{self, theme, Alignment, Application, Color, Command, Element, Length, Theme}, + iced::{self, Alignment, Application, Color, Command, Length}, iced_lazy::responsive, iced_winit::window::{drag, maximize, minimize}, list_view, list_view_item, list_view_row, list_view_section, scrollable, + theme::{self, Theme}, widget::{button, header_bar, list_box, list_row, list_view::*, toggler}, + Element, }; use std::collections::BTreeMap; @@ -106,7 +108,7 @@ impl Application for Window { } fn view(&self) -> Element { - let mut header: Element = header_bar() + let mut header: Element = header_bar() .title(self.title()) .nav_title(String::from("Settings")) .sidebar_active(self.sidebar_toggled) diff --git a/src/lib.rs b/src/lib.rs index ffcf14a..c6dd88f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,13 @@ pub use iced_style; pub use iced_winit; pub mod font; +pub mod theme; pub mod widget; +pub use theme::Theme; +pub type Renderer = iced::Renderer; +pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; + #[derive(Clone, Copy, Debug)] pub enum WindowMsg { Close, diff --git a/src/theme/cosmic.rs b/src/theme/cosmic.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/theme/expander.rs b/src/theme/expander.rs new file mode 100644 index 0000000..d43dea9 --- /dev/null +++ b/src/theme/expander.rs @@ -0,0 +1,56 @@ +use iced_core::{Background, Color}; + +/// The appearance of a [`Expander`](crate::native::expander::Expander). +#[derive(Clone, Copy, Debug)] +pub struct Appearance { + /// The background of the [`Expander`](crate::native::expander::Expander). + pub background: Background, + + /// The border radius of the [`Expander`](crate::native::expander::Expander). + pub border_radius: f32, + + /// The border width of the [`Expander`](crate::native::expander::Expander). + pub border_width: f32, + + /// The border color of the [`Expander`](crate::native::expander::Expander). + pub border_color: Color, + + /// The background of the head of the [`Expander`](crate::native::expander::Expander). + pub head_background: Background, + + /// The text color of the head of the [`Expander`](crate::native::expander::Expander). + pub head_text_color: Color, + + /// The background of the body of the [`Expander`](crate::native::expander::Expander). + pub body_background: Background, + + /// The text color of the body of the [`Expander`](crate::native::expander::Expander). + pub body_text_color: Color, + + /// The color of the close icon of the [`Expander`](crate::native::expander::Expander). + pub toggle_color: Color, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: Color::WHITE.into(), + border_radius: 10.0, //32.0, + border_width: 1.0, + border_color: [0.87, 0.87, 0.87].into(), //Color::BLACK.into(), + head_background: Background::Color([0.87, 0.87, 0.87].into()), + head_text_color: Color::BLACK, + body_background: Color::TRANSPARENT.into(), + body_text_color: Color::BLACK, + toggle_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the [`Appearance`] of a container. +pub trait StyleSheet { + type Style: Default + Copy; + + /// Produces the [`Appearance`] of a container. + fn appearance(&self, style: Self::Style) -> Appearance; +} diff --git a/src/theme/mod.rs b/src/theme/mod.rs new file mode 100644 index 0000000..5b4e572 --- /dev/null +++ b/src/theme/mod.rs @@ -0,0 +1,758 @@ +pub mod expander; +pub mod palette; + +pub use self::palette::Palette; + +use iced_style::application; +use iced_style::button; +use iced_style::checkbox; +use iced_style::container; +use iced_style::menu; +use iced_style::pane_grid; +use iced_style::pick_list; +use iced_style::progress_bar; +use iced_style::radio; +use iced_style::rule; +use iced_style::scrollable; +use iced_style::slider; +use iced_style::text; +use iced_style::text_input; +use iced_style::toggler; + +use iced_core::{Background, Color}; + +type CosmicColor = ::palette::rgb::Srgba; +type CosmicComponent = cosmic_theme::Component; +type CosmicTheme = cosmic_theme::Theme; +type CosmicThemeCss = cosmic_theme::Theme; + +lazy_static::lazy_static! { + pub static ref COSMIC_DARK: CosmicTheme = CosmicThemeCss::dark_default().into_srgba(); + pub static ref COSMIC_LIGHT: CosmicTheme = CosmicThemeCss::light_default().into_srgba(); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { + Light, + Dark, +} + +impl Theme { + pub fn cosmic(self) -> &'static CosmicTheme { + match self { + Self::Dark => &COSMIC_DARK, + Self::Light => &COSMIC_LIGHT, + } + } + + pub fn palette(self) -> Palette { + match self { + Self::Dark => Palette::DARK, + Self::Light => Palette::LIGHT, + } + } + + pub fn extended_palette(&self) -> &self::palette::Extended { + match self { + Self::Dark => &self::palette::EXTENDED_DARK, + Self::Light => &self::palette::EXTENDED_LIGHT, + } + } +} + +impl Default for Theme { + fn default() -> Self { + Self::Dark + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Application { + Default, + Custom(fn(Theme) -> application::Appearance), +} + +impl Default for Application { + fn default() -> Self { + Self::Default + } +} + +impl application::StyleSheet for Theme { + type Style = Application; + + fn appearance(&self, style: Self::Style) -> application::Appearance { + let cosmic = self.cosmic(); + + match style { + Application::Default => application::Appearance { + background_color: cosmic.bg_color().into(), + text_color: cosmic.on_bg_color().into(), + }, + Application::Custom(f) => f(*self), + } + } +} + +/* + * TODO: Button + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Button { + Primary, + Secondary, + Positive, + Destructive, + Text, +} + +impl Default for Button { + fn default() -> Self { + Self::Primary + } +} + +impl Button { + fn cosmic(&self, theme: &Theme) -> &'static CosmicComponent { + let cosmic = theme.cosmic(); + match self { + Button::Primary => &cosmic.accent, + Button::Secondary => &cosmic.primary.component, + Button::Positive => &cosmic.success, + Button::Destructive => &cosmic.destructive, + Button::Text => &cosmic.secondary.component, + } + } +} + +impl button::StyleSheet for Theme { + type Style = Button; + + fn active(&self, style: Self::Style) -> button::Appearance { + let cosmic = style.cosmic(self); + + button::Appearance { + border_radius: 24.0, + background: match style { + Button::Text => None, + _ => Some(Background::Color(cosmic.base.into())), + }, + text_color: cosmic.on.into(), + ..button::Appearance::default() + } + } + + fn hovered(&self, style: Self::Style) -> button::Appearance { + let active = self.active(style); + let cosmic = style.cosmic(self); + + button::Appearance { + background: Some(Background::Color(cosmic.hover.into())), + ..active + } + } +} + +/* + * TODO: Checkbox + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Checkbox { + Primary, + Secondary, + Success, + Danger, +} + +impl Default for Checkbox { + fn default() -> Self { + Self::Primary + } +} + +impl checkbox::StyleSheet for Theme { + type Style = Checkbox; + + fn active( + &self, + style: Self::Style, + is_checked: bool, + ) -> checkbox::Appearance { + let palette = self.extended_palette(); + + match style { + Checkbox::Primary => checkbox_appearance( + palette.primary.strong.text, + palette.background.base, + palette.primary.strong, + is_checked, + ), + Checkbox::Secondary => checkbox_appearance( + palette.background.base.text, + palette.background.base, + palette.background.base, + is_checked, + ), + Checkbox::Success => checkbox_appearance( + palette.success.base.text, + palette.background.base, + palette.success.base, + is_checked, + ), + Checkbox::Danger => checkbox_appearance( + palette.danger.base.text, + palette.background.base, + palette.danger.base, + is_checked, + ), + } + } + + fn hovered( + &self, + style: Self::Style, + is_checked: bool, + ) -> checkbox::Appearance { + let palette = self.extended_palette(); + + match style { + Checkbox::Primary => checkbox_appearance( + palette.primary.strong.text, + palette.background.weak, + palette.primary.base, + is_checked, + ), + Checkbox::Secondary => checkbox_appearance( + palette.background.base.text, + palette.background.weak, + palette.background.base, + is_checked, + ), + Checkbox::Success => checkbox_appearance( + palette.success.base.text, + palette.background.weak, + palette.success.base, + is_checked, + ), + Checkbox::Danger => checkbox_appearance( + palette.danger.base.text, + palette.background.weak, + palette.danger.base, + is_checked, + ), + } + } +} + +fn checkbox_appearance( + checkmark_color: Color, + base: palette::Pair, + accent: palette::Pair, + is_checked: bool, +) -> checkbox::Appearance { + checkbox::Appearance { + background: Background::Color(if is_checked { + accent.color + } else { + base.color + }), + checkmark_color, + border_radius: 4.0, + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: accent.color, + text_color: None, + } +} + +#[derive(Clone, Copy)] +pub enum Expander { + Default, + Custom(fn(&Theme) -> expander::Appearance), +} + +impl Default for Expander { + fn default() -> Self { + Self::Default + } +} + +impl From expander::Appearance> for Expander { + fn from(f: fn(&Theme) -> expander::Appearance) -> Self { + Self::Custom(f) + } +} + +impl expander::StyleSheet for Theme { + type Style = Expander; + + fn appearance(&self, style: Self::Style) -> expander::Appearance { + match style { + Expander::Default => Default::default(), + Expander::Custom(f) => f(self), + } + } +} + +/* + * TODO: Container + */ +#[derive(Clone, Copy)] +pub enum Container { + Transparent, + Box, + Custom(fn(&Theme) -> container::Appearance), +} + +impl Default for Container { + fn default() -> Self { + Self::Transparent + } +} + +impl From container::Appearance> for Container { + fn from(f: fn(&Theme) -> container::Appearance) -> Self { + Self::Custom(f) + } +} + +impl container::StyleSheet for Theme { + type Style = Container; + + fn appearance(&self, style: Self::Style) -> container::Appearance { + match style { + Container::Transparent => Default::default(), + Container::Box => { + let palette = self.extended_palette(); + + container::Appearance { + text_color: None, + background: palette.background.weak.color.into(), + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::Custom(f) => f(self), + } + } +} + +/* + * Slider + */ +impl slider::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: Self::Style) -> slider::Appearance { + let cosmic = self.cosmic(); + + //TODO: no way to set rail thickness + slider::Appearance { + rail_colors: ( + cosmic.accent.base.into(), + //TODO: no way to set color before/after slider + Color::TRANSPARENT, + ), + handle: slider::Handle { + shape: slider::HandleShape::Circle { + radius: 10.0, + }, + color: cosmic.accent.base.into(), + border_color: Color::TRANSPARENT, + border_width: 0.0, + } + } + } + + fn hovered(&self, style: Self::Style) -> slider::Appearance { + let mut style = self.active(style); + style.handle.shape = slider::HandleShape::Circle { + radius: 16.0 + }; + style.handle.border_width = 6.0; + style.handle.border_color = match self { + Theme::Dark => Color::from_rgba8(0xFF, 0xFF, 0xFF, 0.1), + Theme::Light => Color::from_rgba8(0, 0, 0, 0.1), + }; + style + } + + fn dragging(&self, style: Self::Style) -> slider::Appearance { + let mut style = self.hovered(style); + style.handle.border_color = match self { + Theme::Dark => Color::from_rgba8(0xFF, 0xFF, 0xFF, 0.2), + Theme::Light => Color::from_rgba8(0, 0, 0, 0.2), + }; + style + } +} + +/* + * TODO: Menu + */ +impl menu::StyleSheet for Theme { + type Style = (); + + fn appearance(&self, _style: Self::Style) -> menu::Appearance { + let cosmic = self.cosmic(); + + menu::Appearance { + text_color: cosmic.primary.component.on.into(), + background: Background::Color(cosmic.background.base.into()), + border_width: 0.0, + border_radius: 16.0, + border_color: Color::TRANSPARENT, + selected_text_color: cosmic.primary.component.on.into(), + selected_background: Background::Color(cosmic.primary.component.hover.into()), + } + } +} + +/* + * TODO: Pick List + */ +impl pick_list::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: ()) -> pick_list::Appearance { + let cosmic = &self.cosmic().primary.component; + + pick_list::Appearance { + text_color: cosmic.on.into(), + background: Color::TRANSPARENT.into(), + placeholder_color: cosmic.on.into(), + border_radius: 24.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_size: 0.7, + } + } + + fn hovered(&self, style: ()) -> pick_list::Appearance { + let cosmic = &self.cosmic().primary.component; + + pick_list::Appearance { + background: Background::Color(cosmic.hover.into()), + ..self.active(style) + } + } +} + +/* + * TODO: Radio + */ +impl radio::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: Self::Style) -> radio::Appearance { + let palette = self.extended_palette(); + + radio::Appearance { + background: Color::TRANSPARENT.into(), + dot_color: palette.primary.strong.color, + border_width: 1.0, + border_color: palette.primary.strong.color, + text_color: None, + } + } + + fn hovered(&self, style: Self::Style) -> radio::Appearance { + let active = self.active(style); + let palette = self.extended_palette(); + + radio::Appearance { + dot_color: palette.primary.strong.color, + background: palette.primary.weak.color.into(), + ..active + } + } +} + +/* + * Toggler + */ +impl toggler::StyleSheet for Theme { + type Style = (); + + fn active( + &self, + _style: Self::Style, + is_active: bool, + ) -> toggler::Appearance { + let palette = self.palette(); + + toggler::Appearance { + background: if is_active { + palette.primary + } else { + //TODO: Grab neutral from palette + match self { + Theme::Dark => Color::from_rgb8(0x78, 0x78, 0x78), + Theme::Light => Color::from_rgb8(0x93, 0x93, 0x93), + } + }, + background_border: None, + //TODO: Grab neutral from palette + foreground: match self { + Theme::Dark => Color::from_rgb8(0x27, 0x27, 0x27), + Theme::Light => Color::from_rgb8(0xe4, 0xe4, 0xe4), + }, + foreground_border: None, + } + } + + fn hovered( + &self, + style: Self::Style, + is_active: bool, + ) -> toggler::Appearance { + //TODO: grab colors from palette + match self { + Theme::Dark => toggler::Appearance { + background: if is_active { + Color::from_rgb8(0x9f, 0xed, 0xed) + } else { + Color::from_rgb8(0xb6, 0xb6, 0xb6) + }, + ..self.active(style, is_active) + }, + Theme::Light => toggler::Appearance { + background: if is_active { + Color::from_rgb8(0x00, 0x42, 0x62) + } else { + Color::from_rgb8(0x54, 0x54, 0x54) + }, + ..self.active(style, is_active) + } + } + } +} + +/* + * TODO: Pane Grid + */ +impl pane_grid::StyleSheet for Theme { + type Style = (); + + fn picked_split(&self, _style: Self::Style) -> Option { + let palette = self.extended_palette(); + + Some(pane_grid::Line { + color: palette.primary.strong.color, + width: 2.0, + }) + } + + fn hovered_split(&self, _style: Self::Style) -> Option { + let palette = self.extended_palette(); + + Some(pane_grid::Line { + color: palette.primary.base.color, + width: 2.0, + }) + } +} + +/* + * TODO: Progress Bar + */ +#[derive(Clone, Copy)] +pub enum ProgressBar { + Primary, + Success, + Danger, + Custom(fn(&Theme) -> progress_bar::Appearance), +} + +impl Default for ProgressBar { + fn default() -> Self { + Self::Primary + } +} + +impl progress_bar::StyleSheet for Theme { + type Style = ProgressBar; + + fn appearance(&self, style: Self::Style) -> progress_bar::Appearance { + let palette = self.extended_palette(); + + let from_palette = |bar: Color| progress_bar::Appearance { + background: palette.background.strong.color.into(), + bar: bar.into(), + border_radius: 2.0, + }; + + match style { + ProgressBar::Primary => from_palette(palette.primary.base.color), + ProgressBar::Success => from_palette(palette.success.base.color), + ProgressBar::Danger => from_palette(palette.danger.base.color), + ProgressBar::Custom(f) => f(self), + } + } +} + +/* + * TODO: Rule + */ +#[derive(Clone, Copy)] +pub enum Rule { + Default, + Custom(fn(&Theme) -> rule::Appearance), +} + +impl Default for Rule { + fn default() -> Self { + Self::Default + } +} + +impl rule::StyleSheet for Theme { + type Style = Rule; + + fn style(&self, style: Self::Style) -> rule::Appearance { + let palette = self.extended_palette(); + + match style { + Rule::Default => rule::Appearance { + color: palette.background.strong.color, + width: 1, + radius: 0.0, + fill_mode: rule::FillMode::Full, + }, + Rule::Custom(f) => f(self), + } + } +} + +/* + * TODO: Scrollable + */ +impl scrollable::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: Self::Style) -> scrollable::Scrollbar { + let palette = self.extended_palette(); + + scrollable::Scrollbar { + background: palette.background.weak.color.into(), + border_radius: 4.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: palette.background.strong.color, + border_radius: 4.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self, _style: Self::Style) -> scrollable::Scrollbar { + let palette = self.extended_palette(); + + scrollable::Scrollbar { + background: palette.background.weak.color.into(), + border_radius: 4.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: palette.primary.strong.color, + border_radius: 4.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } +} + +/* + * TODO: Text + */ +#[derive(Clone, Copy)] +pub enum Text { + Default, + Color(Color), + Custom(fn(&Theme) -> text::Appearance), +} + +impl Default for Text { + fn default() -> Self { + Self::Default + } +} + +impl From for Text { + fn from(color: Color) -> Self { + Text::Color(color) + } +} + +impl text::StyleSheet for Theme { + type Style = Text; + + fn appearance(&self, style: Self::Style) -> text::Appearance { + match style { + Text::Default => Default::default(), + Text::Color(c) => text::Appearance { color: Some(c) }, + Text::Custom(f) => f(self), + } + } +} + +/* + * TODO: Text Input + */ +impl text_input::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: Self::Style) -> text_input::Appearance { + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.background.strong.color, + } + } + + fn hovered(&self, _style: Self::Style) -> text_input::Appearance { + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.background.base.text, + } + } + + fn focused(&self, _style: Self::Style) -> text_input::Appearance { + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.primary.strong.color, + } + } + + fn placeholder_color(&self, _style: Self::Style) -> Color { + let palette = self.extended_palette(); + + palette.background.strong.color + } + + fn value_color(&self, _style: Self::Style) -> Color { + let palette = self.extended_palette(); + + palette.background.base.text + } + + fn selection_color(&self, _style: Self::Style) -> Color { + let palette = self.extended_palette(); + + palette.primary.weak.color + } +} diff --git a/src/theme/palette.rs b/src/theme/palette.rs new file mode 100644 index 0000000..1f8a660 --- /dev/null +++ b/src/theme/palette.rs @@ -0,0 +1,290 @@ +//TODO: GET CORRECT PALETTE FROM COSMIC-THEME +use iced_core::Color; + +use lazy_static::lazy_static; +use palette::{FromColor, Hsl, Mix, RelativeContrast, Srgb}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Palette { + pub background: Color, + pub text: Color, + pub primary: Color, + pub success: Color, + pub danger: Color, +} + +impl Palette { + pub const LIGHT: Self = Self { + background: Color::from_rgb( + 0xee as f32 / 255.0, + 0xee as f32 / 255.0, + 0xee as f32 / 255.0, + ), + text: Color::from_rgb( + 0x00 as f32 / 255.0, + 0x00 as f32 / 255.0, + 0x00 as f32 / 255.0, + ), + primary: Color::from_rgb( + 0x00 as f32 / 255.0, + 0x49 as f32 / 255.0, + 0x6d as f32 / 255.0, + ), + success: Color::from_rgb( + 0x3b as f32 / 255.0, + 0x6e as f32 / 255.0, + 0x43 as f32 / 255.0, + ), + danger: Color::from_rgb( + 0xa0 as f32 / 255.0, + 0x25 as f32 / 255.0, + 0x2b as f32 / 255.0, + ), + }; + + pub const DARK: Self = Self { + background: Color::from_rgb( + 0x1e as f32 / 255.0, + 0x1e as f32 / 255.0, + 0x1e as f32 / 255.0 + ), + text: Color::from_rgb( + 0xe4 as f32 / 255.0, + 0xe4 as f32 / 255.0, + 0xe4 as f32 / 255.0, + ), + primary: Color::from_rgb( + 0x94 as f32 / 255.0, + 0xeb as f32 / 255.0, + 0xeb as f32 / 255.0, + ), + success: Color::from_rgb( + 0xac as f32 / 255.0, + 0xf7 as f32 / 255.0, + 0xd2 as f32 / 255.0, + ), + danger: Color::from_rgb( + 0xff as f32 / 255.0, + 0xb5 as f32 / 255.0, + 0xb5 as f32 / 255.0, + ), + }; +} + +pub struct Extended { + pub background: Background, + pub primary: Primary, + pub secondary: Secondary, + pub success: Success, + pub danger: Danger, +} + +lazy_static! { + pub static ref EXTENDED_LIGHT: Extended = + Extended::generate(Palette::LIGHT); + pub static ref EXTENDED_DARK: Extended = Extended::generate(Palette::DARK); +} + +impl Extended { + pub fn generate(palette: Palette) -> Self { + Self { + background: Background::new(palette.background, palette.text), + primary: Primary::generate( + palette.primary, + palette.background, + palette.text, + ), + secondary: Secondary::generate(palette.background, palette.text), + success: Success::generate( + palette.success, + palette.background, + palette.text, + ), + danger: Danger::generate( + palette.danger, + palette.background, + palette.text, + ), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Pair { + pub color: Color, + pub text: Color, +} + +impl Pair { + pub fn new(color: Color, text: Color) -> Self { + Self { + color, + text: readable(color, text), + } + } +} + +pub struct Background { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Background { + pub fn new(base: Color, text: Color) -> Self { + let weak = mix(base, text, 0.15); + let strong = mix(base, text, 0.40); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Primary { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Primary { + pub fn generate(base: Color, background: Color, text: Color) -> Self { + let weak = mix(base, background, 0.4); + let strong = deviate(base, 0.1); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Secondary { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Secondary { + pub fn generate(base: Color, text: Color) -> Self { + let base = mix(base, text, 0.2); + let weak = mix(base, text, 0.1); + let strong = mix(base, text, 0.3); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Success { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Success { + pub fn generate(base: Color, background: Color, text: Color) -> Self { + let weak = mix(base, background, 0.4); + let strong = deviate(base, 0.1); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Danger { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Danger { + pub fn generate(base: Color, background: Color, text: Color) -> Self { + let weak = mix(base, background, 0.4); + let strong = deviate(base, 0.1); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +fn darken(color: Color, amount: f32) -> Color { + let mut hsl = to_hsl(color); + + hsl.lightness = if hsl.lightness - amount < 0.0 { + 0.0 + } else { + hsl.lightness - amount + }; + + from_hsl(hsl) +} + +fn lighten(color: Color, amount: f32) -> Color { + let mut hsl = to_hsl(color); + + hsl.lightness = if hsl.lightness + amount > 1.0 { + 1.0 + } else { + hsl.lightness + amount + }; + + from_hsl(hsl) +} + +fn deviate(color: Color, amount: f32) -> Color { + if is_dark(color) { + lighten(color, amount) + } else { + darken(color, amount) + } +} + +fn mix(a: Color, b: Color, factor: f32) -> Color { + let a_lin = Srgb::from(a).into_linear(); + let b_lin = Srgb::from(b).into_linear(); + + let mixed = a_lin.mix(&b_lin, factor); + Srgb::from_linear(mixed).into() +} + +fn readable(background: Color, text: Color) -> Color { + if is_readable(background, text) { + text + } else if is_dark(background) { + Color::WHITE + } else { + Color::BLACK + } +} + +fn is_dark(color: Color) -> bool { + to_hsl(color).lightness < 0.6 +} + +fn is_readable(a: Color, b: Color) -> bool { + let a_srgb = Srgb::from(a); + let b_srgb = Srgb::from(b); + + a_srgb.has_enhanced_contrast_text(&b_srgb) +} + +fn to_hsl(color: Color) -> Hsl { + Hsl::from_color(Srgb::from(color)) +} + +fn from_hsl(hsl: Hsl) -> Color { + Srgb::from_color(hsl).into() +} diff --git a/src/widget/expander.rs b/src/widget/expander.rs index 484e361..5a9cfac 100644 --- a/src/widget/expander.rs +++ b/src/widget/expander.rs @@ -1,12 +1,11 @@ use std::vec; -use crate::{list_box_row, separator, widget::ListRow}; +use crate::{list_box_row, separator, theme, widget::ListRow, Element, Renderer, Theme}; use apply::Apply; use derive_setters::Setters; use iced::{ - theme, widget::{self, button, container, horizontal_space, row, text, Column}, - Alignment, Background, Element, Length, Renderer, Theme, + Alignment, Background, Length, }; use iced_lazy::Component; use iced_native::widget::{column, event_container}; @@ -86,14 +85,14 @@ impl<'a, Message: Clone + 'a> Component for Expander<'a, Mess } fn view(&self, state: &Self::State) -> Element { - let heading: Element = { + let heading: Element = { let mut captions = vec![text(&self.title).size(18).into()]; if let Some(subtitle) = &self.subtitle { captions.push(text(subtitle).size(16).into()); } let text = column(captions); - let space: Element = horizontal_space(Length::Fill).into(); - let toggler: Element = { + let space: Element = horizontal_space(Length::Fill).into(); + let toggler: Element = { let mut icon = super::icon( if state.expanded { "go-down-symbolic" diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 3b324e1..80d089f 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -1,7 +1,8 @@ use apply::Apply; use derive_setters::*; -use iced::{self, alignment::Vertical, theme, widget, Element, Length, Renderer}; +use iced::{self, alignment::Vertical, widget, Length}; use iced_lazy::Component; +use crate::{theme, Element, Renderer}; #[derive(Setters)] pub struct HeaderBar { @@ -103,7 +104,7 @@ impl Component for HeaderBar { .into(); let window_controls = { - let mut widgets: Vec> = Vec::with_capacity(3); + let mut widgets: Vec> = Vec::with_capacity(3); let icon = |name, size, on_press| { super::icon(name, size) diff --git a/src/widget/list/list_box.rs b/src/widget/list/list_box.rs index f8ce7db..d689a7b 100644 --- a/src/widget/list/list_box.rs +++ b/src/widget/list/list_box.rs @@ -1,4 +1,5 @@ use crate::separator; +use crate::theme::{self, Container}; use derive_setters::Setters; use iced::mouse::Interaction; use iced::{overlay, Alignment, Length, Padding, Point, Rectangle}; @@ -12,8 +13,6 @@ use iced_native::{ renderer, row, Background, Clipboard, Color, Element, Event, Layout, Shell, Widget, }; use iced_style::container::{Appearance, StyleSheet}; -use iced_style::theme; -use iced_style::theme::Container; #[derive(Setters)] #[allow(dead_code)] diff --git a/src/widget/list/macros.rs b/src/widget/list/macros.rs index 67ec3c1..f2f3649 100644 --- a/src/widget/list/macros.rs +++ b/src/widget/list/macros.rs @@ -1,4 +1,5 @@ -pub use iced::{widget, Background, Color, Theme}; +pub use iced::{widget, Background, Color}; +pub use crate::Theme; pub mod list_view { #[macro_export] @@ -82,7 +83,7 @@ pub mod list_view { use crate::widget::{Background, Color}; use iced::widget; - use iced_style::Theme; + use crate::Theme; pub use list_view; pub use list_view_item; diff --git a/src/widget/navigation/macros.rs b/src/widget/navigation/macros.rs index e00f878..0cd7085 100644 --- a/src/widget/navigation/macros.rs +++ b/src/widget/navigation/macros.rs @@ -1,5 +1,6 @@ pub mod nav_bar { - use iced::{widget, Background, Color, Theme}; + use iced::{widget, Background, Color}; + use crate::Theme; #[macro_export] macro_rules! nav_button { diff --git a/src/widget/navigation/navbar.rs b/src/widget/navigation/navbar.rs index 67a9981..9daf8f1 100644 --- a/src/widget/navigation/navbar.rs +++ b/src/widget/navigation/navbar.rs @@ -1,13 +1,14 @@ use crate::scrollable; use crate::widget::nav_bar::{nav_bar_pages_style, nav_bar_sections_style}; use crate::widget::{icon, Background}; +use crate::{theme, Theme}; use derive_setters::Setters; use iced::Length; use iced_lazy::Component; use iced_native::widget::{button, column, container, text}; use iced_native::{row, Alignment, Element}; use iced_style::button::Appearance; -use iced_style::{scrollable, theme, Theme}; +use iced_style::scrollable; use std::collections::BTreeMap; #[derive(Setters, Default)]