diff options
Diffstat (limited to '')
| -rw-r--r-- | src/config.rs | 121 | ||||
| -rw-r--r-- | src/dialogs.rs (renamed from iced_builder/src/dialogs.rs) | 19 | ||||
| -rw-r--r-- | src/environment.rs | 43 | ||||
| -rw-r--r-- | src/error.rs (renamed from iced_builder/src/error.rs) | 27 | ||||
| -rw-r--r-- | src/icon.rs (renamed from iced_builder/src/icon.rs) | 0 | ||||
| -rw-r--r-- | src/main.rs (renamed from iced_builder/src/main.rs) | 124 | ||||
| -rw-r--r-- | src/panes.rs (renamed from iced_builder/src/panes.rs) | 0 | ||||
| -rw-r--r-- | src/panes/code_view.rs (renamed from iced_builder/src/panes/code_view.rs) | 9 | ||||
| -rw-r--r-- | src/panes/designer_view.rs (renamed from iced_builder/src/panes/designer_view.rs) | 0 | ||||
| -rw-r--r-- | src/panes/element_list.rs (renamed from iced_builder/src/panes/element_list.rs) | 8 | ||||
| -rw-r--r-- | src/panes/style.rs (renamed from iced_builder/src/panes/style.rs) | 0 | ||||
| -rw-r--r-- | src/theme.rs | 381 | ||||
| -rw-r--r-- | src/types.rs (renamed from iced_builder/src/types.rs) | 10 | ||||
| -rw-r--r-- | src/types/element_name.rs (renamed from iced_builder/src/types/element_name.rs) | 19 | ||||
| -rw-r--r-- | src/types/project.rs (renamed from iced_builder/src/types/project.rs) | 109 | ||||
| -rwxr-xr-x | src/types/rendered_element.rs (renamed from iced_builder/src/types/rendered_element.rs) | 144 | ||||
| -rw-r--r-- | src/widget.rs (renamed from iced_builder/src/widget.rs) | 0 |
17 files changed, 812 insertions, 202 deletions
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9d29af7 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,121 @@ +use std::path::PathBuf; + +use serde::Deserialize; +use tokio_stream::wrappers::ReadDirStream; +use tokio_stream::StreamExt; + +use crate::theme::{theme_from_str, theme_index, Appearance, Theme}; +use crate::{environment, Error}; + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub theme: Appearance, + pub last_project: Option<PathBuf>, +} + +impl Config { + pub fn selected_theme(&self) -> iced::Theme { + self.theme.selected.clone() + } + + pub fn config_dir() -> PathBuf { + let dir = environment::config_dir(); + + if !dir.exists() { + std::fs::create_dir_all(dir.as_path()) + .expect("expected permissions to create config folder"); + } + dir + } + + pub fn themes_dir() -> PathBuf { + let dir = Self::config_dir().join("themes"); + + if !dir.exists() { + std::fs::create_dir_all(dir.as_path()) + .expect("expected permissions to create themes folder"); + } + dir + } + + pub fn config_file_path() -> PathBuf { + Self::config_dir().join(environment::CONFIG_FILE_NAME) + } + + pub async fn load() -> Result<Self, Error> { + use tokio::fs; + + #[derive(Deserialize)] + pub struct Configuration { + #[serde(default)] + pub theme: String, + pub last_project: Option<PathBuf>, + } + + let path = Self::config_file_path(); + if !path.try_exists()? { + return Err(Error::ConfigMissing); + } + + let content = fs::read_to_string(path).await?; + + let Configuration { + theme, + last_project, + } = toml::from_str(content.as_ref())?; + + let theme = Self::load_theme(theme).await.unwrap_or_default(); + + Ok(Self { + theme, + last_project, + }) + } + + pub async fn load_theme(theme_name: String) -> Result<Appearance, Error> { + use tokio::fs; + + let read_entry = |entry: fs::DirEntry| async move { + let content = fs::read_to_string(entry.path()).await.ok()?; + + let theme: Theme = toml::from_str(content.as_ref()).ok()?; + let name = entry.path().file_stem()?.to_string_lossy().to_string(); + + Some(theme.into_iced_theme(name)) + }; + + let mut all = iced::Theme::ALL.to_owned(); + let mut selected = iced::Theme::default(); + + if theme_index(&theme_name, iced::Theme::ALL).is_some() { + selected = theme_from_str(None, &theme_name); + } + + let mut stream = + ReadDirStream::new(fs::read_dir(Self::themes_dir()).await?); + while let Some(entry) = stream.next().await { + let Ok(entry) = entry else { + continue; + }; + + let Some(file_name) = entry.file_name().to_str().map(String::from) + else { + continue; + }; + + if let Some(file_name) = file_name.strip_suffix(".toml") { + if let Some(theme) = read_entry(entry).await { + if file_name == theme_name { + selected = theme.clone(); + } + all.push(theme); + } + } + } + + Ok(Appearance { + selected, + all: all.into(), + }) + } +} diff --git a/iced_builder/src/dialogs.rs b/src/dialogs.rs index 047ffd2..2d916b1 100644 --- a/iced_builder/src/dialogs.rs +++ b/src/dialogs.rs @@ -9,13 +9,22 @@ pub fn error_dialog(description: impl Into<String>) { .show(); } -pub fn unsaved_changes_dialog( - description: impl Into<String>, -) -> MessageDialogResult { - MessageDialog::new() +pub fn warning_dialog(description: impl Into<String>) { + let _ = MessageDialog::new() + .set_level(MessageLevel::Warning) + .set_buttons(MessageButtons::Ok) + .set_title("Heads up!") + .set_description(description) + .show(); +} + +pub fn unsaved_changes_dialog(description: impl Into<String>) -> bool { + let result = MessageDialog::new() .set_level(MessageLevel::Warning) .set_buttons(MessageButtons::OkCancel) .set_title("Unsaved changes") .set_description(description) - .show() + .show(); + + matches!(result, MessageDialogResult::Ok) } diff --git a/src/environment.rs b/src/environment.rs new file mode 100644 index 0000000..3ecb790 --- /dev/null +++ b/src/environment.rs @@ -0,0 +1,43 @@ +use std::env; +use std::path::PathBuf; + +pub const CONFIG_FILE_NAME: &str = "config.toml"; + +pub fn config_dir() -> PathBuf { + portable_dir().unwrap_or_else(platform_specific_config_dir) +} + +fn portable_dir() -> Option<PathBuf> { + let exe = env::current_exe().ok()?; + let dir = exe.parent()?; + + dir.join(CONFIG_FILE_NAME) + .is_file() + .then(|| dir.to_path_buf()) +} + +fn platform_specific_config_dir() -> PathBuf { + #[cfg(target_os = "macos")] + { + xdg_config_dir().unwrap_or_else(|| { + dirs_next::config_dir() + .expect("expected valid config dir") + .join("iced-builder") + }) + } + #[cfg(not(target_os = "macos"))] + { + dirs_next::config_dir() + .expect("expected valid config dir") + .join("iced-builder") + } +} + +#[cfg(target_os = "macos")] +fn xdg_config_dir() -> Option<PathBuf> { + let config_dir = xdg::BaseDirectories::with_prefix("iced-builder") + .ok() + .and_then(|xdg| xdg.find_config_file(CONFIG_FILE_NAME))?; + + config_dir.parent().map(|p| p.to_path_buf()) +} diff --git a/iced_builder/src/error.rs b/src/error.rs index 8876016..f4011bd 100644 --- a/iced_builder/src/error.rs +++ b/src/error.rs @@ -6,13 +6,18 @@ use thiserror::Error; #[derive(Debug, Clone, Error)] #[error(transparent)] pub enum Error { - IOError(Arc<io::Error>), - SerdeError(Arc<serde_json::Error>), - FormatError(Arc<rust_format::Error>), - #[error("The element tree contains no matching element")] + IO(Arc<io::Error>), + #[error("config does not exist")] + ConfigMissing, + #[error("JSON parsing error: {0}")] + SerdeJSON(Arc<serde_json::Error>), + #[error("TOML parsing error: {0}")] + SerdeTOML(#[from] toml::de::Error), + RustFmt(Arc<rust_format::Error>), + #[error("the element tree contains no matching element")] NonExistentElement, #[error( - "The file dialog has been closed without selecting a valid option" + "the file dialog has been closed without selecting a valid option" )] DialogClosed, #[error("{0}")] @@ -21,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::SerdeError(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)) } } @@ -42,3 +47,9 @@ impl From<&str> for Error { Self::Other(value.to_owned()) } } + +impl From<String> for Error { + fn from(value: String) -> Self { + Self::Other(value) + } +} diff --git a/iced_builder/src/icon.rs b/src/icon.rs index f6760d5..f6760d5 100644 --- a/iced_builder/src/icon.rs +++ b/src/icon.rs diff --git a/iced_builder/src/main.rs b/src/main.rs index a041c6f..5b95b94 100644 --- a/iced_builder/src/main.rs +++ b/src/main.rs @@ -1,26 +1,47 @@ +#![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::dialogs::{error_dialog, unsaved_changes_dialog}; -use iced_builder::icon; -use iced_builder::panes::{code_view, designer_view, element_list}; -use iced_builder::types::{ - Action, DesignerPage, ElementName, Message, Project, -}; -use rfd::MessageDialogResult; +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 = { + let rt = runtime::Builder::new_current_thread() + .enable_all() + .build()?; -const THEMES: &'static [Theme] = &[Theme::SolarizedDark, Theme::SolarizedLight]; + rt.block_on(Config::load()) + }; -fn main() -> iced::Result { iced::application(App::title, App::update, App::view) .font(icon::FONT) .theme(|state| state.theme.value().clone()) .subscription(App::subscription) - .run_with(App::new) + .run_with(move || App::new(config_load))?; + + Ok(()) } struct App { @@ -28,7 +49,8 @@ struct App { is_loading: bool, project_path: Option<PathBuf>, project: Project, - theme: Spring<Theme>, + config: Config, + theme: Animated<Theme>, pane_state: pane_grid::State<Panes>, focus: Option<Pane>, designer_page: DesignerPage, @@ -43,7 +65,7 @@ enum Panes { } impl App { - fn new() -> (Self, Task<Message>) { + fn new(config_load: Result<Config, Error>) -> (Self, Task<Message>) { let state = pane_grid::State::with_configuration( pane_grid::Configuration::Split { axis: pane_grid::Axis::Vertical, @@ -52,25 +74,46 @@ impl App { b: Box::new(pane_grid::Configuration::Pane(Panes::ElementList)), }, ); + + let config = config_load.unwrap_or_default(); + let theme = config.selected_theme(); + + let mut task = Task::none(); + + if let Some(path) = config.last_project.clone() { + if path.exists() && path.is_file() { + task = Task::perform( + Project::from_path(path, config.clone()), + Message::FileOpened, + ); + } else { + warning_dialog(format!( + "The file {} does not exist, or isn't a file.", + path.to_string_lossy() + )); + } + } + ( Self { is_dirty: false, is_loading: false, project_path: None, project: Project::new(), - theme: Spring::new(Theme::SolarizedDark), + config, + theme: Animated::new(theme, Easing::EASE_IN), pane_state: state, focus: None, designer_page: DesignerPage::DesignerView, element_list: ElementName::ALL, editor_content: text_editor::Content::new(), }, - Task::none(), + task, ) } fn title(&self) -> String { - let saved_state = if !self.is_dirty { "" } else { " *" }; + let saved_state = if self.is_dirty { " *" } else { "" }; let project_name = match &self.project.title { Some(n) => { @@ -83,7 +126,7 @@ impl App { } ) } - None => "".to_owned(), + None => String::new(), }; format!("iced Builder{project_name}{saved_state}") @@ -104,7 +147,7 @@ impl App { } } Message::RefreshEditorContent => { - match self.project.clone().app_code() { + match self.project.app_code(&self.config) { Ok(code) => { self.editor_content = text_editor::Content::with_text(&code); @@ -119,23 +162,19 @@ impl App { None, None, ) - .into() } Message::HandleNew(name, zones) => { let ids: Vec<Id> = zones.into_iter().map(|z| z.0).collect(); - if ids.len() > 0 { - let action = Action::new( - ids, - &mut self.project.element_tree.clone(), - None, - ); + if !ids.is_empty() { + let eltree_clone = self.project.element_tree.clone(); + let action = Action::new(&ids, &eltree_clone, None); let result = name.handle_action( self.project.element_tree.as_mut(), action, ); match result { Ok(Some(ref element)) => { - self.project.element_tree = Some(element.clone()) + self.project.element_tree = Some(element.clone()); } Err(error) => error_dialog(error.to_string()), _ => {} @@ -152,14 +191,14 @@ impl App { None, None, ) - .into() } Message::HandleMove(element, zones) => { let ids: Vec<Id> = zones.into_iter().map(|z| z.0).collect(); - if ids.len() > 0 { + if !ids.is_empty() { + let eltree_clone = self.project.element_tree.clone(); let action = Action::new( - ids, - &mut self.project.element_tree.clone(), + &ids, + &eltree_clone, Some(element.get_id()), ); let result = element.handle_action( @@ -193,13 +232,11 @@ impl App { self.project = Project::new(); self.project_path = None; self.editor_content = text_editor::Content::new(); - } else { - if let MessageDialogResult::Ok = unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and create a new project?") { + } else if unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and create a new project?") { self.is_dirty = false; self.project = Project::new(); self.project_path = None; self.editor_content = text_editor::Content::new(); - } } } } @@ -209,15 +246,13 @@ impl App { self.is_loading = true; return Task::perform( - Project::from_path(), + Project::from_file(self.config.clone()), Message::FileOpened, ); - } else { - if let MessageDialogResult::Ok = unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and open another project?") { + } 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_path(), Message::FileOpened); - } + return Task::perform(Project::from_file(self.config.clone()), Message::FileOpened); } } } @@ -227,11 +262,12 @@ 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 - .app_code() + &self + .project + .app_code(&self.config) .unwrap_or_else(|err| err.to_string()), ); } @@ -299,7 +335,7 @@ impl App { fn view(&self) -> Element<'_, Message> { let header = row![pick_list( - THEMES, + self.config.theme.all.clone(), Some(self.theme.target().clone()), |theme| { Message::ToggleTheme(theme.into()) } )] @@ -311,12 +347,12 @@ impl App { Panes::Designer => match &self.designer_page { DesignerPage::DesignerView => designer_view::view( &self.project.element_tree, - self.project.get_theme(), + self.project.get_theme(&self.config), is_focused, ), DesignerPage::CodeView => code_view::view( &self.editor_content, - self.theme.value().clone(), + self.theme.target().clone(), is_focused, ), }, diff --git a/iced_builder/src/panes.rs b/src/panes.rs index 387662a..387662a 100644 --- a/iced_builder/src/panes.rs +++ b/src/panes.rs diff --git a/iced_builder/src/panes/code_view.rs b/src/panes/code_view.rs index fe7801c..f545157 100644 --- a/iced_builder/src/panes/code_view.rs +++ b/src/panes/code_view.rs @@ -1,15 +1,15 @@ use iced::widget::{button, pane_grid, row, text, text_editor, Space}; -use iced::{Alignment, Length, Theme}; +use iced::{Alignment, Font, Length, Theme}; use super::style; use crate::icon::copy; use crate::types::{DesignerPage, Message}; use crate::widget::tip; -pub fn view<'a>( - editor_content: &'a text_editor::Content, +pub fn view( + editor_content: &text_editor::Content, theme: Theme, is_focused: bool, -) -> pane_grid::Content<'a, Message> { +) -> pane_grid::Content<'_, Message> { let title = row![ text("Generated Code"), Space::with_width(Length::Fill), @@ -36,6 +36,7 @@ pub fn view<'a>( } else { highlighter::Theme::InspiredGitHub }, + .font(Font::MONOSPACE) ) .height(Length::Fill) .padding(20), diff --git a/iced_builder/src/panes/designer_view.rs b/src/panes/designer_view.rs index 76456db..76456db 100644 --- a/iced_builder/src/panes/designer_view.rs +++ b/src/panes/designer_view.rs diff --git a/iced_builder/src/panes/element_list.rs b/src/panes/element_list.rs index 74188af..8a1c6eb 100644 --- a/iced_builder/src/panes/element_list.rs +++ b/src/panes/element_list.rs @@ -5,7 +5,7 @@ use iced_drop::droppable; use super::style; use crate::types::{ElementName, Message}; -fn items_list_view<'a>(items: &'a [ElementName]) -> Element<'a, Message> { +fn items_list_view(items: &[ElementName]) -> Element<'_, Message> { let mut column = Column::new() .spacing(20) .align_x(Alignment::Center) @@ -26,10 +26,10 @@ fn items_list_view<'a>(items: &'a [ElementName]) -> Element<'a, Message> { .into() } -pub fn view<'a>( - element_list: &'a [ElementName], +pub fn view( + element_list: &[ElementName], is_focused: bool, -) -> pane_grid::Content<'a, Message> { +) -> pane_grid::Content<'_, Message> { let items_list = items_list_view(element_list); let content = column![items_list] .align_x(Alignment::Center) diff --git a/iced_builder/src/panes/style.rs b/src/panes/style.rs index 1eefb2d..1eefb2d 100644 --- a/iced_builder/src/panes/style.rs +++ b/src/panes/style.rs diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..7d18aa9 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,381 @@ +use std::sync::Arc; + +use iced::theme::palette::Extended; +use iced::Color; + +use crate::config::Config; + +pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option<usize> { + slice + .iter() + .position(|theme| theme.to_string() == theme_name) +} + +pub fn theme_from_str( + config: Option<&Config>, + theme_name: &str, +) -> iced::Theme { + match theme_name { + "Light" => iced::Theme::Light, + "Dark" => iced::Theme::Dark, + "Dracula" => iced::Theme::Dracula, + "Nord" => iced::Theme::Nord, + "Solarized Light" => iced::Theme::SolarizedLight, + "Solarized Dark" => iced::Theme::SolarizedDark, + "Gruvbox Light" => iced::Theme::GruvboxLight, + "Gruvbox Dark" => iced::Theme::GruvboxDark, + "Catppuccin Latte" => iced::Theme::CatppuccinLatte, + "Catppuccin Frappé" => iced::Theme::CatppuccinFrappe, + "Catppuccin Macchiato" => iced::Theme::CatppuccinMacchiato, + "Catppuccin Mocha" => iced::Theme::CatppuccinMocha, + "Tokyo Night" => iced::Theme::TokyoNight, + "Tokyo Night Storm" => iced::Theme::TokyoNightStorm, + "Tokyo Night Light" => iced::Theme::TokyoNightLight, + "Kanagawa Wave" => iced::Theme::KanagawaWave, + "Kanagawa Dragon" => iced::Theme::KanagawaDragon, + "Kanagawa Lotus" => iced::Theme::KanagawaLotus, + "Moonfly" => iced::Theme::Moonfly, + "Nightfly" => iced::Theme::Nightfly, + "Oxocarbon" => iced::Theme::Oxocarbon, + "Ferra" => iced::Theme::Ferra, + _ => { + if let Some(config) = config { + if theme_name == config.theme.selected.to_string() { + config.theme.selected.clone() + } else if let Some(index) = + theme_index(theme_name, &config.theme.all) + { + config.theme.all[index].clone() + } else { + iced::Theme::default() + } + } else { + iced::Theme::default() + } + } + } +} + +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(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, + pub all: Arc<[iced::Theme]>, +} + +impl Default for Appearance { + fn default() -> Self { + Self { + selected: iced::Theme::default(), + all: iced::Theme::ALL.into(), + } + } +} + +#[derive(Debug, Default, serde::Deserialize)] +pub struct Theme { + palette: ThemePalette, + is_dark: Option<bool>, + #[serde(flatten)] + extended: Option<ExtendedThemePalette>, +} + +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ThemePalette { + #[serde(with = "color_serde")] + background: Color, + #[serde(with = "color_serde")] + text: Color, + #[serde(with = "color_serde")] + primary: Color, + #[serde(with = "color_serde")] + success: Color, + #[serde(with = "color_serde")] + danger: Color, +} + +impl Theme { + pub fn into_iced_theme(self, name: String) -> iced::Theme { + iced::Theme::custom_with_fn(name, self.palette.clone().into(), |_| { + self.into() + }) + } +} + +impl Default for ThemePalette { + fn default() -> Self { + let palette = iced::Theme::default().palette(); + Self { + background: palette.background, + text: palette.text, + primary: palette.primary, + success: palette.success, + danger: palette.danger, + } + } +} + +impl From<ThemePalette> for iced::theme::Palette { + fn from(palette: ThemePalette) -> Self { + iced::theme::Palette { + background: palette.background, + text: palette.text, + primary: palette.primary, + success: palette.success, + danger: palette.danger, + } + } +} + +impl From<Theme> for Extended { + fn from(theme: Theme) -> Self { + let mut extended = Extended::generate(theme.palette.into()); + + if let Some(is_dark) = theme.is_dark { + extended.is_dark = is_dark; + } + + if let Some(extended_palette) = theme.extended { + if let Some(background) = extended_palette.background { + if let Some(base) = background.base { + extended.background.base = base.into(); + } + if let Some(weak) = background.weak { + extended.background.weak = weak.into(); + } + if let Some(strong) = background.strong { + extended.background.strong = strong.into(); + } + } + + // Handle primary + if let Some(primary) = extended_palette.primary { + if let Some(base) = primary.base { + extended.primary.base = base.into(); + } + if let Some(weak) = primary.weak { + extended.primary.weak = weak.into(); + } + if let Some(strong) = primary.strong { + extended.primary.strong = strong.into(); + } + } + + // Handle secondary + if let Some(secondary) = extended_palette.secondary { + if let Some(base) = secondary.base { + extended.secondary.base = base.into(); + } + if let Some(weak) = secondary.weak { + extended.secondary.weak = weak.into(); + } + if let Some(strong) = secondary.strong { + extended.secondary.strong = strong.into(); + } + } + + // Handle success + if let Some(success) = extended_palette.success { + if let Some(base) = success.base { + extended.success.base = base.into(); + } + if let Some(weak) = success.weak { + extended.success.weak = weak.into(); + } + if let Some(strong) = success.strong { + extended.success.strong = strong.into(); + } + } + + // Handle danger + if let Some(danger) = extended_palette.danger { + if let Some(base) = danger.base { + extended.danger.base = base.into(); + } + if let Some(weak) = danger.weak { + extended.danger.weak = weak.into(); + } + if let Some(strong) = danger.strong { + extended.danger.strong = strong.into(); + } + } + } + + extended + } +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ExtendedThemePalette { + background: Option<ThemeBackground>, + primary: Option<ThemePrimary>, + secondary: Option<ThemeSecondary>, + success: Option<ThemeSuccess>, + danger: Option<ThemeDanger>, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeBackground { + base: Option<ThemePair>, + weak: Option<ThemePair>, + strong: Option<ThemePair>, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemePrimary { + base: Option<ThemePair>, + weak: Option<ThemePair>, + strong: Option<ThemePair>, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeSecondary { + base: Option<ThemePair>, + weak: Option<ThemePair>, + strong: Option<ThemePair>, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeSuccess { + base: Option<ThemePair>, + weak: Option<ThemePair>, + strong: Option<ThemePair>, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeDanger { + base: Option<ThemePair>, + weak: Option<ThemePair>, + strong: Option<ThemePair>, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemePair { + #[serde(with = "color_serde")] + color: Color, + #[serde(with = "color_serde")] + text: Color, +} + +impl From<ThemePair> for iced::theme::palette::Pair { + fn from(pair: ThemePair) -> Self { + Self { + color: pair.color, + text: pair.text, + } + } +} + +mod color_serde { + use iced::Color; + use serde::{Deserialize, Deserializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result<Color, D::Error> + where + D: Deserializer<'de>, + { + Ok(String::deserialize(deserializer) + .map(|hex| Color::parse(&hex))? + .unwrap_or(Color::TRANSPARENT)) + } +} diff --git a/iced_builder/src/types.rs b/src/types.rs index 161b5e1..ac9d039 100644 --- a/iced_builder/src/types.rs +++ b/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/src/types/element_name.rs index e172227..2687673 100644 --- a/iced_builder/src/types/element_name.rs +++ b/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,23 +31,22 @@ 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), Self::Column => column(None), }; match action { - Action::Stop => Ok(None), - Action::Drop => Ok(None), + Action::Stop | Action::Drop => Ok(None), Action::AddNew => Ok(Some(element)), Action::PushFront(id) => { element_tree - .ok_or("The action was of kind `PushFront`, but no element tree was provided.")? + .ok_or("the action was of kind `PushFront`, but no element tree was provided.")? .find_by_id(id) .ok_or(Error::NonExistentElement)? .push_front(&element); @@ -56,7 +55,7 @@ impl ElementName { Action::InsertAfter(parent_id, child_id) => { element_tree .ok_or( - "The action was of kind `InsertAfter`, but no element tree was provided.", + "the action was of kind `InsertAfter`, but no element tree was provided.", )? .find_by_id(parent_id) .ok_or(Error::NonExistentElement)? @@ -75,7 +74,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/src/types/project.rs index f4dbcc4..27c576b 100644 --- a/iced_builder/src/types/project.rs +++ b/src/types/project.rs @@ -1,17 +1,29 @@ 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::{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 { + fn default() -> Self { + Self::new() + } } impl Project { @@ -20,41 +32,43 @@ impl Project { title: None, theme: None, element_tree: None, + theme_cache: FxHashMap::default(), } } - pub fn get_theme(&self) -> Theme { + pub fn get_theme(&self, config: &Config) -> Theme { match &self.theme { - Some(theme) => match theme.as_str() { - "Light" => Theme::Light, - "Dark" => Theme::Dark, - "Dracula" => Theme::Dracula, - "Nord" => Theme::Nord, - "Solarized Light" => Theme::SolarizedLight, - "Solarized Dark" => Theme::SolarizedDark, - "Gruvbox Light" => Theme::GruvboxLight, - "Gruvbox Dark" => Theme::GruvboxDark, - "Catppuccin Latte" => Theme::CatppuccinLatte, - "Catppuccin Frappé" => Theme::CatppuccinFrappe, - "Catppuccin Macchiato" => Theme::CatppuccinMacchiato, - "Catppuccin Mocha" => Theme::CatppuccinMocha, - "Tokyo Night" => Theme::TokyoNight, - "Tokyo Night Storm" => Theme::TokyoNightStorm, - "Tokyo Night Light" => Theme::TokyoNightLight, - "Kanagawa Wave" => Theme::KanagawaWave, - "Kanagawa Dragon" => Theme::KanagawaDragon, - "Kanagawa Lotus" => Theme::KanagawaLotus, - "Moonfly" => Theme::Moonfly, - "Nightfly" => Theme::Nightfly, - "Oxocarbon" => Theme::Oxocarbon, - "Ferra" => Theme::Ferra, - _ => Theme::Dark, - }, - None => Theme::Dark, + Some(theme) => theme_from_str(Some(config), 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() -> Result<(PathBuf, Self)> { + pub async fn from_path( + path: PathBuf, + config: Config, + ) -> 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)); + + Ok((path, project)) + } + + 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"]) @@ -64,13 +78,13 @@ impl Project { let path = picked_file.path().to_owned(); - let contents = tokio::fs::read_to_string(&path).await?; - let element: Self = serde_json::from_str(&contents)?; - - Ok((path, element)) + 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 { @@ -91,16 +105,25 @@ impl Project { Ok(path) } - pub fn app_code(&self) -> 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() @@ -127,9 +150,9 @@ impl Project { Some(ref t) => t, None => "New app", }, - self.get_theme().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/src/types/rendered_element.rs index d4d1a6c..b001556 100755 --- a/iced_builder/src/types/rendered_element.rs +++ b/src/types/rendered_element.rs @@ -1,17 +1,18 @@ use std::collections::BTreeMap; -use blob_uuid::random_blob; use iced::advanced::widget::Id; use iced::{widget, Element, Length}; use serde::{Deserialize, Serialize}; +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 { - id: String, + #[serde(skip, default = "Uuid::new_v4")] + id: Uuid, child_elements: Option<Vec<RenderedElement>>, name: ElementName, options: BTreeMap<String, Option<String>>, @@ -20,7 +21,7 @@ pub struct RenderedElement { impl RenderedElement { fn new(name: ElementName) -> Self { Self { - id: random_blob(), + id: Uuid::new_v4(), child_elements: None, name, options: BTreeMap::new(), @@ -29,7 +30,7 @@ impl RenderedElement { fn with(name: ElementName, child_elements: Vec<RenderedElement>) -> Self { Self { - id: random_blob(), + id: Uuid::new_v4(), child_elements: Some(child_elements), name, options: BTreeMap::new(), @@ -37,22 +38,22 @@ impl RenderedElement { } pub fn get_id(&self) -> Id { - Id::new(self.id.clone()) + Id::new(self.id.to_string()) } - pub fn find_by_id(&mut self, id: Id) -> Option<&mut Self> { - if self.get_id() == id.clone() { - return Some(self); + pub fn find_by_id(&mut self, id: &Id) -> Option<&mut Self> { + if &self.get_id() == id { + Some(self) } else if let Some(child_elements) = self.child_elements.as_mut() { for element in child_elements { - let element = element.find_by_id(id.clone()); + let element = element.find_by_id(id); if element.is_some() { return element; } } - return None; + None } else { - return None; + None } } @@ -66,24 +67,21 @@ impl RenderedElement { if self .child_elements .clone() - .unwrap_or(vec![]) + .unwrap_or_default() .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; } + None } pub fn is_parent(&self) -> bool { @@ -111,10 +109,10 @@ impl RenderedElement { } } - pub fn insert_after(&mut self, id: Id, element: &RenderedElement) { + pub fn insert_after(&mut self, id: &Id, element: &RenderedElement) { if let Some(child_elements) = self.child_elements.as_mut() { if let Some(index) = - child_elements.iter().position(|x| x.get_id() == id) + child_elements.iter().position(|x| &x.get_id() == id) { child_elements.insert(index + 1, element.clone()); } else { @@ -127,7 +125,7 @@ impl RenderedElement { &self, element_tree: Option<&mut RenderedElement>, action: Action, - ) -> Result<()> { + ) -> Result<(), Error> { let element_tree = element_tree.unwrap(); match action { @@ -159,7 +157,7 @@ impl RenderedElement { } } - fn preset_options<'a>(mut self, options: &[&'a str]) -> Self { + fn preset_options(mut self, options: &[&str]) -> Self { for opt in options { let _ = self.options.insert(opt.to_string(), None); } @@ -174,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( @@ -219,7 +217,7 @@ impl RenderedElement { for element in els { let (c_imports, children) = element.codegen(); imports = format!("{imports}{c_imports}"); - elements = format!("{elements}{},", children); + elements = format!("{elements}{children},"); } } @@ -262,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}"); } @@ -336,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 { @@ -347,13 +345,13 @@ impl<'a> From<RenderedElement> for Element<'a, Message> { .padding(20) .into() } - ElementName::Row => widget::Row::from_iter( - child_elements.into_iter().map(|el| el.into()), + ElementName::Row => widget::Row::from_vec( + child_elements.into_iter().map(Into::into).collect(), ) .padding(20) .into(), - ElementName::Column => widget::Column::from_iter( - child_elements.into_iter().map(|el| el.into()), + ElementName::Column => widget::Column::from_vec( + child_elements.into_iter().map(Into::into).collect(), ) .padding(20) .into(), @@ -369,18 +367,18 @@ impl<'a> From<RenderedElement> for Element<'a, Message> { } #[derive(Debug, Clone)] -pub enum Action { +pub enum Action<'a> { AddNew, - PushFront(Id), - InsertAfter(Id, Id), + PushFront(&'a Id), + InsertAfter(&'a Id, &'a Id), Drop, Stop, } -impl Action { +impl<'a> Action<'a> { pub fn new( - ids: Vec<Id>, - element_tree: &mut Option<RenderedElement>, + ids: &'a [Id], + element_tree: &'a Option<RenderedElement>, source_id: Option<Id>, ) -> Self { let mut action = Self::Stop; @@ -391,51 +389,39 @@ impl Action { action = Self::Drop; } } else { - let id: Id = match source_id { + let id: &Id = match source_id { Some(id) if ids.contains(&id) => { let element_id = - ids[ids.iter().position(|x| *x == id).unwrap()].clone(); - if ids.len() > 2 && ids[ids.clone().len() - 1] == element_id - { + &ids[ids.iter().position(|x| *x == id).unwrap()]; + if ids.len() > 2 && &ids[ids.len() - 1] == element_id { return Self::Stop; } element_id } - _ => ids.last().cloned().unwrap(), + _ => ids.last().unwrap(), }; - let element = element_tree - .as_mut() - .unwrap() - .find_by_id(id.clone()) - .unwrap(); - - // Element IS a parent but ISN'T a non-empty container - match element.is_parent() - && !(element.name == ElementName::Container - && !element.is_empty()) + let mut element_tree = element_tree.clone().unwrap(); + let element = element_tree.find_by_id(id).unwrap(); + + // Element is a parent and isn't a non-empty container + if (element.is_empty() || !(element.name == ElementName::Container)) + && element.is_parent() { - true => { - action = Self::PushFront(id); - } - false if ids.len() > 2 => { - let parent = element_tree - .as_mut() - .unwrap() - .find_by_id(ids[&ids.len() - 2].clone()) - .unwrap(); - - if parent.name == ElementName::Container - && parent.child_elements != Some(vec![]) - { - action = Self::Stop; - } else { - action = Self::InsertAfter( - ids[&ids.len() - 2].clone(), - ids[&ids.len() - 1].clone(), - ); - } + action = Self::PushFront(id); + } else if ids.len() > 2 { + let parent = + element_tree.find_by_id(&ids[ids.len() - 2]).unwrap(); + + if parent.name == ElementName::Container + && parent.child_elements != Some(vec![]) + { + action = Self::Stop; + } else { + action = Self::InsertAfter( + &ids[ids.len() - 2], + &ids[ids.len() - 1], + ); } - _ => {} } } action @@ -456,7 +442,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 { diff --git a/iced_builder/src/widget.rs b/src/widget.rs index ed2073a..ed2073a 100644 --- a/iced_builder/src/widget.rs +++ b/src/widget.rs |
