From 61926598ce96bee00aafe5340af4a905759b122a Mon Sep 17 00:00:00 2001 From: pml68 Date: Sat, 11 Jan 2025 01:50:16 +0100 Subject: refactor: remove iced_drop & workspace --- src/types/element_name.rs | 85 ++++++++ src/types/project.rs | 165 +++++++++++++++ src/types/rendered_element.rs | 468 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 718 insertions(+) create mode 100644 src/types/element_name.rs create mode 100644 src/types/project.rs create mode 100755 src/types/rendered_element.rs (limited to 'src/types') diff --git a/src/types/element_name.rs b/src/types/element_name.rs new file mode 100644 index 0000000..2687673 --- /dev/null +++ b/src/types/element_name.rs @@ -0,0 +1,85 @@ +use serde::{Deserialize, Serialize}; + +use super::rendered_element::{ + button, column, container, image, row, svg, text, Action, RenderedElement, +}; +use crate::Error; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ElementName { + Text(String), + Button(String), + Svg(String), + Image(String), + Container, + Row, + Column, +} + +impl ElementName { + pub const ALL: &'static [Self; 7] = &[ + Self::Text(String::new()), + Self::Button(String::new()), + Self::Svg(String::new()), + Self::Image(String::new()), + Self::Container, + Self::Row, + Self::Column, + ]; + + pub fn handle_action( + &self, + element_tree: Option<&mut RenderedElement>, + action: Action, + ) -> Result, Error> { + let element = match self { + Self::Text(_) => text(""), + Self::Button(_) => button(""), + Self::Svg(_) => svg(""), + Self::Image(_) => image(""), + Self::Container => container(None), + Self::Row => row(None), + Self::Column => column(None), + }; + match action { + 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.")? + .find_by_id(id) + .ok_or(Error::NonExistentElement)? + .push_front(&element); + Ok(None) + } + Action::InsertAfter(parent_id, child_id) => { + element_tree + .ok_or( + "the action was of kind `InsertAfter`, but no element tree was provided.", + )? + .find_by_id(parent_id) + .ok_or(Error::NonExistentElement)? + .insert_after(child_id, &element); + Ok(None) + } + } + } +} + +impl std::fmt::Display for ElementName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Text(_) => "Text", + Self::Button(_) => "Button", + Self::Svg(_) => "SVG", + Self::Image(_) => "Image", + Self::Container => "Container", + Self::Row => "Row", + Self::Column => "Column", + } + ) + } +} diff --git a/src/types/project.rs b/src/types/project.rs new file mode 100644 index 0000000..27c576b --- /dev/null +++ b/src/types/project.rs @@ -0,0 +1,165 @@ +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::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, + 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,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()), + } + } +} diff --git a/src/types/rendered_element.rs b/src/types/rendered_element.rs new file mode 100755 index 0000000..b001556 --- /dev/null +++ b/src/types/rendered_element.rs @@ -0,0 +1,468 @@ +use std::collections::BTreeMap; + +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::Error; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RenderedElement { + #[serde(skip, default = "Uuid::new_v4")] + id: Uuid, + child_elements: Option>, + name: ElementName, + options: BTreeMap>, +} + +impl RenderedElement { + fn new(name: ElementName) -> Self { + Self { + id: Uuid::new_v4(), + child_elements: None, + name, + options: BTreeMap::new(), + } + } + + fn with(name: ElementName, child_elements: Vec) -> Self { + Self { + id: Uuid::new_v4(), + child_elements: Some(child_elements), + name, + options: BTreeMap::new(), + } + } + + pub fn get_id(&self) -> Id { + Id::new(self.id.to_string()) + } + + 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); + if element.is_some() { + return element; + } + } + None + } else { + None + } + } + + pub fn find_parent( + &mut self, + child_element: &RenderedElement, + ) -> Option<&mut Self> { + if child_element == self { + return Some(self); + } else if self.child_elements.is_some() { + if self + .child_elements + .clone() + .unwrap_or_default() + .contains(child_element) + { + return Some(self); + } + 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; + } + } + } + } + None + } + + pub fn is_parent(&self) -> bool { + self.child_elements.is_some() + } + + pub fn is_empty(&self) -> bool { + self.child_elements == Some(vec![]) + } + + pub fn remove(&mut self, element: &RenderedElement) { + let parent = self.find_parent(element).unwrap(); + if let Some(child_elements) = parent.child_elements.as_mut() { + if let Some(index) = + child_elements.iter().position(|x| x == element) + { + let _ = child_elements.remove(index); + } + } + } + + pub fn push_front(&mut self, element: &RenderedElement) { + if let Some(child_elements) = self.child_elements.as_mut() { + child_elements.insert(0, element.clone()); + } + } + + 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.insert(index + 1, element.clone()); + } else { + child_elements.push(element.clone()); + } + } + } + + pub fn handle_action( + &self, + element_tree: Option<&mut RenderedElement>, + action: Action, + ) -> Result<(), Error> { + let element_tree = element_tree.unwrap(); + + match action { + Action::Stop => Ok(()), + Action::Drop => { + element_tree.remove(self); + + Ok(()) + } + Action::AddNew => Err( + "the action was of kind `AddNew`, but invoking it on an existing element tree is not possible".into(), + ), + Action::PushFront(id) => { + element_tree.remove(self); + + let new_parent = element_tree.find_by_id(id).unwrap(); + new_parent.push_front(self); + + Ok(()) + } + Action::InsertAfter(parent_id, target_id) => { + element_tree.remove(self); + + let new_parent = element_tree.find_by_id(parent_id).unwrap(); + new_parent.insert_after(target_id, self); + + Ok(()) + } + } + } + + fn preset_options(mut self, options: &[&str]) -> Self { + for opt in options { + let _ = self.options.insert(opt.to_string(), None); + } + self + } + + pub fn option<'a>(mut self, option: &'a str, value: &'a str) -> Self { + let _ = self + .options + .entry(option.to_owned()) + .and_modify(|opt| *opt = Some(value.to_owned())); + self + } + + 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().into_element()); + } + } + iced_drop::droppable( + widget::container( + widget::column![ + widget::text(self.name.clone().to_string()), + children + ] + .width(Length::Fill) + .spacing(10), + ) + .padding(10) + .style(widget::container::bordered_box), + ) + .id(self.get_id()) + .drag_hide(true) + .on_drop(move |point, rect| { + Message::MoveElement(self.clone(), point, rect) + }) + .into() + } + + pub fn codegen(&self) -> (String, String) { + let mut imports = String::new(); + let mut view = String::new(); + let mut options = String::new(); + + for (k, v) in self.options.clone() { + if let Some(v) = v { + options = format!("{options}.{k}({v})"); + } + } + + let mut elements = String::new(); + + if let Some(els) = &self.child_elements { + for element in els { + let (c_imports, children) = element.codegen(); + imports = format!("{imports}{c_imports}"); + elements = format!("{elements}{children},"); + } + } + + match &self.name { + ElementName::Container => { + imports = format!("{imports}container,"); + view = format!("{view}\ncontainer({elements}){options}"); + } + ElementName::Row => { + imports = format!("{imports}row,"); + view = format!("{view}\nrow![{elements}]{options}"); + } + ElementName::Column => { + imports = format!("{imports}column,"); + view = format!("{view}\ncolumn![{elements}]{options}"); + } + ElementName::Text(string) => { + imports = format!("{imports}text,"); + view = format!( + "{view}\ntext(\"{}\"){options}", + if *string == String::new() { + "New Text" + } else { + string + } + ); + } + ElementName::Button(string) => { + imports = format!("{imports}button,"); + view = format!( + "{view}\nbutton(\"{}\"){options}", + if *string == String::new() { + "New Button" + } else { + string + } + ); + } + ElementName::Image(path) => { + imports = format!("{imports}image,"); + view = format!("{view}\nimage(\"{path}\"){options}"); + } + ElementName::Svg(path) => { + imports = format!("{imports}svg,"); + view = format!("{view}\nsvg(\"{path}\"){options}"); + } + } + + (imports, view) + } +} + +impl std::fmt::Display for RenderedElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut has_options = false; + f.pad("")?; + f.write_fmt(format_args!("{:?}\n", self.name))?; + f.pad("")?; + f.write_str("Options: (")?; + for (k, v) in &self.options { + if let Some(value) = v { + has_options = true; + f.write_fmt(format_args!( + "\n{:width$.precision$}{}: {}", + "", + k, + value, + width = f.width().unwrap_or(0) + f.precision().unwrap_or(0), + precision = f.precision().unwrap_or(0) + ))?; + } + } + if has_options { + f.write_str("\n")?; + f.pad("")?; + } + f.write_str(")")?; + if let Some(els) = &self.child_elements { + f.write_str(" {\n")?; + for el in els { + f.write_fmt(format_args!( + "\n{:width$.precision$}\n", + el, + width = f.width().unwrap_or(0) + f.precision().unwrap_or(0), + precision = f.precision().unwrap_or(0) + ))?; + } + f.pad("")?; + f.write_str("}")?; + } + Ok(()) + } +} + +impl<'a> From for Element<'a, Message> { + fn from(value: RenderedElement) -> Self { + let child_elements = match value.child_elements { + Some(ref elements) => elements.clone(), + None => vec![], + }; + + let content: Element<'a, Message> = match value.name.clone() { + ElementName::Text(s) => { + if s == String::new() { + widget::text("New Text").into() + } else { + widget::text(s).into() + } + } + ElementName::Button(s) => { + if s == String::new() { + widget::button(widget::text("New Button")).into() + } else { + widget::button(widget::text(s)).into() + } + } + ElementName::Svg(p) => widget::svg(p).into(), + ElementName::Image(p) => widget::image(p).into(), + ElementName::Container => { + widget::container(if child_elements.len() == 1 { + child_elements[0].clone().into() + } else { + Element::from("") + }) + .padding(20) + .into() + } + ElementName::Row => widget::Row::from_vec( + child_elements.into_iter().map(Into::into).collect(), + ) + .padding(20) + .into(), + ElementName::Column => widget::Column::from_vec( + child_elements.into_iter().map(Into::into).collect(), + ) + .padding(20) + .into(), + }; + iced_drop::droppable(content) + .id(value.get_id()) + .drag_hide(true) + .on_drop(move |point, rect| { + Message::MoveElement(value.clone(), point, rect) + }) + .into() + } +} + +#[derive(Debug, Clone)] +pub enum Action<'a> { + AddNew, + PushFront(&'a Id), + InsertAfter(&'a Id, &'a Id), + Drop, + Stop, +} + +impl<'a> Action<'a> { + pub fn new( + ids: &'a [Id], + element_tree: &'a Option, + source_id: Option, + ) -> Self { + let mut action = Self::Stop; + if ids.len() == 1 { + if element_tree.is_none() { + action = Self::AddNew; + } else { + action = Self::Drop; + } + } else { + let id: &Id = match source_id { + Some(id) if ids.contains(&id) => { + let 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().unwrap(), + }; + 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() + { + 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 + } +} + +pub fn text(text: &str) -> RenderedElement { + RenderedElement::new(ElementName::Text(text.to_owned())).preset_options(&[ + "size", + "line_height", + "width", + "height", + ]) +} + +pub fn button(text: &str) -> RenderedElement { + RenderedElement::new(ElementName::Button(text.to_owned())) +} + +pub fn svg(path: &str) -> RenderedElement { + RenderedElement::new(ElementName::Svg(path.to_owned())) +} + +pub fn image(path: &str) -> RenderedElement { + RenderedElement::new(ElementName::Image(path.to_owned())) +} + +pub fn container(content: Option) -> RenderedElement { + match content { + Some(el) => RenderedElement::with(ElementName::Container, vec![el]), + None => RenderedElement::with(ElementName::Container, vec![]), + } +} + +pub fn row(child_elements: Option>) -> RenderedElement { + RenderedElement::with(ElementName::Row, child_elements.unwrap_or_default()) +} + +pub fn column(child_elements: Option>) -> RenderedElement { + RenderedElement::with( + ElementName::Column, + child_elements.unwrap_or_default(), + ) +} -- cgit v1.2.3