use std::path::{Path, PathBuf}; 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}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Project { pub title: Option, pub theme: Option, pub element_tree: Option, #[serde(skip)] theme_cache: FxHashMap, } impl Default for Project { fn default() -> Self { Self::new() } } impl Project { pub fn new() -> Self { Self { title: None, theme: None, element_tree: None, theme_cache: FxHashMap::default(), } } pub fn get_theme(&self, config: &Config) -> Theme { match &self.theme { 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( 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"]) .pick_file() .await .ok_or(Error::DialogClosed)?; let path = picked_file.path().to_owned(); Self::from_path(path, config).await } pub async fn write_to_file( self, path: Option, ) -> Result { let path = if let Some(p) = path { p } else { rfd::AsyncFileDialog::new() .set_title("Save to JSON file...") .add_filter("*.json, *.JSON", &["json", "JSON"]) .save_file() .await .as_ref() .map(rfd::FileHandle::path) .map(Path::to_owned) .ok_or(Error::DialogClosed)? }; let contents = serde_json::to_string(&self)?; tokio::fs::write(&path, contents).await?; Ok(path) } pub fn app_code(&mut self, config: &Config) -> Result { 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 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,Warning,Pair}}}}}};\n"; } else { theme_imports = "use iced::{{color,theme::Palette}};\n"; } } let app_code = format!( r#"// Automatically generated by iced Builder use iced::{{widget::{{{imports}}},Element}}; {theme_imports} fn main() -> iced::Result {{ iced::application("{}", State::update, State::view).theme(State::theme).run() }} #[derive(Default)] struct State; #[derive(Debug, Clone)] enum Message {{}} impl State {{ fn update(&mut self, _message: Message) {{}} fn theme(&self) -> iced::Theme {{ iced::Theme::{} }} fn view(&self) -> Element {{ {view}.into() }} }}"#, match self.title { Some(ref t) => t, None => "New app", }, theme_code ); let config = rust_format::Config::new_str() .edition(Edition::Rust2021) .option("trailing_comma", "Never") .option("imports_granularity", "Crate"); let rustfmt = RustFmt::from_config(config); Ok(rustfmt.format_str(app_code)?) } None => Err("No element tree present".into()), } } }