diff options
Diffstat (limited to 'iced_builder/src')
| -rw-r--r-- | iced_builder/src/config.rs | 10 | ||||
| -rw-r--r-- | iced_builder/src/environment.rs | 8 | ||||
| -rw-r--r-- | iced_builder/src/error.rs | 14 | ||||
| -rw-r--r-- | iced_builder/src/lib.rs | 12 | ||||
| -rw-r--r-- | iced_builder/src/main.rs | 47 | ||||
| -rw-r--r-- | iced_builder/src/theme.rs | 107 | ||||
| -rw-r--r-- | iced_builder/src/types.rs | 10 | ||||
| -rw-r--r-- | iced_builder/src/types/element_name.rs | 12 | ||||
| -rw-r--r-- | iced_builder/src/types/project.rs | 73 | ||||
| -rwxr-xr-x | iced_builder/src/types/rendered_element.rs | 33 |
10 files changed, 225 insertions, 101 deletions
diff --git a/iced_builder/src/config.rs b/iced_builder/src/config.rs index 631b35a..9d29af7 100644 --- a/iced_builder/src/config.rs +++ b/iced_builder/src/config.rs @@ -5,9 +5,9 @@ use tokio_stream::wrappers::ReadDirStream; use tokio_stream::StreamExt; use crate::theme::{theme_from_str, theme_index, Appearance, Theme}; -use crate::{environment, Error, Result}; +use crate::{environment, Error}; -#[derive(Debug, Default)] +#[derive(Debug, Clone, Default)] pub struct Config { pub theme: Appearance, pub last_project: Option<PathBuf>, @@ -42,7 +42,7 @@ impl Config { Self::config_dir().join(environment::CONFIG_FILE_NAME) } - pub async fn load() -> Result<Self> { + pub async fn load() -> Result<Self, Error> { use tokio::fs; #[derive(Deserialize)] @@ -72,7 +72,7 @@ impl Config { }) } - pub async fn load_theme(theme_name: String) -> Result<Appearance> { + pub async fn load_theme(theme_name: String) -> Result<Appearance, Error> { use tokio::fs; let read_entry = |entry: fs::DirEntry| async move { @@ -87,7 +87,7 @@ impl Config { let mut all = iced::Theme::ALL.to_owned(); let mut selected = iced::Theme::default(); - if theme_index(theme_name.clone(), iced::Theme::ALL).is_some() { + if theme_index(&theme_name, iced::Theme::ALL).is_some() { selected = theme_from_str(None, &theme_name); } diff --git a/iced_builder/src/environment.rs b/iced_builder/src/environment.rs index 52e7ca5..3ecb790 100644 --- a/iced_builder/src/environment.rs +++ b/iced_builder/src/environment.rs @@ -7,14 +7,6 @@ pub fn config_dir() -> PathBuf { portable_dir().unwrap_or_else(platform_specific_config_dir) } -pub fn data_dir() -> PathBuf { - portable_dir().unwrap_or_else(|| { - dirs_next::data_dir() - .expect("expected valid data dir") - .join("iced-builder") - }) -} - fn portable_dir() -> Option<PathBuf> { let exe = env::current_exe().ok()?; let dir = exe.parent()?; diff --git a/iced_builder/src/error.rs b/iced_builder/src/error.rs index 9cbb6ee..f4011bd 100644 --- a/iced_builder/src/error.rs +++ b/iced_builder/src/error.rs @@ -6,14 +6,14 @@ use thiserror::Error; #[derive(Debug, Clone, Error)] #[error(transparent)] pub enum Error { - IOError(Arc<io::Error>), + IO(Arc<io::Error>), #[error("config does not exist")] ConfigMissing, #[error("JSON parsing error: {0}")] - SerdeJSONError(Arc<serde_json::Error>), + SerdeJSON(Arc<serde_json::Error>), #[error("TOML parsing error: {0}")] - SerdeTOMLError(#[from] toml::de::Error), - FormatError(Arc<rust_format::Error>), + SerdeTOML(#[from] toml::de::Error), + RustFmt(Arc<rust_format::Error>), #[error("the element tree contains no matching element")] NonExistentElement, #[error( @@ -26,19 +26,19 @@ pub enum Error { impl From<io::Error> for Error { fn from(value: io::Error) -> Self { - Self::IOError(Arc::new(value)) + Self::IO(Arc::new(value)) } } impl From<serde_json::Error> for Error { fn from(value: serde_json::Error) -> Self { - Self::SerdeJSONError(Arc::new(value)) + Self::SerdeJSON(Arc::new(value)) } } impl From<rust_format::Error> for Error { fn from(value: rust_format::Error) -> Self { - Self::FormatError(Arc::new(value)) + Self::RustFmt(Arc::new(value)) } } diff --git a/iced_builder/src/lib.rs b/iced_builder/src/lib.rs deleted file mode 100644 index 847e01e..0000000 --- a/iced_builder/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub mod config; -pub mod dialogs; -pub mod environment; -pub mod error; -pub mod icon; -pub mod panes; -pub mod theme; -pub mod types; -pub mod widget; - -pub use error::Error; -pub type Result<T> = core::result::Result<T, Error>; diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs index 437410a..b30afaa 100644 --- a/iced_builder/src/main.rs +++ b/iced_builder/src/main.rs @@ -1,20 +1,30 @@ +#![feature(test)] +mod config; +mod dialogs; +mod environment; +mod error; +mod icon; +mod panes; +mod theme; +mod types; +mod widget; + use std::path::PathBuf; +use config::Config; +use dialogs::{error_dialog, unsaved_changes_dialog, warning_dialog}; +use error::Error; use iced::advanced::widget::Id; use iced::widget::pane_grid::{self, Pane, PaneGrid}; use iced::widget::{container, pick_list, row, text_editor, Column}; use iced::{clipboard, keyboard, Alignment, Element, Length, Task, Theme}; -use iced_anim::{Animation, Spring}; -use iced_builder::config::Config; -use iced_builder::dialogs::{ - error_dialog, unsaved_changes_dialog, warning_dialog, -}; -use iced_builder::panes::{code_view, designer_view, element_list}; -use iced_builder::types::{ - Action, DesignerPage, ElementName, Message, Project, -}; -use iced_builder::{icon, Error}; +use iced_anim::transition::Easing; +use iced_anim::{Animated, Animation}; +use panes::{code_view, designer_view, element_list}; use tokio::runtime; +use types::{Action, DesignerPage, ElementName, Message, Project}; + +//pub(crate) type Result<T> = core::result::Result<T, Error>; fn main() -> Result<(), Box<dyn std::error::Error>> { let config_load = { @@ -40,7 +50,7 @@ struct App { project_path: Option<PathBuf>, project: Project, config: Config, - theme: Spring<Theme>, + theme: Animated<Theme>, pane_state: pane_grid::State<Panes>, focus: Option<Pane>, designer_page: DesignerPage, @@ -73,7 +83,7 @@ impl App { if let Some(path) = config.last_project.clone() { if path.exists() && path.is_file() { task = Task::perform( - Project::from_path(path), + Project::from_path(path, config.clone()), Message::FileOpened, ); } else { @@ -91,7 +101,7 @@ impl App { project_path: None, project: Project::new(), config, - theme: Spring::new(theme), + theme: Animated::new(theme, Easing::EASE_IN), pane_state: state, focus: None, designer_page: DesignerPage::DesignerView, @@ -137,7 +147,7 @@ impl App { } } Message::RefreshEditorContent => { - match self.project.clone().app_code(&self.config) { + match self.project.app_code(&self.config) { Ok(code) => { self.editor_content = text_editor::Content::with_text(&code); @@ -238,13 +248,13 @@ impl App { self.is_loading = true; return Task::perform( - Project::from_file(), + Project::from_file(self.config.clone()), Message::FileOpened, ); } else if unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and open another project?") { self.is_dirty = false; self.is_loading = true; - return Task::perform(Project::from_file(), Message::FileOpened); + return Task::perform(Project::from_file(self.config.clone()), Message::FileOpened); } } } @@ -254,10 +264,11 @@ impl App { match result { Ok((path, project)) => { - self.project = project.clone(); + self.project = project; self.project_path = Some(path); self.editor_content = text_editor::Content::with_text( - &project + &self + .project .app_code(&self.config) .unwrap_or_else(|err| err.to_string()), ); diff --git a/iced_builder/src/theme.rs b/iced_builder/src/theme.rs index e41474c..e128162 100644 --- a/iced_builder/src/theme.rs +++ b/iced_builder/src/theme.rs @@ -5,10 +5,10 @@ use iced::Color; use crate::config::Config; -pub fn theme_index(theme_name: String, slice: &[iced::Theme]) -> Option<usize> { +pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option<usize> { slice .iter() - .position(|theme| theme.to_string() == theme_name) + .position(|theme| &theme.to_string() == theme_name) } pub fn theme_from_str( @@ -56,7 +56,108 @@ pub fn theme_from_str( } } -#[derive(Debug)] +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{}), + }}"#, + 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), + ) +} + +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{}),}},}},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), + ) +} + +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(16); + + 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, pub all: Arc<[iced::Theme]>, diff --git a/iced_builder/src/types.rs b/iced_builder/src/types.rs index 161b5e1..ac9d039 100644 --- a/iced_builder/src/types.rs +++ b/iced_builder/src/types.rs @@ -7,15 +7,15 @@ use std::path::PathBuf; pub use element_name::ElementName; use iced::widget::{pane_grid, text_editor}; use iced::Theme; -use iced_anim::SpringEvent; +use iced_anim::Event; pub use project::Project; pub use rendered_element::*; -use crate::Result; +use crate::Error; #[derive(Debug, Clone)] pub enum Message { - ToggleTheme(SpringEvent<Theme>), + ToggleTheme(Event<Theme>), CopyCode, SwitchPage(DesignerPage), EditorAction(text_editor::Action), @@ -35,10 +35,10 @@ pub enum Message { PaneDragged(pane_grid::DragEvent), NewFile, OpenFile, - FileOpened(Result<(PathBuf, Project)>), + FileOpened(Result<(PathBuf, Project), Error>), SaveFile, SaveFileAs, - FileSaved(Result<PathBuf>), + FileSaved(Result<PathBuf, Error>), } #[derive(Debug, Clone)] diff --git a/iced_builder/src/types/element_name.rs b/iced_builder/src/types/element_name.rs index e172227..0e8aa65 100644 --- a/iced_builder/src/types/element_name.rs +++ b/iced_builder/src/types/element_name.rs @@ -3,13 +3,13 @@ use serde::{Deserialize, Serialize}; use super::rendered_element::{ button, column, container, image, row, svg, text, Action, RenderedElement, }; -use crate::{Error, Result}; +use crate::Error; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ElementName { Text(String), Button(String), - SVG(String), + Svg(String), Image(String), Container, Row, @@ -20,7 +20,7 @@ impl ElementName { pub const ALL: &'static [Self; 7] = &[ Self::Text(String::new()), Self::Button(String::new()), - Self::SVG(String::new()), + Self::Svg(String::new()), Self::Image(String::new()), Self::Container, Self::Row, @@ -31,11 +31,11 @@ impl ElementName { &self, element_tree: Option<&mut RenderedElement>, action: Action, - ) -> Result<Option<RenderedElement>> { + ) -> Result<Option<RenderedElement>, Error> { let element = match self { Self::Text(_) => text(""), Self::Button(_) => button(""), - Self::SVG(_) => svg(""), + Self::Svg(_) => svg(""), Self::Image(_) => image(""), Self::Container => container(None), Self::Row => row(None), @@ -75,7 +75,7 @@ impl std::fmt::Display for ElementName { match self { Self::Text(_) => "Text", Self::Button(_) => "Button", - Self::SVG(_) => "SVG", + Self::Svg(_) => "SVG", Self::Image(_) => "Image", Self::Container => "Container", Self::Row => "Row", diff --git a/iced_builder/src/types/project.rs b/iced_builder/src/types/project.rs index 6f3b7ed..6479b1d 100644 --- a/iced_builder/src/types/project.rs +++ b/iced_builder/src/types/project.rs @@ -1,18 +1,23 @@ use std::path::{Path, PathBuf}; +extern crate fxhash; +use fxhash::FxHashMap; use iced::Theme; -use rust_format::{Config, Edition, Formatter, RustFmt}; +use rust_format::{Edition, Formatter, RustFmt}; use serde::{Deserialize, Serialize}; use super::rendered_element::RenderedElement; -use crate::theme::theme_from_str; -use crate::{Error, Result}; +use crate::config::Config; +use crate::theme::{theme_from_str, theme_index, theme_to_string}; +use crate::Error; #[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 { @@ -27,24 +32,42 @@ impl Project { title: None, theme: None, element_tree: None, + theme_cache: FxHashMap::default(), } } - pub fn get_theme(&self, config: &crate::config::Config) -> Theme { + pub fn get_theme(&self, config: &Config) -> Theme { match &self.theme { Some(theme) => theme_from_str(Some(config), theme), - None => Theme::Dark, + None => Theme::default(), } } - pub async fn from_path(path: PathBuf) -> Result<(PathBuf, Self)> { + 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: Config, + ) -> Result<(PathBuf, Self), Error> { let contents = tokio::fs::read_to_string(&path).await?; - let element: Self = serde_json::from_str(&contents)?; + let mut project: Self = serde_json::from_str(&contents)?; + + let _ = project.theme_code(&project.get_theme(&config)); - Ok((path, element)) + Ok((path, project)) } - pub async fn from_file() -> Result<(PathBuf, Self)> { + pub async fn from_file(config: Config) -> Result<(PathBuf, Self), Error> { let picked_file = rfd::AsyncFileDialog::new() .set_title("Open a JSON file...") .add_filter("*.json, *.JSON", &["json", "JSON"]) @@ -54,10 +77,13 @@ impl Project { let path = picked_file.path().to_owned(); - Self::from_path(path).await + Self::from_path(path, config).await } - pub async fn write_to_file(self, path: Option<PathBuf>) -> Result<PathBuf> { + pub async fn write_to_file( + self, + path: Option<PathBuf>, + ) -> Result<PathBuf, Error> { let path = if let Some(p) = path { p } else { @@ -78,16 +104,25 @@ impl Project { Ok(path) } - pub fn app_code(&self, config: &crate::config::Config) -> Result<String> { + pub fn app_code(&mut self, config: &Config) -> Result<String, Error> { match self.element_tree { Some(ref element_tree) => { let (imports, view) = element_tree.codegen(); - let mut app_code = - format!("use iced::{{widget::{{{imports}}},Element}};"); - - app_code = format!( + let theme = self.get_theme(config); + let theme_code = self.theme_code(&theme); + let mut theme_imports = ""; + if theme_index(&theme.to_string(), Theme::ALL).is_none() { + if theme_code.contains("Extended") { + theme_imports = "use iced::{{color,theme::{{Palette,palette::{{Extended,Background,Primary,Secondary,Success,Danger,Pair}}}}}};\n"; + } else { + theme_imports = "use iced::{{color,theme::Palette}};\n" + } + } + + let app_code = format!( r#"// Automatically generated by iced Builder - {app_code} + use iced::{{widget::{{{imports}}},Element}}; + {theme_imports} fn main() -> iced::Result {{ iced::application("{}", State::update, State::view).theme(State::theme).run() @@ -114,9 +149,9 @@ impl Project { Some(ref t) => t, None => "New app", }, - self.get_theme(config).to_string().replace(" ", "") + theme_code ); - let config = Config::new_str() + let config = rust_format::Config::new_str() .edition(Edition::Rust2021) .option("trailing_comma", "Never") .option("imports_granularity", "Crate"); diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs index d7efed1..177b43d 100755 --- a/iced_builder/src/types/rendered_element.rs +++ b/iced_builder/src/types/rendered_element.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use super::ElementName; use crate::types::Message; -use crate::Result; +use crate::Error; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RenderedElement { @@ -62,7 +62,7 @@ impl RenderedElement { child_element: &RenderedElement, ) -> Option<&mut Self> { if child_element == self { - Some(self) + return Some(self); } else if self.child_elements.is_some() { if self .child_elements @@ -71,20 +71,17 @@ impl RenderedElement { .contains(child_element) { return Some(self); - } else { - if let Some(child_elements) = self.child_elements.as_mut() { - for element in child_elements { - let element = element.find_parent(child_element); - if element.is_some() { - return element; - } + } + if let Some(child_elements) = self.child_elements.as_mut() { + for element in child_elements { + let element = element.find_parent(child_element); + if element.is_some() { + return element; } } - return None; } - } else { - return None; } + return None; } pub fn is_parent(&self) -> bool { @@ -128,7 +125,7 @@ impl RenderedElement { &self, element_tree: Option<&mut RenderedElement>, action: Action, - ) -> Result<()> { + ) -> Result<(), Error> { let element_tree = element_tree.unwrap(); match action { @@ -175,12 +172,12 @@ impl RenderedElement { self } - pub fn as_element<'a>(self) -> Element<'a, Message> { + pub fn into_element<'a>(self) -> Element<'a, Message> { let mut children = widget::column![]; if let Some(els) = self.child_elements.clone() { for el in els { - children = children.push(el.clone().as_element()); + children = children.push(el.clone().into_element()); } } iced_drop::droppable( @@ -263,7 +260,7 @@ impl RenderedElement { imports = format!("{imports}image,"); view = format!("{view}\nimage(\"{path}\"){options}"); } - ElementName::SVG(path) => { + ElementName::Svg(path) => { imports = format!("{imports}svg,"); view = format!("{view}\nsvg(\"{path}\"){options}"); } @@ -337,7 +334,7 @@ impl<'a> From<RenderedElement> for Element<'a, Message> { widget::button(widget::text(s)).into() } } - ElementName::SVG(p) => widget::svg(p).into(), + ElementName::Svg(p) => widget::svg(p).into(), ElementName::Image(p) => widget::image(p).into(), ElementName::Container => { widget::container(if child_elements.len() == 1 { @@ -457,7 +454,7 @@ pub fn button(text: &str) -> RenderedElement { } pub fn svg(path: &str) -> RenderedElement { - RenderedElement::new(ElementName::SVG(path.to_owned())) + RenderedElement::new(ElementName::Svg(path.to_owned())) } pub fn image(path: &str) -> RenderedElement { |
