summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--assets/themes/dark.toml46
-rw-r--r--assets/themes/light.toml46
-rw-r--r--src/main.rs10
-rw-r--r--src/theme.rs335
-rw-r--r--src/types/project.rs46
7 files changed, 318 insertions, 167 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1aee68c..e92c3fc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1971,6 +1971,7 @@ dependencies = [
name = "iced_builder"
version = "0.1.0"
dependencies = [
+ "dark-light",
"dirs-next",
"embed-resource 3.0.2",
"fxhash",
diff --git a/Cargo.toml b/Cargo.toml
index d646fd7..39e69af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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") {