diff options
Diffstat (limited to 'iced_builder/src/types')
| -rw-r--r-- | iced_builder/src/types/element_name.rs | 86 | ||||
| -rw-r--r-- | iced_builder/src/types/mod.rs | 9 | ||||
| -rw-r--r-- | iced_builder/src/types/project.rs | 137 | ||||
| -rwxr-xr-x | iced_builder/src/types/rendered_element.rs | 430 |
4 files changed, 662 insertions, 0 deletions
diff --git a/iced_builder/src/types/element_name.rs b/iced_builder/src/types/element_name.rs new file mode 100644 index 0000000..8d00814 --- /dev/null +++ b/iced_builder/src/types/element_name.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Serialize}; + +use crate::Error; + +use super::rendered_element::{ + button, column, container, image, row, svg, text, ActionKind, RenderedElement, +}; + +#[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: [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: ActionKind, + ) -> Result<Option<RenderedElement>, 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 { + ActionKind::Stop => Ok(None), + ActionKind::AddNew => Ok(Some(element)), + ActionKind::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) + } + ActionKind::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/iced_builder/src/types/mod.rs b/iced_builder/src/types/mod.rs new file mode 100644 index 0000000..a48a2d8 --- /dev/null +++ b/iced_builder/src/types/mod.rs @@ -0,0 +1,9 @@ +pub mod element_name; +pub mod project; +pub mod rendered_element; + +#[derive(Debug, Clone)] +pub enum DesignerPage { + Designer, + CodeView, +} diff --git a/iced_builder/src/types/project.rs b/iced_builder/src/types/project.rs new file mode 100644 index 0000000..0e0442a --- /dev/null +++ b/iced_builder/src/types/project.rs @@ -0,0 +1,137 @@ +use rust_format::{Config, Edition, Formatter, RustFmt}; +use std::path::{Path, PathBuf}; + +use iced::Theme; +use serde::{Deserialize, Serialize}; + +use crate::Error; + +use super::rendered_element::RenderedElement; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub title: Option<String>, + pub theme: Option<String>, + pub content: Option<RenderedElement>, +} + +impl Project { + pub fn new() -> Self { + Self { + title: None, + theme: None, + content: None, + } + } + + pub fn get_theme(&self) -> 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, + } + } + + 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"]) + .pick_file() + .await + .ok_or(Error::DialogClosed)?; + + 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)) + } + + pub async fn write_to_file(self, path: Option<PathBuf>) -> Result<PathBuf, Error> { + 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.clone())?; + tokio::fs::write(&path, contents).await?; + + Ok(path) + } + + pub fn app_code(self) -> Result<String, Error> { + match &self.content { + Some(el) => { + let (imports, view) = el.codegen(); + let mut app_code = format!("use iced::{{widget::{{{imports}}},Element}};"); + + app_code = format!( + r#"// Automatically generated by iced Builder + {app_code} + + fn main() -> iced::Result {{ + iced::run("{}", State::update, State::view) + }} + + #[derive(Default)] + struct State; + + #[derive(Debug, Clone)] + enum Message {{}} + + impl State {{ + fn update(&mut self, _message: Message) {{}} + + fn view(&self) -> Element<Message> {{ + {view}.into() + }} + }}"#, + match &self.title { + Some(t) => t, + None => "New app", + } + ); + let config = 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/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs new file mode 100755 index 0000000..e2bebfa --- /dev/null +++ b/iced_builder/src/types/rendered_element.rs @@ -0,0 +1,430 @@ +use indexmap::IndexMap; + +use iced::advanced::widget::Id; +use iced::{widget, Element, Length}; +use serde::{Deserialize, Serialize}; +use unique_id::{string::StringGenerator, Generator}; + +use crate::{Error, Message}; + +use super::element_name::ElementName; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RenderedElement { + id: String, + pub child_elements: Option<Vec<RenderedElement>>, + pub name: ElementName, + pub options: IndexMap<String, Option<String>>, +} + +impl RenderedElement { + fn new(name: ElementName) -> Self { + let gen = StringGenerator::default(); + Self { + id: gen.next_id(), + child_elements: None, + name, + options: IndexMap::new(), + } + } + + fn with(name: ElementName, child_elements: Vec<RenderedElement>) -> Self { + let gen = StringGenerator::default(); + Self { + id: gen.next_id(), + child_elements: Some(child_elements), + name, + options: IndexMap::new(), + } + } + + pub fn get_id(&self) -> Id { + Id::new(self.id.clone()) + } + + pub fn find_by_id(&mut self, id: Id) -> Option<&mut Self> { + if self.get_id() == id.clone() { + return 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()); + if element.is_some() { + return element; + } + } + return None; + } else { + return 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(vec![]) + .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; + } + } + } + return None; + } + } else { + return 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) { + if let Some(child_elements) = self.child_elements.as_mut() { + if let Some(index) = child_elements.iter().position(|x| x == element) { + 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| Id::new(x.id.clone()) == 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: ActionKind, + ) -> Result<(), Error> { + let element_tree = element_tree.unwrap(); + + match action { + ActionKind::Stop => Ok(()), + ActionKind::AddNew => Err( + "The action was of kind `AddNew`, but invoking it on an existing element tree is not possible.".into(), + ), + ActionKind::PushFront(id) => { + let old_parent = element_tree.find_parent(self).unwrap(); + old_parent.remove(self); + + let new_parent = element_tree.find_by_id(id).unwrap(); + new_parent.push_front(self); + + Ok(()) + } + ActionKind::InsertAfter(parent_id, target_id) => { + let old_parent = element_tree.find_parent(self).unwrap(); + old_parent.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: Vec<&str>) -> Self { + for opt in options { + self.options.insert(opt.to_owned(), None); + } + self + } + + pub fn option<'a>(&mut self, option: &'a str, value: &'a str) -> Self { + self.options + .entry(option.to_owned()) + .and_modify(|opt| *opt = Some(value.to_owned())); + self.clone() + } + + pub fn as_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()); + } + } + iced_drop::droppable( + widget::container( + widget::column![widget::text(self.name.clone().to_string()), children] + .width(Length::Fill), + ) + .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() + } + + fn props_codegen(&self) -> String { + let mut props_string = String::new(); + + for (k, v) in self.options.clone() { + if let Some(v) = v { + props_string = format!("{props_string}.{k}({v})"); + } + } + + props_string + } + + pub fn codegen(&self) -> (String, String) { + let mut imports = String::new(); + let mut view = String::new(); + let props = self.props_codegen(); + + 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}){props}"); + } + ElementName::Row => { + imports = format!("{imports}row,"); + view = format!("{view}\nrow![{elements}]{props}"); + } + ElementName::Column => { + imports = format!("{imports}column,"); + view = format!("{view}\ncolumn![{elements}]{props}"); + } + ElementName::Text(string) => { + imports = format!("{imports}text,"); + view = format!( + "{view}\ntext(\"{}\"){props}", + if *string == String::new() { + "New Text" + } else { + string + } + ); + } + ElementName::Button(string) => { + imports = format!("{imports}button,"); + view = format!( + "{view}\nbutton(\"{}\"){props}", + if *string == String::new() { + "New Button" + } else { + string + } + ); + } + ElementName::Image(path) => { + imports = format!("{imports}image,"); + view = format!("{view}\nimage(\"{path}\"){props}"); + } + ElementName::SVG(path) => { + imports = format!("{imports}svg,"); + view = format!("{view}\nsvg(\"{path}\"){props}"); + } + } + + (imports, view) + } + + pub fn test() -> RenderedElement { + let text1 = text("wow").option("height", "120.5").option("width", "230"); + + let element = container(Some(row(Some(vec![ + text1, + text("heh"), + svg("/mnt/drive_d/git/obs-website/src/lib/assets/bars-solid.svg"), + ])))); + + element + } +} + +impl std::fmt::Display for RenderedElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut has_props = 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_props = 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_props { + 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(()) + } +} + +#[derive(Debug, Clone)] +pub enum ActionKind { + AddNew, + PushFront(Id), + InsertAfter(Id, Id), + Stop, +} + +impl ActionKind { + pub fn new( + ids: Vec<Id>, + element_tree: &mut Option<RenderedElement>, + source_id: Option<Id>, + ) -> Self { + let mut action = Self::Stop; + if ids.len() == 1 { + if element_tree.is_none() { + action = Self::AddNew; + } + } else { + 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 { + return Self::Stop; + } + element_id + } + _ => ids.last().cloned().unwrap(), + }; + let element = element_tree + .as_mut() + .unwrap() + .find_by_id(id.clone()) + .unwrap(); + + match ( + element.is_parent(), + element.name == ElementName::Container && !element.is_empty(), + ) { + (true, false) => { + action = Self::PushFront(id); + } + _ 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 + } +} + +pub fn text(text: &str) -> RenderedElement { + RenderedElement::new(ElementName::Text(text.to_owned())).preset_options(vec![ + "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>) -> RenderedElement { + match content { + Some(el) => RenderedElement::with(ElementName::Container, vec![el]), + None => RenderedElement::with(ElementName::Container, vec![]), + } +} + +pub fn row(child_elements: Option<Vec<RenderedElement>>) -> RenderedElement { + match child_elements { + Some(els) => RenderedElement::with(ElementName::Row, els), + None => RenderedElement::with(ElementName::Row, vec![]), + } +} + +pub fn column(child_elements: Option<Vec<RenderedElement>>) -> RenderedElement { + match child_elements { + Some(els) => RenderedElement::with(ElementName::Column, els), + None => RenderedElement::with(ElementName::Column, vec![]), + } +} |
