diff options
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | assets/themes/dark.toml | 46 | ||||
| -rw-r--r-- | assets/themes/light.toml | 46 | ||||
| -rw-r--r-- | src/main.rs | 10 | ||||
| -rw-r--r-- | src/theme.rs | 335 | ||||
| -rw-r--r-- | src/types/project.rs | 46 |
7 files changed, 318 insertions, 167 deletions
@@ -1971,6 +1971,7 @@ dependencies = [ name = "iced_builder" version = "0.1.0" dependencies = [ + "dark-light", "dirs-next", "embed-resource 3.0.2", "fxhash", @@ -32,6 +32,7 @@ rust-format = "0.3.4" fxhash = "0.2.1" thiserror = "2.0.11" dirs-next = "2.0.0" +dark-light = "2.0.0" [workspace.dependencies.iced] git = "https://github.com/pml68/iced" diff --git a/assets/themes/dark.toml b/assets/themes/dark.toml new file mode 100644 index 0000000..a3bde68 --- /dev/null +++ b/assets/themes/dark.toml @@ -0,0 +1,46 @@ +name = "Dark" + +[primary] +primary = "#6200ee" +on_primary = "#ffffff" +primary_container = "#3700b3" +on_primary_container = "#ffffff" + +[secondary] +secondary = "#03dac6" +on_secondary = "#ffffff" +secondary_container = "#018786" +on_secondary_container = "#ffffff" + +[tertiary] +tertiary = "#bb86fc" +on_tertiary = "#000000" +tertiary_container = "#6200ee" +on_tertiary_container = "#000000" + +[error] +error = "#b00020" +on_error = "#ffffff" +error_container = "#cf6679" +on_error_container = "#000000" + +[surface] +surface = "#121212" +on_surface = "#ffffff" +on_surface_variant = "#b0b0b0" + +[surface.surface_container] +lowest = "#1e1e2e" +low = "#333333" +base = "#444444" +high = "#555555" +highest = "#666666" + +[inverse] +inverse_surface = "#212121" +inverse_on_surface = "#ffffff" +inverse_primary = "#bb86fc" + +[outline] +outline = "#737373" +outline_variant = "#aaaaaa" diff --git a/assets/themes/light.toml b/assets/themes/light.toml new file mode 100644 index 0000000..4852b8b --- /dev/null +++ b/assets/themes/light.toml @@ -0,0 +1,46 @@ +name = "Dark" + +[primary] +primary = "#6200ee" +on_primary = "#ffffff" +primary_container = "#e1bee7" +on_primary_container = "#000000" + +[secondary] +secondary = "#03dac6" +on_secondary = "#ffffff" +secondary_container = "#018786" +on_secondary_container = "#ffffff" + +[tertiary] +tertiary = "#bb86fc" +on_tertiary = "#000000" +tertiary_container = "#6200ee" +on_tertiary_container = "#000000" + +[error] +error = "#b00020" +on_error = "#ffffff" +error_container = "#cf6679" +on_error_container = "#000000" + +[surface] +surface = "#ffffff" +on_surface = "#000000" +on_surface_variant = "#757575" + +[surface.surface_container] +lowest = "#fafafa" +low = "#eeeeee" +base = "#dddddd" +high = "#cccccc" +highest = "#bbbbbb" + +[inverse] +inverse_surface = "#121212" +inverse_on_surface = "#ffffff" +inverse_primary = "#bb86fc" + +[outline] +outline = "#757575" +outline_variant = "#b0b0b0" diff --git a/src/main.rs b/src/main.rs index d5715ef..c8a60c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,10 +104,7 @@ impl App { let task = if let Some(path) = config.last_project.clone() { if path.exists() && path.is_file() { - Task::perform( - Project::from_path(path, config.clone()), - Message::FileOpened, - ) + Task::perform(Project::from_path(path), Message::FileOpened) } else { warning_dialog(format!( "The file {} does not exist, or isn't a file.", @@ -181,7 +178,8 @@ impl App { } Err(error) => return error_dialog(error), } - } + Err(error) => Task::future(error_dialog(error)).discard(), + }, Message::DropNewElement(name, point, _) => { return iced_drop::zones_on_point( move |zones| Message::HandleNew(name.clone(), zones), @@ -405,7 +403,7 @@ impl App { Panes::Designer => match &self.designer_page { DesignerPane::DesignerView => designer_view::view( self.project.element_tree.as_ref(), - self.project.get_theme(&self.config), + self.project.get_theme(), is_focused, ), DesignerPane::CodeView => { diff --git a/src/theme.rs b/src/theme.rs index a072714..3e6092b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,12 +1,14 @@ -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use iced::Color; +use iced::theme::Base; use iced::theme::palette::Extended; +use serde::Deserialize; use crate::config::Config; -const DEFAULT_THEME_CONTENT: &str = - include_str!("../assets/themes/rose_pine.toml"); +const DARK_THEME_CONTENT: &str = include_str!("../assets/themes/dark.toml"); +const LIGHT_THEME_CONTENT: &str = include_str!("../assets/themes/light.toml"); pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option<usize> { slice @@ -59,115 +61,6 @@ pub fn theme_from_str( } } -fn palette_to_string(palette: &iced::theme::Palette) -> String { - format!( - r"Palette {{ - background: color!(0x{}), - text: color!(0x{}), - primary: color!(0x{}), - success: color!(0x{}), - danger: color!(0x{}), - warning: color!(0x{}), - }}", - color_to_hex(palette.background), - color_to_hex(palette.text), - color_to_hex(palette.primary), - color_to_hex(palette.success), - color_to_hex(palette.danger), - color_to_hex(palette.warning), - ) -} - -fn extended_to_string(extended: &Extended) -> String { - format!( - r" -Extended{{background:Background{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},primary:Primary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},secondary:Secondary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},success:Success{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},danger:Danger{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},warning:Warning{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},is_dark:true,}}", - color_to_hex(extended.background.base.color), - color_to_hex(extended.background.base.text), - color_to_hex(extended.background.weak.color), - color_to_hex(extended.background.weak.text), - color_to_hex(extended.background.strong.color), - color_to_hex(extended.background.strong.text), - color_to_hex(extended.primary.base.color), - color_to_hex(extended.primary.base.text), - color_to_hex(extended.primary.weak.color), - color_to_hex(extended.primary.weak.text), - color_to_hex(extended.primary.strong.color), - color_to_hex(extended.primary.strong.text), - color_to_hex(extended.secondary.base.color), - color_to_hex(extended.secondary.base.text), - color_to_hex(extended.secondary.weak.color), - color_to_hex(extended.secondary.weak.text), - color_to_hex(extended.secondary.strong.color), - color_to_hex(extended.secondary.strong.text), - color_to_hex(extended.success.base.color), - color_to_hex(extended.success.base.text), - color_to_hex(extended.success.weak.color), - color_to_hex(extended.success.weak.text), - color_to_hex(extended.success.strong.color), - color_to_hex(extended.success.strong.text), - color_to_hex(extended.danger.base.color), - color_to_hex(extended.danger.base.text), - color_to_hex(extended.danger.weak.color), - color_to_hex(extended.danger.weak.text), - color_to_hex(extended.danger.strong.color), - color_to_hex(extended.danger.strong.text), - color_to_hex(extended.warning.base.color), - color_to_hex(extended.warning.base.text), - color_to_hex(extended.warning.weak.color), - color_to_hex(extended.warning.weak.text), - color_to_hex(extended.warning.strong.color), - color_to_hex(extended.warning.strong.text), - ) -} - -pub fn theme_to_string(theme: &iced::Theme) -> String { - let palette = theme.palette(); - let extended = theme.extended_palette(); - - let generated_extended = Extended::generate(palette); - - if &generated_extended == extended { - format!( - r#"custom( - "{}".to_string(), - {} - )"#, - theme, - palette_to_string(&palette) - ) - } else { - format!( - r#"custom_with_fn( - "{}".to_string(), - {}, - |_| {} - )"#, - theme, - palette_to_string(&palette), - extended_to_string(extended) - ) - } -} - -fn color_to_hex(color: Color) -> String { - use std::fmt::Write; - - let mut hex = String::with_capacity(12); - - let [r, g, b, a] = color.into_rgba8(); - - let _ = write!(&mut hex, "{:02X}", r); - let _ = write!(&mut hex, "{:02X}", g); - let _ = write!(&mut hex, "{:02X}", b); - - if a < u8::MAX { - let _ = write!(&mut hex, ", {:.2}", a as f32 / 255.0); - } - - hex -} - #[derive(Debug, Clone)] pub struct Appearance { pub selected: iced::Theme, @@ -187,7 +80,158 @@ impl Default for Appearance { } } -#[derive(Debug, serde::Deserialize)] +#[derive(Debug, PartialEq, Deserialize)] +pub struct OtherTheme { + name: String, + #[serde(flatten)] + colorscheme: ColorScheme, +} + +impl Clone for OtherTheme { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + colorscheme: self.colorscheme, + } + } + + fn clone_from(&mut self, source: &Self) { + self.name = source.name.clone(); + self.colorscheme = source.colorscheme; + } +} + +impl Default for OtherTheme { + fn default() -> Self { + match dark_light::detect().unwrap_or(dark_light::Mode::Unspecified) { + dark_light::Mode::Dark | dark_light::Mode::Unspecified => { + DARK.clone() + } + dark_light::Mode::Light => LIGHT.clone(), + } + } +} + +impl Base for OtherTheme { + fn base(&self) -> iced::theme::Style { + iced::theme::Style { + background_color: self.colorscheme.surface.surface, + text_color: self.colorscheme.surface.on_surface, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct ColorScheme { + pub primary: Primary, + pub secondary: Secondary, + pub tertiary: Tertiary, + pub error: Error, + pub surface: Surface, + pub inverse: Inverse, + pub outline: Outline, +} + +pub static DARK: LazyLock<OtherTheme> = LazyLock::new(|| { + toml::from_str(DARK_THEME_CONTENT).expect("parse dark theme") +}); + +pub static LIGHT: LazyLock<OtherTheme> = LazyLock::new(|| { + toml::from_str(LIGHT_THEME_CONTENT).expect("parse light theme") +}); + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Primary { + #[serde(with = "color_serde")] + pub primary: Color, + #[serde(with = "color_serde")] + pub on_primary: Color, + #[serde(with = "color_serde")] + pub primary_container: Color, + #[serde(with = "color_serde")] + pub on_primary_container: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Secondary { + #[serde(with = "color_serde")] + pub secondary: Color, + #[serde(with = "color_serde")] + pub on_secondary: Color, + #[serde(with = "color_serde")] + pub secondary_container: Color, + #[serde(with = "color_serde")] + pub on_secondary_container: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Tertiary { + #[serde(with = "color_serde")] + pub tertiary: Color, + #[serde(with = "color_serde")] + pub on_tertiary: Color, + #[serde(with = "color_serde")] + pub tertiary_container: Color, + #[serde(with = "color_serde")] + pub on_tertiary_container: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Error { + #[serde(with = "color_serde")] + pub error: Color, + #[serde(with = "color_serde")] + pub on_error: Color, + #[serde(with = "color_serde")] + pub error_container: Color, + #[serde(with = "color_serde")] + pub on_error_container: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Surface { + #[serde(with = "color_serde")] + pub surface: Color, + #[serde(with = "color_serde")] + pub on_surface: Color, + #[serde(with = "color_serde")] + pub on_surface_variant: Color, + pub surface_container: SurfaceContainer, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct SurfaceContainer { + #[serde(with = "color_serde")] + pub lowest: Color, + #[serde(with = "color_serde")] + pub low: Color, + #[serde(with = "color_serde")] + pub base: Color, + #[serde(with = "color_serde")] + pub high: Color, + #[serde(with = "color_serde")] + pub highest: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Inverse { + #[serde(with = "color_serde")] + pub inverse_surface: Color, + #[serde(with = "color_serde")] + pub inverse_on_surface: Color, + #[serde(with = "color_serde")] + pub inverse_primary: Color, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct Outline { + #[serde(with = "color_serde")] + pub outline: Color, + #[serde(with = "color_serde")] + pub outline_variant: Color, +} + +#[derive(Debug, Deserialize)] pub struct Theme { name: String, palette: ThemePalette, @@ -200,7 +244,7 @@ impl From<Theme> for iced::Theme { fn from(value: Theme) -> Self { iced::Theme::custom_with_fn( value.name.clone(), - value.palette.clone().into(), + value.palette.into(), |_| value.into(), ) } @@ -208,11 +252,12 @@ impl From<Theme> for iced::Theme { impl Default for Theme { fn default() -> Self { - toml::from_str(DEFAULT_THEME_CONTENT).expect("parse default theme") + toml::from_str(include_str!("../assets/themes/rose_pine.toml")) + .expect("parse default theme") } } -#[derive(Debug, Clone, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Deserialize)] pub struct ThemePalette { #[serde(with = "color_serde")] background: Color, @@ -341,7 +386,7 @@ impl From<Theme> for Extended { } } -#[derive(Debug, Default, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, Deserialize)] struct ExtendedThemePalette { background: Option<ThemeBackground>, primary: Option<ThemePrimary>, @@ -351,49 +396,49 @@ struct ExtendedThemePalette { warning: Option<ThemeWarning>, } -#[derive(Debug, Default, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, Deserialize)] struct ThemeBackground { base: Option<ThemePair>, weak: Option<ThemePair>, strong: Option<ThemePair>, } -#[derive(Debug, Default, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, Deserialize)] struct ThemePrimary { base: Option<ThemePair>, weak: Option<ThemePair>, strong: Option<ThemePair>, } -#[derive(Debug, Default, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, Deserialize)] struct ThemeSecondary { base: Option<ThemePair>, weak: Option<ThemePair>, strong: Option<ThemePair>, } -#[derive(Debug, Default, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, Deserialize)] struct ThemeSuccess { base: Option<ThemePair>, weak: Option<ThemePair>, strong: Option<ThemePair>, } -#[derive(Debug, Default, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, Deserialize)] struct ThemeDanger { base: Option<ThemePair>, weak: Option<ThemePair>, strong: Option<ThemePair>, } -#[derive(Debug, Default, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, Deserialize)] struct ThemeWarning { base: Option<ThemePair>, weak: Option<ThemePair>, strong: Option<ThemePair>, } -#[derive(Debug, Default, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, Deserialize)] struct ThemePair { #[serde(with = "color_serde")] color: Color, @@ -410,16 +455,56 @@ impl From<ThemePair> for iced::theme::palette::Pair { } } +pub fn parse_argb(s: &str) -> Option<Color> { + let hex = s.strip_prefix('#').unwrap_or(s); + + let parse_channel = |from: usize, to: usize| { + let num = + usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0; + + // If we only got half a byte (one letter), expand it into a full byte (two letters) + Some(if from == to { num + num * 16.0 } else { num }) + }; + + Some(match hex.len() { + 3 => Color::from_rgb( + parse_channel(0, 0)?, + parse_channel(1, 1)?, + parse_channel(2, 2)?, + ), + 4 => Color::from_rgba( + parse_channel(1, 1)?, + parse_channel(2, 2)?, + parse_channel(3, 3)?, + parse_channel(0, 0)?, + ), + 6 => Color::from_rgb( + parse_channel(0, 1)?, + parse_channel(2, 3)?, + parse_channel(4, 5)?, + ), + 8 => Color::from_rgba( + parse_channel(2, 3)?, + parse_channel(4, 5)?, + parse_channel(6, 7)?, + parse_channel(0, 1)?, + ), + _ => None?, + }) +} + mod color_serde { use iced::Color; use serde::{Deserialize, Deserializer}; + use super::parse_argb; + pub fn deserialize<'de, D>(deserializer: D) -> Result<Color, D::Error> where D: Deserializer<'de>, { Ok(String::deserialize(deserializer) - .map(|hex| Color::parse(&hex))? + .map(|hex| parse_argb(&hex))? .unwrap_or(Color::TRANSPARENT)) } } diff --git a/src/types/project.rs b/src/types/project.rs index 721783e..50cbb69 100644 --- a/src/types/project.rs +++ b/src/types/project.rs @@ -1,24 +1,19 @@ use std::path::{Path, PathBuf}; -use std::sync::Arc; extern crate fxhash; -use fxhash::FxHashMap; use iced::Theme; use rust_format::{Edition, Formatter, RustFmt}; use serde::{Deserialize, Serialize}; use super::rendered_element::RenderedElement; use crate::Error; -use crate::config::Config; -use crate::theme::{theme_from_str, theme_index, theme_to_string}; +use crate::theme::{theme_from_str, theme_index}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Project { pub title: Option<String>, pub theme: Option<String>, pub element_tree: Option<RenderedElement>, - #[serde(skip)] - theme_cache: FxHashMap<String, String>, } impl Default for Project { @@ -33,45 +28,24 @@ impl Project { title: None, theme: None, element_tree: None, - theme_cache: FxHashMap::default(), } } - pub fn get_theme(&self, config: &Config) -> Theme { + pub fn get_theme(&self) -> Theme { match &self.theme { - Some(theme) => theme_from_str(Some(config), theme), + Some(theme) => theme_from_str(None, theme), None => Theme::default(), } } - fn theme_code(&mut self, theme: &Theme) -> String { - let theme_name = theme.to_string(); - if theme_index(&theme_name, Theme::ALL).is_none() { - (*self - .theme_cache - .entry(theme_name) - .or_insert(theme_to_string(theme))) - .to_string() - } else { - theme_name.replace(" ", "") - } - } - - pub async fn from_path( - path: PathBuf, - config: Arc<Config>, - ) -> Result<(PathBuf, Self), Error> { + pub async fn from_path(path: PathBuf) -> Result<(PathBuf, Self), Error> { let contents = tokio::fs::read_to_string(&path).await?; - let mut project: Self = serde_json::from_str(&contents)?; - - let _ = project.theme_code(&project.get_theme(&config)); + let project: Self = serde_json::from_str(&contents)?; Ok((path, project)) } - pub async fn from_file( - config: Arc<Config>, - ) -> Result<(PathBuf, Self), Error> { + pub async fn from_file() -> Result<(PathBuf, Self), Error> { let picked_file = rfd::AsyncFileDialog::new() .set_title("Open a JSON file...") .add_filter("*.json, *.JSON", &["json", "JSON"]) @@ -81,7 +55,7 @@ impl Project { let path = picked_file.path().to_owned(); - Self::from_path(path, config).await + Self::from_path(path).await } pub async fn write_to_file( @@ -108,12 +82,12 @@ impl Project { Ok(path) } - pub fn app_code(&mut self, config: &Config) -> Result<String, Error> { + pub fn app_code(&mut self) -> Result<String, Error> { match self.element_tree { Some(ref element_tree) => { let (imports, view) = element_tree.codegen(); - let theme = self.get_theme(config); - let theme_code = self.theme_code(&theme); + let theme = self.get_theme(); + let theme_code = theme.to_string().replace(" ", ""); let mut theme_imports = ""; if theme_index(&theme.to_string(), Theme::ALL).is_none() { if theme_code.contains("Extended") { |
