summaryrefslogtreecommitdiff
path: root/src/types
diff options
context:
space:
mode:
authorpml68 <contact@pml68.dev>2025-01-11 01:50:16 +0100
committerpml68 <contact@pml68.dev>2025-01-11 01:50:16 +0100
commit61926598ce96bee00aafe5340af4a905759b122a (patch)
treeb79e13b3decc778cc7c66af7187c647ae0a21a52 /src/types
parentrefactor: apply clippy suggestions (diff)
downloadiced-builder-61926598ce96bee00aafe5340af4a905759b122a.tar.gz
refactor: remove iced_drop & workspace
Diffstat (limited to 'src/types')
-rw-r--r--src/types/element_name.rs85
-rw-r--r--src/types/project.rs165
-rwxr-xr-xsrc/types/rendered_element.rs468
3 files changed, 718 insertions, 0 deletions
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<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 {
+ 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<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 {
+ 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<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)?;
+ tokio::fs::write(&path, contents).await?;
+
+ Ok(path)
+ }
+
+ 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 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<Message> {{
+ {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<Vec<RenderedElement>>,
+ name: ElementName,
+ options: BTreeMap<String, Option<String>>,
+}
+
+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<RenderedElement>) -> 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<RenderedElement> 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<RenderedElement>,
+ source_id: Option<Id>,
+ ) -> 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>) -> 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 {
+ RenderedElement::with(ElementName::Row, child_elements.unwrap_or_default())
+}
+
+pub fn column(child_elements: Option<Vec<RenderedElement>>) -> RenderedElement {
+ RenderedElement::with(
+ ElementName::Column,
+ child_elements.unwrap_or_default(),
+ )
+}