diff options
| author | pml68 <contact@pml68.me> | 2024-10-04 00:44:02 +0200 |
|---|---|---|
| committer | pml68 <contact@pml68.me> | 2024-10-04 00:44:02 +0200 |
| commit | 510d68b92972b99868e187dd5340f04780b4c354 (patch) | |
| tree | 6d0937824d8606423b5afef2a16e182a3a984f8f /iced_builder | |
| parent | feat: implement fmt::Display for RenderedElement, work on props (diff) | |
| download | iced-builder-510d68b92972b99868e187dd5340f04780b4c354.tar.gz | |
feat: update to iced 0.13.1, basic project state file, prepare for drag&drop
Diffstat (limited to 'iced_builder')
| -rw-r--r-- | iced_builder/Cargo.toml | 5 | ||||
| -rw-r--r-- | iced_builder/src/codegen/mod.rs | 122 | ||||
| -rw-r--r-- | iced_builder/src/lib.rs | 85 | ||||
| -rw-r--r-- | iced_builder/src/main.rs | 292 | ||||
| -rw-r--r-- | iced_builder/src/types/mod.rs | 10 | ||||
| -rw-r--r-- | iced_builder/src/types/project/mod.rs | 59 | ||||
| -rw-r--r-- | iced_builder/src/types/rendered_element.rs | 124 |
7 files changed, 471 insertions, 226 deletions
diff --git a/iced_builder/Cargo.toml b/iced_builder/Cargo.toml index 8106b09..a7b1e7b 100644 --- a/iced_builder/Cargo.toml +++ b/iced_builder/Cargo.toml @@ -9,12 +9,13 @@ license = "GPL-3.0-or-later" keywords = ["gui", "iced"] [dependencies] -iced = { version = "0.12.1", features = [ "image","svg","canvas","qr_code","advanced","tokio","highlighter"] } -iced_aw = { version = "0.9.3", default-features = false, features = ["menu","color_picker"] } +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" diff --git a/iced_builder/src/codegen/mod.rs b/iced_builder/src/codegen/mod.rs index 927b6e4..38e8ec3 100644 --- a/iced_builder/src/codegen/mod.rs +++ b/iced_builder/src/codegen/mod.rs @@ -1,8 +1,12 @@ use rust_format::{Config, Edition, Formatter, RustFmt}; -use crate::types::{ - rendered_element::{container, row, svg, text, RenderedElement}, - ElementName, +use crate::{ + types::{ + project::Project, + rendered_element::{container, row, svg, text, RenderedElement}, + ElementName, + }, + Error, }; impl RenderedElement { @@ -10,8 +14,8 @@ impl RenderedElement { let mut props_string = String::new(); for (k, v) in self.props.clone() { - if let Some(value) = v { - props_string = format!("{props_string}.{k}({value})"); + if let Some(v) = v { + props_string = format!("{props_string}.{k}({v})"); } } @@ -81,70 +85,62 @@ impl RenderedElement { (imports, view) } - pub fn app_code( - &self, - title: &str, - theme: Option<iced::Theme>, - ) -> Result<String, Box<dyn std::error::Error>> { - let (imports, view) = self.codegen(); - let mut app_code = format!("use iced::{{widget::{{{imports}}},Sandbox,Settings,Element}};"); - - app_code = format!( - r#"// Automatically generated by iced Builder - {app_code} - - fn main() -> iced::Result {{ - App::run(Settings::default()) - }} - - struct App; - - impl Sandbox for App {{ - type Message = (); - - fn new() -> Self {{ - Self {{}} - }} - - fn title(&self) -> String {{ - "{title}".into() - }} - - fn theme(&self) -> iced::Theme {{ - iced::Theme::{} - }} - - fn update(&mut self, message: Self::Message) {{ - - }} - - fn view(&self) -> Element<Self::Message> {{ - {view}.into() - }} - }}"#, - if let Some(c) = theme { - c.to_string().replace(' ', "") - } else { - "default()".to_owned() - } - ); - 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)?) - } - pub fn test() -> RenderedElement { - let text1 = text("wow").option("height", "120.5").option("width", "230"); + let mut text1 = text("wow"); + text1.option("height", "120.5"); + text1.option("width", "230"); - let element = container(row(vec![ + 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 Project { + 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/lib.rs b/iced_builder/src/lib.rs index e69de29..971e0e3 100644 --- a/iced_builder/src/lib.rs +++ b/iced_builder/src/lib.rs @@ -0,0 +1,85 @@ +pub mod codegen; +pub mod types; + +use std::path::PathBuf; + +use iced::widget::{pane_grid, text_editor}; +use types::{project::Project, rendered_element::RenderedElement, DesignerPage}; + +#[derive(Debug, Clone)] +pub enum Error { + IOError(std::io::ErrorKind), + SerdeError(String), + FormatError(String), + 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::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), + DropNewElement(types::ElementName, iced::Point, iced::Rectangle), + HandleNew( + types::ElementName, + Vec<(iced::advanced::widget::Id, iced::Rectangle)>, + ), + MoveElement(RenderedElement, iced::Point, iced::Rectangle), + HandleMove( + RenderedElement, + Vec<(iced::advanced::widget::Id, iced::Rectangle)>, + ), + Resized(pane_grid::ResizeEvent), + Clicked(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 index f0f58cc..aeb5ea6 100644 --- a/iced_builder/src/main.rs +++ b/iced_builder/src/main.rs @@ -1,67 +1,50 @@ -mod codegen; -mod types; +use std::path::PathBuf; use iced::{ - clipboard, executor, - highlighter::{self, Highlighter}, - theme, + clipboard, highlighter, keyboard, widget::{ button, column, container, pane_grid::{self, Pane, PaneGrid}, row, text, text_editor, tooltip, Column, Space, }, - Alignment, Application, Color, Command, Element, Font, Length, Settings, + Alignment, Element, Font, Length, Settings, Task, Theme, }; +use iced_builder::types::{project::Project, DesignerPage, ElementName}; +use iced_builder::Message; use iced_drop::droppable; -use types::{rendered_element::RenderedElement, DesignerPage, DesignerState}; fn main() -> iced::Result { - App::run(Settings { - fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], - ..Settings::default() - }) + 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_saved: bool, - current_project: Option<String>, + 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_state: DesignerState, - element_list: Vec<types::ElementName>, + designer_page: DesignerPage, + element_list: Vec<ElementName>, editor_content: text_editor::Content, } -#[derive(Debug, Clone)] -enum Message { - ToggleTheme, - CopyCode, - SwitchPage(DesignerPage), - EditorAction(text_editor::Action), - Drop(types::ElementName, iced::Point, iced::Rectangle), - HandleZones( - types::ElementName, - Vec<(iced::advanced::widget::Id, iced::Rectangle)>, - ), - Resized(pane_grid::ResizeEvent), - Clicked(pane_grid::Pane), - PaneDragged(pane_grid::DragEvent), -} - #[derive(Clone, Debug)] enum Panes { Designer, ElementList, } -impl Application for App { - type Message = Message; - type Theme = theme::Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command<Message>) { +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, @@ -70,26 +53,25 @@ impl Application for App { }); ( Self { - is_saved: true, - current_project: None, + is_dirty: false, + is_loading: false, + project_path: None, + project: Project::new(), dark_theme: true, pane_state: state, focus: None, - designer_state: DesignerState { - designer_content: Some(RenderedElement::test()), - designer_page: DesignerPage::Designer, - }, - element_list: types::ElementName::ALL.to_vec(), + designer_page: DesignerPage::Designer, + element_list: ElementName::ALL.to_vec(), editor_content: text_editor::Content::new(), }, - Command::none(), + Task::none(), ) } fn title(&self) -> String { - let saved_state = if self.is_saved { "" } else { " *" }; + let saved_state = if !self.is_dirty { "" } else { " *" }; - let project_name = match &self.current_project { + let project_name = match &self.project.title { Some(n) => { format!( " - {}", @@ -108,17 +90,17 @@ impl Application for App { fn theme(&self) -> iced::Theme { if self.dark_theme { - theme::Theme::CatppuccinMocha + Theme::SolarizedDark } else { - theme::Theme::CatppuccinLatte + Theme::SolarizedLight } } - fn update(&mut self, message: Message) -> Command<Message> { + 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_state.designer_page = page, + Message::SwitchPage(page) => self.designer_page = page, Message::EditorAction(action) => { if let text_editor::Action::Scroll { lines: _ } = action { self.editor_content.perform(action); @@ -130,38 +112,103 @@ impl Application for App { Message::Clicked(pane) => { self.focus = Some(pane); } - Message::Drop(name, point, _) => { + Message::DropNewElement(name, point, _) => { return iced_drop::zones_on_point( - move |zones| Message::HandleZones(name.clone(), zones), + move |zones| Message::HandleNew(name.clone(), zones), point, None, None, ) .into() } - Message::HandleZones(name, zones) => { - println!("{:?}\n{name}", zones); - println!("{:?}\n{name}\n{:?}", zones, self.title()); - if let Some(el) = &self.designer_state.designer_content { - self.editor_content = text_editor::Content::with_text( - &el.app_code( - match &self.current_project { - Some(title) => &title, - None => "New App", - }, - None, - ) - .unwrap(), - ); - } + Message::HandleNew(name, zones) => { + println!("\n\n{:?}\n{name}\n{:?}", zones, self.title()); + let code = self + .project + .clone() + .app_code() + .unwrap_or_else(|err| err.to_string()); + self.editor_content = text_editor::Content::with_text(&code); + } + 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) => { + println!( + "\n\n{:?}\n{element:0.4}", + zones + .into_iter() + .map(|c| c.0) + .collect::<Vec<iced::advanced::widget::Id>>() + ); } 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; + } + } } - Command::none() + 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), + _ => None, + }) } fn view(&self) -> Element<Message> { @@ -172,28 +219,23 @@ impl Application for App { 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_state.designer_page { + Panes::Designer => match &self.designer_page { DesignerPage::Designer => { - let content = container(text(format!( - "{:0.4}", - self.designer_state - .designer_content - .clone() - .expect("Designer content hasn't been set yet."), - ))) - .id(iced::widget::container::Id::new("drop_zone")) - .height(Length::Fill) - .width(Length::Fill); + 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(el_tree) + .id(iced::widget::container::Id::new("drop_zone")) + .height(Length::Fill) + .width(Length::Fill); let title = row![ - text("Designer").style(if is_focused { - PANE_ID_COLOR_FOCUSED - } else { - PANE_ID_COLOR_UNFOCUSED - }), + 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); @@ -207,44 +249,37 @@ impl Application for App { } DesignerPage::CodeView => { let title = row![ - text("Generated Code").style(if is_focused { - PANE_ID_COLOR_FOCUSED - } else { - PANE_ID_COLOR_UNFOCUSED - }), + text("Generated Code"), Space::with_width(Length::Fill), tooltip( button( container( text('\u{0e801}').font(Font::with_name("editor-icons")) ) - .width(30) - .center_x() + .center_x(30) ) .on_press(Message::CopyCode), "Copy code to clipboard", - tooltip::Position::Left + 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::<Highlighter>( - highlighter::Settings { - theme: if self.dark_theme { - highlighter::Theme::Base16Mocha - } else { - highlighter::Theme::InspiredGitHub - }, - extension: "rs".to_string(), + .highlight( + "rs", + if self.dark_theme { + highlighter::Theme::SolarizedDark + } else { + highlighter::Theme::InspiredGitHub }, - |highlight, _theme| highlight.to_format(), ) .height(Length::Fill) .padding(20), @@ -260,14 +295,10 @@ impl Application for App { Panes::ElementList => { let items_list = items_list_view(self.element_list.clone()); let content = column![items_list] - .align_items(Alignment::Center) + .align_x(Alignment::Center) .height(Length::Fill) .width(Length::Fill); - let title = text("Element List").style(if is_focused { - PANE_ID_COLOR_FOCUSED - } else { - PANE_ID_COLOR_UNFOCUSED - }); + let title = text("Element List"); let title_bar = pane_grid::TitleBar::new(title) .padding(10) .style(style::title_bar); @@ -292,39 +323,24 @@ impl Application for App { .push(header) .push(pane_grid) .spacing(5) - .align_items(Alignment::Center) + .align_x(Alignment::Center) .width(Length::Fill); container(content).height(Length::Fill).into() } } -const fn from_grayscale(grayscale: f32) -> Color { - Color { - r: grayscale, - g: grayscale, - b: grayscale, - a: 1.0, - } -} - -// #ffffff -const PANE_ID_COLOR_FOCUSED: Color = from_grayscale(1.0); - -// #e8e8e8 -const PANE_ID_COLOR_UNFOCUSED: Color = from_grayscale(0xE8 as f32 / 255.0); - -fn items_list_view(items: Vec<types::ElementName>) -> Element<'static, Message> { +fn items_list_view<'a>(items: Vec<ElementName>) -> Element<'a, Message> { let mut column = Column::new() .spacing(20) - .align_items(Alignment::Center) + .align_x(Alignment::Center) .width(Length::Fill); for item in items { let value = item.clone(); column = column.push( droppable(text(value.to_string())) - .on_drop(move |point, rect| Message::Drop(value.clone(), point, rect)), + .on_drop(move |point, rect| Message::DropNewElement(value.clone(), point, rect)), ); } @@ -332,23 +348,23 @@ fn items_list_view(items: Vec<types::ElementName>) -> Element<'static, Message> } mod style { - use iced::widget::container; - use iced::{Border, Theme}; + use iced::widget::{container::Style as CStyle, text::Style as TStyle}; + use iced::{color, Border, Theme}; - pub fn title_bar(theme: &Theme) -> container::Appearance { + pub fn title_bar(theme: &Theme) -> CStyle { let palette = theme.extended_palette(); - container::Appearance { + CStyle { text_color: Some(palette.background.strong.text), background: Some(palette.background.strong.color.into()), ..Default::default() } } - pub fn pane_active(theme: &Theme) -> container::Appearance { + pub fn pane_active(theme: &Theme) -> CStyle { let palette = theme.extended_palette(); - container::Appearance { + CStyle { background: Some(palette.background.weak.color.into()), border: Border { width: 1.0, @@ -359,10 +375,10 @@ mod style { } } - pub fn pane_focused(theme: &Theme) -> container::Appearance { + pub fn pane_focused(theme: &Theme) -> CStyle { let palette = theme.extended_palette(); - container::Appearance { + CStyle { background: Some(palette.background.weak.color.into()), border: Border { width: 4.0, diff --git a/iced_builder/src/types/mod.rs b/iced_builder/src/types/mod.rs index 344f543..2d6cd8f 100644 --- a/iced_builder/src/types/mod.rs +++ b/iced_builder/src/types/mod.rs @@ -1,15 +1,9 @@ +pub mod project; pub mod rendered_element; -use rendered_element::RenderedElement; use serde::{Deserialize, Serialize}; -#[derive(Clone)] -pub struct DesignerState { - pub designer_content: Option<RenderedElement>, - pub designer_page: DesignerPage, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ElementName { Text(String), Button(String), diff --git a/iced_builder/src/types/project/mod.rs b/iced_builder/src/types/project/mod.rs new file mode 100644 index 0000000..557fa92 --- /dev/null +++ b/iced_builder/src/types/project/mod.rs @@ -0,0 +1,59 @@ +use std::path::{Path, PathBuf}; + +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 content: Option<RenderedElement>, +} + +impl Project { + pub fn new() -> Self { + Self { + title: None, + content: None, + } + } + + 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) + } +} diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs index 2d5c81a..967352b 100644 --- a/iced_builder/src/types/rendered_element.rs +++ b/iced_builder/src/types/rendered_element.rs @@ -1,11 +1,15 @@ use std::collections::HashMap; +use iced::advanced::widget::Id; +use iced::{widget, Element, Length}; use serde::{Deserialize, Serialize}; use unique_id::{string::StringGenerator, Generator}; +use crate::Message; + use super::ElementName; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RenderedElement { pub id: String, pub child_elements: Option<Vec<RenderedElement>>, @@ -34,27 +38,104 @@ impl RenderedElement { } } - fn preset_options(mut self, options: Vec<&str>) -> Self { - for opt in options { - self.props.insert(opt.to_owned(), None); + pub fn find_by_id(&mut self, id: Id) -> Option<&mut Self> { + if Id::new(self.id.clone()) == id.clone() { + println!(""); + 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; } - self } - pub fn push(mut self, element: RenderedElement) -> Self { - if let Some(els) = self.child_elements.as_mut() { - els.push(element); + 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()?.contains(child_element) { + return Some(self); + } else { + if let Some(child_elements) = self.child_elements.as_mut() { + for element in child_elements { + let element: Option<&mut Self> = element.find_parent(child_element); + if element.is_some() { + return element; + } + } + } + return None; + } } else { - self.child_elements = Some(vec![element]); + return None; + } + } + + pub fn remove(&mut self, element: &RenderedElement) { + let parent = self.find_parent(element); + if let Some(child_elements) = parent.unwrap().child_elements.as_mut() { + if let Some(index) = child_elements.iter().position(|x| x == element) { + child_elements.remove(index); + } + } + } + + pub fn push(&mut self, element: RenderedElement) { + if let Some(child_elements) = self.child_elements.as_mut() { + child_elements.push(element); + } + } + + 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, element); + } else { + child_elements.push(element); + } + } + } + + fn preset_options(mut self, options: Vec<&str>) -> Self { + for opt in options { + self.props.insert(opt.to_owned(), None); } self } - pub fn option(mut self, option: &'static str, value: &'static str) -> Self { + pub fn option(&mut self, option: &'static str, value: &'static str) { self.props .entry(option.to_owned()) .and_modify(|opt| *opt = Some(value.to_owned())); - self + } + + pub fn as_element(self) -> Element<'static, 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), + ) + .style(widget::container::bordered_box), + ) + .id(Id::new(self.id.clone())) + .on_drop(move |point, rect| Message::MoveElement(self.clone(), point, rect)) + .into() } } @@ -121,10 +202,23 @@ pub fn image(path: &str) -> RenderedElement { RenderedElement::new(ElementName::Image(path.to_owned())) } -pub fn container(content: RenderedElement) -> RenderedElement { - RenderedElement::from_vec(ElementName::Container, vec![content]) +pub fn container(content: Option<RenderedElement>) -> RenderedElement { + match content { + Some(el) => RenderedElement::from_vec(ElementName::Container, vec![el]), + None => RenderedElement::from_vec(ElementName::Container, vec![]), + } } -pub fn row(child_elements: Vec<RenderedElement>) -> RenderedElement { - RenderedElement::from_vec(ElementName::Row, child_elements) +pub fn row(child_elements: Option<Vec<RenderedElement>>) -> RenderedElement { + match child_elements { + Some(els) => RenderedElement::from_vec(ElementName::Row, els), + None => RenderedElement::from_vec(ElementName::Row, vec![]), + } +} + +pub fn column(child_elements: Option<Vec<RenderedElement>>) -> RenderedElement { + match child_elements { + Some(els) => RenderedElement::from_vec(ElementName::Column, els), + None => RenderedElement::from_vec(ElementName::Column, vec![]), + } } |
