summaryrefslogtreecommitdiff
path: root/iced_builder
diff options
context:
space:
mode:
authorpml68 <contact@pml68.me>2024-10-04 00:44:02 +0200
committerpml68 <contact@pml68.me>2024-10-04 00:44:02 +0200
commit510d68b92972b99868e187dd5340f04780b4c354 (patch)
tree6d0937824d8606423b5afef2a16e182a3a984f8f /iced_builder
parentfeat: implement fmt::Display for RenderedElement, work on props (diff)
downloadiced-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.toml5
-rw-r--r--iced_builder/src/codegen/mod.rs122
-rw-r--r--iced_builder/src/lib.rs85
-rw-r--r--iced_builder/src/main.rs292
-rw-r--r--iced_builder/src/types/mod.rs10
-rw-r--r--iced_builder/src/types/project/mod.rs59
-rw-r--r--iced_builder/src/types/rendered_element.rs124
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![]),
+ }
}