summaryrefslogtreecommitdiff
path: root/iced_builder
diff options
context:
space:
mode:
authorPolesznyák Márk László <116908301+pml68@users.noreply.github.com>2024-10-24 23:18:46 +0200
committerGitHub <noreply@github.com>2024-10-24 23:18:46 +0200
commitb351dd45dcd4b4c9142f069c62b51159c00922bf (patch)
treed2f24b449c3f82a9f844ce35198bad351c2ca8af /iced_builder
parentMerge pull request #1 from pml68/feat/codegen (diff)
parentfeat: implement d&d for existing elements (diff)
downloadiced-builder-b351dd45dcd4b4c9142f069c62b51159c00922bf.tar.gz
Merge pull request #2 from pml68/feat/drag-and-drop
Drag & Drop done
Diffstat (limited to '')
-rw-r--r--iced_builder/Cargo.toml25
-rw-r--r--iced_builder/fonts/icons.ttf (renamed from fonts/icons.ttf)bin6352 -> 6352 bytes
-rw-r--r--iced_builder/src/lib.rs91
-rw-r--r--iced_builder/src/main.rs415
-rw-r--r--iced_builder/src/types/element_name.rs86
-rw-r--r--iced_builder/src/types/mod.rs9
-rw-r--r--iced_builder/src/types/project.rs137
-rwxr-xr-xiced_builder/src/types/rendered_element.rs430
8 files changed, 1193 insertions, 0 deletions
diff --git a/iced_builder/Cargo.toml b/iced_builder/Cargo.toml
new file mode 100644
index 0000000..d788bc2
--- /dev/null
+++ b/iced_builder/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "iced_builder"
+description = "GUI builder for iced, built with iced."
+version = "0.1.0"
+edition = "2021"
+authors = ["pml68 <contact@pml68.me>"]
+repository = "https://github.com/pml68/iced-builder"
+license = "GPL-3.0-or-later"
+keywords = ["gui", "iced"]
+
+[dependencies]
+iced = { version = "0.13.1", features = [ "image","svg","canvas","qr_code","advanced","tokio","highlighter"] }
+iced_aw = { version = "0.11.0", default-features = false, features = ["menu","color_picker"] }
+iced_drop = { path = "../iced_drop" }
+serde = { version = "1.0.210", features = ["derive"] }
+serde_json = "1.0.128"
+tokio = { version = "1.40.0", features = ["fs"] }
+rfd = "0.15.0"
+rust-format = "0.3.4"
+unique_id = "0.1.5"
+indexmap = { version = "2.6.0", features = ["serde"] }
+
+[[bin]]
+name = "iced-builder"
+path = "src/main.rs"
diff --git a/fonts/icons.ttf b/iced_builder/fonts/icons.ttf
index 393c692..393c692 100644
--- a/fonts/icons.ttf
+++ b/iced_builder/fonts/icons.ttf
Binary files differ
diff --git a/iced_builder/src/lib.rs b/iced_builder/src/lib.rs
new file mode 100644
index 0000000..420b14c
--- /dev/null
+++ b/iced_builder/src/lib.rs
@@ -0,0 +1,91 @@
+pub mod types;
+
+use std::path::PathBuf;
+
+use iced::widget::{pane_grid, text_editor};
+use types::{
+ element_name::ElementName, project::Project, rendered_element::RenderedElement, DesignerPage,
+};
+
+#[derive(Debug, Clone)]
+pub enum Error {
+ IOError(std::io::ErrorKind),
+ SerdeError(String),
+ FormatError(String),
+ NonExistentElement,
+ DialogClosed,
+ String(String),
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::SerdeError(string) | Self::FormatError(string) | Self::String(string) => {
+ write!(f, "{}", string)
+ }
+ Self::IOError(kind) => {
+ write!(f, "{}", kind)
+ }
+ Self::NonExistentElement => {
+ write!(f, "The element tree contains no matching element.")
+ }
+ Self::DialogClosed => {
+ write!(
+ f,
+ "The file dialog has been closed without selecting a valid option."
+ )
+ }
+ }
+ }
+}
+
+impl From<std::io::Error> for Error {
+ fn from(value: std::io::Error) -> Self {
+ Self::IOError(value.kind())
+ }
+}
+
+impl From<serde_json::Error> for Error {
+ fn from(value: serde_json::Error) -> Self {
+ Self::SerdeError(value.to_string())
+ }
+}
+
+impl From<rust_format::Error> for Error {
+ fn from(value: rust_format::Error) -> Self {
+ Self::FormatError(value.to_string())
+ }
+}
+
+impl From<&'static str> for Error {
+ fn from(value: &'static str) -> Self {
+ Self::String(value.to_owned())
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Message {
+ ToggleTheme,
+ CopyCode,
+ SwitchPage(DesignerPage),
+ EditorAction(text_editor::Action),
+ RefreshEditorContent,
+ DropNewElement(ElementName, iced::Point, iced::Rectangle),
+ HandleNew(
+ ElementName,
+ Vec<(iced::advanced::widget::Id, iced::Rectangle)>,
+ ),
+ MoveElement(RenderedElement, iced::Point, iced::Rectangle),
+ HandleMove(
+ RenderedElement,
+ Vec<(iced::advanced::widget::Id, iced::Rectangle)>,
+ ),
+ PaneResized(pane_grid::ResizeEvent),
+ PaneClicked(pane_grid::Pane),
+ PaneDragged(pane_grid::DragEvent),
+ NewFile,
+ OpenFile,
+ FileOpened(Result<(PathBuf, Project), Error>),
+ SaveFile,
+ FileSaved(Result<PathBuf, Error>),
+}
diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs
new file mode 100644
index 0000000..fc7f18c
--- /dev/null
+++ b/iced_builder/src/main.rs
@@ -0,0 +1,415 @@
+use std::path::PathBuf;
+
+use iced::{
+ advanced::widget::Id,
+ clipboard, highlighter, keyboard,
+ widget::{
+ button, column, container,
+ pane_grid::{self, Pane, PaneGrid},
+ row, text, text_editor, themer, tooltip, Column, Space,
+ },
+ Alignment, Element, Font, Length, Settings, Task, Theme,
+};
+use iced_builder::types::{
+ element_name::ElementName, project::Project, rendered_element::ActionKind, DesignerPage,
+};
+use iced_builder::Message;
+use iced_drop::droppable;
+
+fn main() -> iced::Result {
+ iced::application(App::title, App::update, App::view)
+ .settings(Settings {
+ fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()],
+ ..Settings::default()
+ })
+ .theme(App::theme)
+ .subscription(App::subscription)
+ .run_with(App::new)
+}
+
+struct App {
+ is_dirty: bool,
+ is_loading: bool,
+ project_path: Option<PathBuf>,
+ project: Project,
+ dark_theme: bool,
+ pane_state: pane_grid::State<Panes>,
+ focus: Option<Pane>,
+ designer_page: DesignerPage,
+ element_list: Vec<ElementName>,
+ editor_content: text_editor::Content,
+}
+
+#[derive(Clone, Copy, Debug)]
+enum Panes {
+ Designer,
+ ElementList,
+}
+
+impl App {
+ fn new() -> (Self, Task<Message>) {
+ let state = pane_grid::State::with_configuration(pane_grid::Configuration::Split {
+ axis: pane_grid::Axis::Vertical,
+ ratio: 0.8,
+ a: Box::new(pane_grid::Configuration::Pane(Panes::Designer)),
+ b: Box::new(pane_grid::Configuration::Pane(Panes::ElementList)),
+ });
+ (
+ Self {
+ is_dirty: false,
+ is_loading: false,
+ project_path: None,
+ project: Project::new(),
+ dark_theme: true,
+ pane_state: state,
+ focus: None,
+ designer_page: DesignerPage::Designer,
+ element_list: ElementName::ALL.to_vec(),
+ editor_content: text_editor::Content::new(),
+ },
+ Task::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ let saved_state = if !self.is_dirty { "" } else { " *" };
+
+ let project_name = match &self.project.title {
+ Some(n) => {
+ format!(
+ " - {}",
+ if n.len() > 60 {
+ format!("...{}", &n[n.len() - 40..])
+ } else {
+ n.to_owned()
+ }
+ )
+ }
+ None => "".to_owned(),
+ };
+
+ format!("iced Builder{project_name}{saved_state}")
+ }
+
+ fn theme(&self) -> iced::Theme {
+ if self.dark_theme {
+ Theme::SolarizedDark
+ } else {
+ Theme::SolarizedLight
+ }
+ }
+
+ fn update(&mut self, message: Message) -> Task<Message> {
+ match message {
+ Message::ToggleTheme => self.dark_theme = !self.dark_theme,
+ Message::CopyCode => return clipboard::write(self.editor_content.text()),
+ Message::SwitchPage(page) => self.designer_page = page,
+ Message::EditorAction(action) => {
+ if let text_editor::Action::Scroll { lines: _ } = action {
+ self.editor_content.perform(action);
+ }
+ }
+ Message::RefreshEditorContent => {
+ let code = self
+ .project
+ .clone()
+ .app_code()
+ .unwrap_or_else(|err| err.to_string());
+ self.editor_content = text_editor::Content::with_text(&code);
+ }
+ Message::DropNewElement(name, point, _) => {
+ return iced_drop::zones_on_point(
+ move |zones| Message::HandleNew(name.clone(), zones),
+ point,
+ 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 = ActionKind::new(ids, &mut self.project.content.clone(), None);
+ let result = name.handle_action(self.project.content.as_mut(), action);
+ if let Ok(Some(ref element)) = result {
+ self.project.content = Some(element.clone());
+ }
+ println!("{:?}", result);
+ }
+
+ return Task::done(Message::RefreshEditorContent);
+ }
+ Message::MoveElement(element, point, _) => {
+ return iced_drop::zones_on_point(
+ move |zones| Message::HandleMove(element.clone(), zones),
+ point,
+ None,
+ None,
+ )
+ .into()
+ }
+ Message::HandleMove(element, zones) => {
+ let ids: Vec<Id> = zones.into_iter().map(|z| z.0).collect();
+ if ids.len() > 0 {
+ let action = ActionKind::new(
+ ids,
+ &mut self.project.content.clone(),
+ Some(element.get_id()),
+ );
+ let result = element.handle_action(self.project.content.as_mut(), action);
+
+ println!("{result:?}");
+ }
+
+ return Task::done(Message::RefreshEditorContent);
+ }
+ Message::PaneResized(pane_grid::ResizeEvent { split, ratio }) => {
+ self.pane_state.resize(split, ratio);
+ }
+ Message::PaneClicked(pane) => {
+ self.focus = Some(pane);
+ }
+ Message::PaneDragged(pane_grid::DragEvent::Dropped { pane, target }) => {
+ self.pane_state.drop(pane, target);
+ }
+ Message::PaneDragged(_) => {}
+ Message::NewFile => {
+ if !self.is_loading {
+ self.project = Project::new();
+ self.project_path = None;
+ self.editor_content = text_editor::Content::new();
+ }
+ }
+ Message::OpenFile => {
+ if !self.is_loading {
+ self.is_loading = true;
+
+ return Task::perform(Project::from_file(), Message::FileOpened);
+ }
+ }
+ Message::FileOpened(result) => {
+ self.is_loading = false;
+ self.is_dirty = false;
+
+ if let Ok((path, project)) = result {
+ self.project = project.clone();
+ self.project_path = Some(path);
+ self.editor_content = text_editor::Content::with_text(
+ &project.app_code().unwrap_or_else(|err| err.to_string()),
+ );
+ }
+ }
+ Message::SaveFile => {
+ if !self.is_loading {
+ self.is_loading = true;
+
+ return Task::perform(
+ self.project
+ .clone()
+ .write_to_file(self.project_path.clone()),
+ Message::FileSaved,
+ );
+ }
+ }
+ Message::FileSaved(result) => {
+ self.is_loading = false;
+
+ if let Ok(path) = result {
+ self.project_path = Some(path);
+ self.is_dirty = false;
+ }
+ }
+ }
+
+ Task::none()
+ }
+
+ fn subscription(&self) -> iced::Subscription<Message> {
+ keyboard::on_key_press(|key, modifiers| match key.as_ref() {
+ keyboard::Key::Character("o") if modifiers.command() => Some(Message::OpenFile),
+ keyboard::Key::Character("s") if modifiers.command() => Some(Message::SaveFile),
+ keyboard::Key::Character("n") if modifiers.command() => Some(Message::NewFile),
+ _ => None,
+ })
+ }
+
+ fn view(&self) -> Element<Message> {
+ let header = row![button("Toggle Theme")
+ .on_press(Message::ToggleTheme)
+ .padding(5)]
+ .width(200);
+ let pane_grid = PaneGrid::new(&self.pane_state, |id, pane, _is_maximized| {
+ let is_focused = Some(id) == self.focus;
+ match pane {
+ Panes::Designer => match &self.designer_page {
+ DesignerPage::Designer => {
+ let el_tree = match self.project.content.clone() {
+ Some(tree) => tree.as_element(),
+ None => text("Open a project or begin creating one").into(),
+ };
+ let content = container(themer(self.project.get_theme(), el_tree))
+ .id(iced::widget::container::Id::new("drop_zone"))
+ .height(Length::Fill)
+ .width(Length::Fill);
+ let title = row![
+ text("Designer"),
+ Space::with_width(Length::Fill),
+ button("Switch to Code view")
+ .on_press(Message::SwitchPage(DesignerPage::CodeView)),
+ ]
+ .align_y(Alignment::Center);
+ let title_bar = pane_grid::TitleBar::new(title)
+ .padding(10)
+ .style(style::title_bar);
+ pane_grid::Content::new(content)
+ .title_bar(title_bar)
+ .style(if is_focused {
+ style::pane_focused
+ } else {
+ style::pane_active
+ })
+ }
+ DesignerPage::CodeView => {
+ let title = row![
+ text("Generated Code"),
+ Space::with_width(Length::Fill),
+ tooltip(
+ button(
+ container(
+ text('\u{0e801}').font(Font::with_name("editor-icons"))
+ )
+ .center_x(30)
+ )
+ .on_press(Message::CopyCode),
+ "Copy code to clipboard",
+ tooltip::Position::FollowCursor
+ ),
+ Space::with_width(20),
+ button("Switch to Designer view")
+ .on_press(Message::SwitchPage(DesignerPage::Designer))
+ ]
+ .align_y(Alignment::Center);
+ let title_bar = pane_grid::TitleBar::new(title)
+ .padding(10)
+ .style(style::title_bar);
+ pane_grid::Content::new(
+ text_editor(&self.editor_content)
+ .on_action(Message::EditorAction)
+ .highlight(
+ "rs",
+ if self.dark_theme {
+ highlighter::Theme::SolarizedDark
+ } else {
+ highlighter::Theme::InspiredGitHub
+ },
+ )
+ .height(Length::Fill)
+ .padding(20),
+ )
+ .title_bar(title_bar)
+ .style(if is_focused {
+ style::pane_focused
+ } else {
+ style::pane_active
+ })
+ }
+ },
+ Panes::ElementList => {
+ let items_list = items_list_view(self.element_list.clone());
+ let content = column![items_list]
+ .align_x(Alignment::Center)
+ .height(Length::Fill)
+ .width(Length::Fill);
+ let title = text("Element List");
+ let title_bar = pane_grid::TitleBar::new(title)
+ .padding(10)
+ .style(style::title_bar);
+ pane_grid::Content::new(content)
+ .title_bar(title_bar)
+ .style(if is_focused {
+ style::pane_focused
+ } else {
+ style::pane_active
+ })
+ }
+ }
+ })
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .spacing(10)
+ .on_resize(10, Message::PaneResized)
+ .on_click(Message::PaneClicked)
+ .on_drag(Message::PaneDragged);
+
+ let content = Column::new()
+ .push(header)
+ .push(pane_grid)
+ .spacing(5)
+ .align_x(Alignment::Center)
+ .width(Length::Fill);
+
+ container(content).height(Length::Fill).into()
+ }
+}
+
+fn items_list_view<'a>(items: Vec<ElementName>) -> Element<'a, Message> {
+ let mut column = Column::new()
+ .spacing(20)
+ .align_x(Alignment::Center)
+ .width(Length::Fill);
+
+ for item in items {
+ column = column.push(
+ droppable(text(item.clone().to_string()))
+ .on_drop(move |point, rect| Message::DropNewElement(item.clone(), point, rect)),
+ );
+ }
+
+ container(column)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
+}
+
+mod style {
+ use iced::widget::container::Style;
+ use iced::{Border, Theme};
+
+ pub fn title_bar(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ Style {
+ text_color: Some(palette.background.strong.text),
+ background: Some(palette.background.strong.color.into()),
+ ..Default::default()
+ }
+ }
+
+ pub fn pane_active(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ Style {
+ background: Some(palette.background.weak.color.into()),
+ border: Border {
+ width: 1.0,
+ color: palette.background.strong.color,
+ ..Border::default()
+ },
+ ..Default::default()
+ }
+ }
+
+ pub fn pane_focused(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ Style {
+ background: Some(palette.background.weak.color.into()),
+ border: Border {
+ width: 4.0,
+ color: palette.background.strong.color,
+ ..Border::default()
+ },
+ ..Default::default()
+ }
+ }
+}
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![]),
+ }
+}