diff options
Diffstat (limited to 'iced_builder')
23 files changed, 570 insertions, 289 deletions
diff --git a/iced_builder/Cargo.toml b/iced_builder/Cargo.toml index d08d485..a1b41cc 100644 --- a/iced_builder/Cargo.toml +++ b/iced_builder/Cargo.toml @@ -3,24 +3,55 @@ name = "iced_builder" description = "GUI builder for iced, built with iced." version = "0.1.0" edition = "2021" -authors = ["pml68 <contact@pml68.me>"] +authors = ["pml68 <contact@pml68.dev>"] 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_aw = { version = "0.11.0", default-features = false, features = ["menu","color_picker"] } +iced_anim = { version = "0.1.4", features = ["derive", "serde"] } 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" +serde = { version = "1.0.216", features = ["derive"] } +serde_json = "1.0.133" +tokio = { version = "1.42.0", features = ["fs"] } +rfd = { version = "0.15.1", default-features = false, features = ["async-std", "gtk3"] } rust-format = "0.3.4" -unique_id = "0.1.5" -indexmap = { version = "2.6.0", features = ["serde"] } -thiserror = "1.0.65" +blob-uuid = "0.5.0" +thiserror = "2.0.6" + +[build-dependencies] +iced_fontello = "0.13.1" + +[target.'cfg(windows)'.build-dependencies] +embed-resource = "3.0.1" +windows_exe_info = "0.4" [[bin]] name = "iced-builder" path = "src/main.rs" + +[lints.rust] +missing_debug_implementations = "deny" +# missing_docs = "deny" +unsafe_code = "deny" +unused_results = "deny" + +[lints.clippy] +type-complexity = "allow" +semicolon_if_nothing_returned = "deny" +trivially-copy-pass-by-ref = "deny" +default_trait_access = "deny" +match-wildcard-for-single-variants = "deny" +redundant-closure-for-method-calls = "deny" +filter_map_next = "deny" +manual_let_else = "deny" +unused_async = "deny" +from_over_into = "deny" +needless_borrow = "deny" +new_without_default = "deny" +useless_conversion = "deny" + +[lints.rustdoc] +broken_intra_doc_links = "forbid" diff --git a/iced_builder/assets/windows/iced_builder.manifest b/iced_builder/assets/windows/iced_builder.manifest new file mode 100644 index 0000000..82039bf --- /dev/null +++ b/iced_builder/assets/windows/iced_builder.manifest @@ -0,0 +1,8 @@ +<?xml version='1.0' encoding='UTF-8' standalone='yes'?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" > + <asmv3:application> + <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"> + <dpiAwareness>PerMonitorV2, unaware</dpiAwareness> + </asmv3:windowsSettings> + </asmv3:application> +</assembly> diff --git a/iced_builder/assets/windows/iced_builder.rc b/iced_builder/assets/windows/iced_builder.rc new file mode 100644 index 0000000..7255b65 --- /dev/null +++ b/iced_builder/assets/windows/iced_builder.rc @@ -0,0 +1,3 @@ +#define RT_MANIFEST 24 + +1 RT_MANIFEST "iced_builder.manifest" diff --git a/iced_builder/build.rs b/iced_builder/build.rs new file mode 100644 index 0000000..438ce37 --- /dev/null +++ b/iced_builder/build.rs @@ -0,0 +1,12 @@ +fn main() { + println!("cargo::rerun-if-changed=fonts/icons.toml"); + iced_fontello::build("fonts/icons.toml").expect("Build icons font"); + #[cfg(windows)] + { + embed_resource::compile( + "assets/windows/iced_builder.rc", + embed_resource::NONE, + ); + windows_exe_info::versioninfo::link_cargo_env(); + } +} diff --git a/iced_builder/fonts/icons.toml b/iced_builder/fonts/icons.toml new file mode 100644 index 0000000..a70c0e7 --- /dev/null +++ b/iced_builder/fonts/icons.toml @@ -0,0 +1,6 @@ +module = "icon" + +[glyphs] +save = "entypo-floppy" +open = "fontawesome-folder-open-empty" +copy = "fontawesome-file-code" diff --git a/iced_builder/fonts/icons.ttf b/iced_builder/fonts/icons.ttf Binary files differindex 393c692..7af6b0e 100644 --- a/iced_builder/fonts/icons.ttf +++ b/iced_builder/fonts/icons.ttf diff --git a/iced_builder/rustfmt.toml b/iced_builder/rustfmt.toml new file mode 100644 index 0000000..197262a --- /dev/null +++ b/iced_builder/rustfmt.toml @@ -0,0 +1,4 @@ +edition = "2021" +imports_granularity = "Module" +group_imports = "StdExternalCrate" +max_width = 80 diff --git a/iced_builder/src/dialogs.rs b/iced_builder/src/dialogs.rs new file mode 100644 index 0000000..047ffd2 --- /dev/null +++ b/iced_builder/src/dialogs.rs @@ -0,0 +1,21 @@ +use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel}; + +pub fn error_dialog(description: impl Into<String>) { + let _ = MessageDialog::new() + .set_level(MessageLevel::Error) + .set_buttons(MessageButtons::Ok) + .set_title("Oops! Something went wrong.") + .set_description(description) + .show(); +} + +pub fn unsaved_changes_dialog( + description: impl Into<String>, +) -> MessageDialogResult { + MessageDialog::new() + .set_level(MessageLevel::Warning) + .set_buttons(MessageButtons::OkCancel) + .set_title("Unsaved changes") + .set_description(description) + .show() +} diff --git a/iced_builder/src/error.rs b/iced_builder/src/error.rs new file mode 100644 index 0000000..8876016 --- /dev/null +++ b/iced_builder/src/error.rs @@ -0,0 +1,44 @@ +use std::io; +use std::sync::Arc; + +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +#[error(transparent)] +pub enum Error { + IOError(Arc<io::Error>), + SerdeError(Arc<serde_json::Error>), + FormatError(Arc<rust_format::Error>), + #[error("The element tree contains no matching element")] + NonExistentElement, + #[error( + "The file dialog has been closed without selecting a valid option" + )] + DialogClosed, + #[error("{0}")] + Other(String), +} + +impl From<io::Error> for Error { + fn from(value: io::Error) -> Self { + Self::IOError(Arc::new(value)) + } +} + +impl From<serde_json::Error> for Error { + fn from(value: serde_json::Error) -> Self { + Self::SerdeError(Arc::new(value)) + } +} + +impl From<rust_format::Error> for Error { + fn from(value: rust_format::Error) -> Self { + Self::FormatError(Arc::new(value)) + } +} + +impl From<&str> for Error { + fn from(value: &str) -> Self { + Self::Other(value.to_owned()) + } +} diff --git a/iced_builder/src/icon.rs b/iced_builder/src/icon.rs new file mode 100644 index 0000000..f6760d5 --- /dev/null +++ b/iced_builder/src/icon.rs @@ -0,0 +1,23 @@ +// Generated automatically by iced_fontello at build time. +// Do not edit manually. Source: ../fonts/icons.toml +// 02c7558d187cdc056fdd0e6a638ef805fa10f5955f834575e51d75acd35bc70e +use iced::widget::{text, Text}; +use iced::Font; + +pub const FONT: &[u8] = include_bytes!("../fonts/icons.ttf"); + +pub fn copy<'a>() -> Text<'a> { + icon("\u{F1C9}") +} + +pub fn open<'a>() -> Text<'a> { + icon("\u{F115}") +} + +pub fn save<'a>() -> Text<'a> { + icon("\u{1F4BE}") +} + +fn icon<'a>(codepoint: &'a str) -> Text<'a> { + text(codepoint).font(Font::with_name("icons")) +} diff --git a/iced_builder/src/lib.rs b/iced_builder/src/lib.rs index 14a044e..f3165f5 100644 --- a/iced_builder/src/lib.rs +++ b/iced_builder/src/lib.rs @@ -1,79 +1,9 @@ +pub mod dialogs; +pub mod error; +pub mod icon; +pub mod panes; pub mod types; -pub mod views; +pub mod widget; -use std::path::PathBuf; - -use iced::widget::{pane_grid, text_editor}; -use types::{ - element_name::ElementName, project::Project, rendered_element::RenderedElement, DesignerPage, -}; - -use thiserror::Error; - -#[derive(Debug, Clone, Error)] -pub enum Error { - #[error("an IO error accured: {0}")] - IOError(String), - #[error("a Serde error accured: {0}")] - SerdeError(String), - #[error("an RustFmt error accured: {0}")] - FormatError(String), - #[error("the element tree contains no matching element")] - NonExistentElement, - #[error("the file dialog has been closed without selecting a valid option")] - DialogClosed, - #[error("{0}")] - String(String), -} - -impl From<std::io::Error> for Error { - fn from(value: std::io::Error) -> Self { - Self::IOError(value.to_string()) - } -} - -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, - SaveFileAs, - FileSaved(Result<PathBuf, Error>), -} +pub use error::Error; +pub type Result<T> = core::result::Result<T, Error>; diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs index ed3f264..a041c6f 100644 --- a/iced_builder/src/main.rs +++ b/iced_builder/src/main.rs @@ -1,28 +1,24 @@ use std::path::PathBuf; -use iced::{ - advanced::widget::Id, - clipboard, keyboard, - widget::{ - button, container, - pane_grid::{self, Pane, PaneGrid}, - row, text_editor, Column, - }, - Alignment, Element, Length, Settings, Task, Theme, -}; -use iced_builder::{ - types::{element_name::ElementName, project::Project, rendered_element::Action, DesignerPage}, - views::{code_view, designer_view, element_list}, - Message, +use iced::advanced::widget::Id; +use iced::widget::pane_grid::{self, Pane, PaneGrid}; +use iced::widget::{container, pick_list, row, text_editor, Column}; +use iced::{clipboard, keyboard, Alignment, Element, Length, Task, Theme}; +use iced_anim::{Animation, Spring}; +use iced_builder::dialogs::{error_dialog, unsaved_changes_dialog}; +use iced_builder::icon; +use iced_builder::panes::{code_view, designer_view, element_list}; +use iced_builder::types::{ + Action, DesignerPage, ElementName, Message, Project, }; +use rfd::MessageDialogResult; + +const THEMES: &'static [Theme] = &[Theme::SolarizedDark, Theme::SolarizedLight]; 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) + .font(icon::FONT) + .theme(|state| state.theme.value().clone()) .subscription(App::subscription) .run_with(App::new) } @@ -32,11 +28,11 @@ struct App { is_loading: bool, project_path: Option<PathBuf>, project: Project, - dark_theme: bool, + theme: Spring<Theme>, pane_state: pane_grid::State<Panes>, focus: Option<Pane>, designer_page: DesignerPage, - element_list: Vec<ElementName>, + element_list: &'static [ElementName], editor_content: text_editor::Content, } @@ -48,23 +44,25 @@ enum Panes { 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)), - }); + 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, + theme: Spring::new(Theme::SolarizedDark), pane_state: state, focus: None, - designer_page: DesignerPage::Designer, - element_list: ElementName::ALL.to_vec(), + designer_page: DesignerPage::DesignerView, + element_list: ElementName::ALL, editor_content: text_editor::Content::new(), }, Task::none(), @@ -91,18 +89,14 @@ impl App { 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::ToggleTheme(event) => { + self.theme.update(event); + } + 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 { @@ -110,12 +104,13 @@ impl App { } } 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); + match self.project.clone().app_code() { + Ok(code) => { + self.editor_content = + text_editor::Content::with_text(&code); + } + Err(error) => error_dialog(error.to_string()), + } } Message::DropNewElement(name, point, _) => { return iced_drop::zones_on_point( @@ -129,14 +124,26 @@ impl App { Message::HandleNew(name, zones) => { let ids: Vec<Id> = zones.into_iter().map(|z| z.0).collect(); if ids.len() > 0 { - let action = Action::new(ids, &mut self.project.element_tree.clone(), None); - let result = name.handle_action(self.project.element_tree.as_mut(), action); - if let Ok(Some(ref element)) = result { - self.project.element_tree = Some(element.clone()); + let action = Action::new( + ids, + &mut self.project.element_tree.clone(), + None, + ); + let result = name.handle_action( + self.project.element_tree.as_mut(), + action, + ); + match result { + Ok(Some(ref element)) => { + self.project.element_tree = Some(element.clone()) + } + Err(error) => error_dialog(error.to_string()), + _ => {} } - } - return Task::done(Message::RefreshEditorContent); + self.is_dirty = true; + return Task::done(Message::RefreshEditorContent); + } } Message::MoveElement(element, point, _) => { return iced_drop::zones_on_point( @@ -155,10 +162,17 @@ impl App { &mut self.project.element_tree.clone(), Some(element.get_id()), ); - let _ = element.handle_action(self.project.element_tree.as_mut(), action); - } + let result = element.handle_action( + self.project.element_tree.as_mut(), + action, + ); + if let Err(error) = result { + error_dialog(error.to_string()); + } - return Task::done(Message::RefreshEditorContent); + self.is_dirty = true; + return Task::done(Message::RefreshEditorContent); + } } Message::PaneResized(pane_grid::ResizeEvent { split, ratio }) => { self.pane_state.resize(split, ratio); @@ -166,34 +180,62 @@ impl App { Message::PaneClicked(pane) => { self.focus = Some(pane); } - Message::PaneDragged(pane_grid::DragEvent::Dropped { pane, target }) => { + 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(); + if !self.is_dirty { + self.project = Project::new(); + self.project_path = None; + self.editor_content = text_editor::Content::new(); + } else { + if let MessageDialogResult::Ok = unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and create a new project?") { + self.is_dirty = false; + 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; + if !self.is_dirty { + self.is_loading = true; - return Task::perform(Project::from_file(), Message::FileOpened); + return Task::perform( + Project::from_path(), + Message::FileOpened, + ); + } else { + if let MessageDialogResult::Ok = unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and open another project?") { + self.is_dirty = false; + self.is_loading = true; + return Task::perform(Project::from_path(), 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()), - ); + match result { + Ok((path, project)) => { + 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()), + ); + } + Err(error) => error_dialog(error.to_string()), } } Message::SaveFile => { @@ -221,9 +263,12 @@ impl App { Message::FileSaved(result) => { self.is_loading = false; - if let Ok(path) = result { - self.project_path = Some(path); - self.is_dirty = false; + match result { + Ok(path) => { + self.project_path = Some(path); + self.is_dirty = false; + } + Err(error) => error_dialog(error.to_string()), } } } @@ -232,47 +277,60 @@ impl App { } 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() => { - if modifiers.shift() { - Some(Message::SaveFileAs) - } else { - Some(Message::SaveFile) + keyboard::on_key_press(|key, modifiers| { + if modifiers.command() { + match key.as_ref() { + keyboard::Key::Character("o") => Some(Message::OpenFile), + keyboard::Key::Character("s") => { + Some(if modifiers.shift() { + Message::SaveFileAs + } else { + Message::SaveFile + }) + } + keyboard::Key::Character("n") => Some(Message::NewFile), + _ => None, } + } else { + None } - 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)] + fn view(&self) -> Element<'_, Message> { + let header = row![pick_list( + THEMES, + Some(self.theme.target().clone()), + |theme| { Message::ToggleTheme(theme.into()) } + )] .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 => designer_view::view( - &self.project.element_tree, - self.project.get_theme(), - is_focused, - ), - DesignerPage::CodeView => { - code_view::view(&self.editor_content, self.dark_theme, is_focused) + 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::DesignerView => designer_view::view( + &self.project.element_tree, + self.project.get_theme(), + is_focused, + ), + DesignerPage::CodeView => code_view::view( + &self.editor_content, + self.theme.value().clone(), + is_focused, + ), + }, + Panes::ElementList => { + element_list::view(self.element_list, is_focused) } - }, - Panes::ElementList => element_list::view(&self.element_list, is_focused), - } - }) - .width(Length::Fill) - .height(Length::Fill) - .spacing(10) - .on_resize(10, Message::PaneResized) - .on_click(Message::PaneClicked) - .on_drag(Message::PaneDragged); + } + }) + .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) @@ -281,6 +339,8 @@ impl App { .align_x(Alignment::Center) .width(Length::Fill); - container(content).height(Length::Fill).into() + Animation::new(&self.theme, container(content).height(Length::Fill)) + .on_update(Message::ToggleTheme) + .into() } } diff --git a/iced_builder/src/views/mod.rs b/iced_builder/src/panes.rs index 387662a..387662a 100644 --- a/iced_builder/src/views/mod.rs +++ b/iced_builder/src/panes.rs diff --git a/iced_builder/src/views/code_view.rs b/iced_builder/src/panes/code_view.rs index 4515687..fe7801c 100644 --- a/iced_builder/src/views/code_view.rs +++ b/iced_builder/src/panes/code_view.rs @@ -1,27 +1,26 @@ +use iced::widget::{button, pane_grid, row, text, text_editor, Space}; +use iced::{Alignment, Length, Theme}; use super::style; -use crate::{types::DesignerPage, Message}; -use iced::{ - highlighter, - widget::{button, container, pane_grid, row, text, text_editor, tooltip, Space}, - Alignment, Font, Length, -}; +use crate::icon::copy; +use crate::types::{DesignerPage, Message}; +use crate::widget::tip; pub fn view<'a>( editor_content: &'a text_editor::Content, - dark_theme: bool, + theme: Theme, is_focused: bool, ) -> pane_grid::Content<'a, Message> { 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), + tip( + button(copy()).on_press(Message::CopyCode), "Copy code to clipboard", - tooltip::Position::FollowCursor + tip::Position::FollowCursor ), Space::with_width(20), - button("Switch to Designer view").on_press(Message::SwitchPage(DesignerPage::Designer)) + button("Switch to Designer view") + .on_press(Message::SwitchPage(DesignerPage::DesignerView)) ] .align_y(Alignment::Center); let title_bar = pane_grid::TitleBar::new(title) @@ -32,7 +31,7 @@ pub fn view<'a>( .on_action(Message::EditorAction) .highlight( "rs", - if dark_theme { + if theme.to_string().contains("Dark") { highlighter::Theme::SolarizedDark } else { highlighter::Theme::InspiredGitHub diff --git a/iced_builder/src/views/designer_view.rs b/iced_builder/src/panes/designer_view.rs index 6f31a51..76456db 100644 --- a/iced_builder/src/views/designer_view.rs +++ b/iced_builder/src/panes/designer_view.rs @@ -1,20 +1,16 @@ +use iced::widget::{button, container, pane_grid, row, text, themer, Space}; +use iced::{Alignment, Element, Length}; + use super::style; -use crate::{ - types::{rendered_element::RenderedElement, DesignerPage}, - Message, -}; -use iced::{ - widget::{button, container, pane_grid, row, text, themer, Space}, - Alignment, Length, -}; +use crate::types::{DesignerPage, Message, RenderedElement}; pub fn view<'a>( element_tree: &Option<RenderedElement>, designer_theme: iced::Theme, is_focused: bool, ) -> pane_grid::Content<'a, Message> { - let el_tree = match element_tree { - Some(tree) => tree.clone().as_element(), + let el_tree: Element<'a, Message> = match element_tree { + Some(tree) => tree.clone().into(), None => text("Open a project or begin creating one").into(), }; let content = container(themer(designer_theme, el_tree)) @@ -24,7 +20,8 @@ pub fn view<'a>( let title = row![ text("Designer"), Space::with_width(Length::Fill), - button("Switch to Code view").on_press(Message::SwitchPage(DesignerPage::CodeView)), + button("Switch to Code view") + .on_press(Message::SwitchPage(DesignerPage::CodeView)), ] .align_y(Alignment::Center); let title_bar = pane_grid::TitleBar::new(title) diff --git a/iced_builder/src/views/element_list.rs b/iced_builder/src/panes/element_list.rs index f0fdd2f..74188af 100644 --- a/iced_builder/src/views/element_list.rs +++ b/iced_builder/src/panes/element_list.rs @@ -1,22 +1,23 @@ -use super::style; -use crate::{types::element_name::ElementName, Message}; -use iced::{ - widget::{column, container, pane_grid, text, Column}, - Alignment, Element, Length, -}; +use iced::widget::{column, container, pane_grid, text, Column}; +use iced::{Alignment, Element, Length}; use iced_drop::droppable; -fn items_list_view<'a>(items: &'a Vec<ElementName>) -> Element<'a, Message> { +use super::style; +use crate::types::{ElementName, Message}; + +fn items_list_view<'a>(items: &'a [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)), - ); + column = + column.push(droppable(text(item.clone().to_string())).on_drop( + move |point, rect| { + Message::DropNewElement(item.clone(), point, rect) + }, + )); } container(column) @@ -26,7 +27,7 @@ fn items_list_view<'a>(items: &'a Vec<ElementName>) -> Element<'a, Message> { } pub fn view<'a>( - element_list: &'a Vec<ElementName>, + element_list: &'a [ElementName], is_focused: bool, ) -> pane_grid::Content<'a, Message> { let items_list = items_list_view(element_list); diff --git a/iced_builder/src/views/style.rs b/iced_builder/src/panes/style.rs index 1eefb2d..1eefb2d 100644 --- a/iced_builder/src/views/style.rs +++ b/iced_builder/src/panes/style.rs diff --git a/iced_builder/src/types.rs b/iced_builder/src/types.rs new file mode 100644 index 0000000..161b5e1 --- /dev/null +++ b/iced_builder/src/types.rs @@ -0,0 +1,48 @@ +pub mod element_name; +pub mod project; +pub mod rendered_element; + +use std::path::PathBuf; + +pub use element_name::ElementName; +use iced::widget::{pane_grid, text_editor}; +use iced::Theme; +use iced_anim::SpringEvent; +pub use project::Project; +pub use rendered_element::*; + +use crate::Result; + +#[derive(Debug, Clone)] +pub enum Message { + ToggleTheme(SpringEvent<Theme>), + 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)>), + SaveFile, + SaveFileAs, + FileSaved(Result<PathBuf>), +} + +#[derive(Debug, Clone)] +pub enum DesignerPage { + DesignerView, + CodeView, +} diff --git a/iced_builder/src/types/element_name.rs b/iced_builder/src/types/element_name.rs index 93e12a1..e172227 100644 --- a/iced_builder/src/types/element_name.rs +++ b/iced_builder/src/types/element_name.rs @@ -1,10 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::Error; - use super::rendered_element::{ button, column, container, image, row, svg, text, Action, RenderedElement, }; +use crate::{Error, Result}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ElementName { @@ -18,7 +17,7 @@ pub enum ElementName { } impl ElementName { - pub const ALL: [Self; 7] = [ + pub const ALL: &'static [Self; 7] = &[ Self::Text(String::new()), Self::Button(String::new()), Self::SVG(String::new()), @@ -32,7 +31,7 @@ impl ElementName { &self, element_tree: Option<&mut RenderedElement>, action: Action, - ) -> Result<Option<RenderedElement>, Error> { + ) -> Result<Option<RenderedElement>> { let element = match self { Self::Text(_) => text(""), Self::Button(_) => button(""), @@ -44,6 +43,7 @@ impl ElementName { }; match action { Action::Stop => Ok(None), + Action::Drop => Ok(None), Action::AddNew => Ok(Some(element)), Action::PushFront(id) => { element_tree diff --git a/iced_builder/src/types/mod.rs b/iced_builder/src/types/mod.rs deleted file mode 100644 index a48a2d8..0000000 --- a/iced_builder/src/types/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -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 index 52da41c..f4dbcc4 100644 --- a/iced_builder/src/types/project.rs +++ b/iced_builder/src/types/project.rs @@ -1,12 +1,11 @@ -use rust_format::{Config, Edition, Formatter, RustFmt}; use std::path::{Path, PathBuf}; use iced::Theme; +use rust_format::{Config, Edition, Formatter, RustFmt}; use serde::{Deserialize, Serialize}; -use crate::Error; - use super::rendered_element::RenderedElement; +use crate::{Error, Result}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Project { @@ -55,10 +54,10 @@ impl Project { } } - pub async fn from_file() -> Result<(PathBuf, Self), Error> { + pub async fn from_path() -> Result<(PathBuf, Self)> { let picked_file = rfd::AsyncFileDialog::new() .set_title("Open a JSON file...") - .add_filter("*.JSON, *.json", &["JSON", "json"]) + .add_filter("*.json, *.JSON", &["json", "JSON"]) .pick_file() .await .ok_or(Error::DialogClosed)?; @@ -71,13 +70,13 @@ impl Project { Ok((path, element)) } - pub async fn write_to_file(self, path: Option<PathBuf>) -> Result<PathBuf, Error> { + pub async fn write_to_file(self, path: Option<PathBuf>) -> Result<PathBuf> { let path = if let Some(p) = path { p } else { rfd::AsyncFileDialog::new() .set_title("Save to JSON file...") - .add_filter("*.JSON, *.json", &["JSON", "json"]) + .add_filter("*.json, *.JSON", &["json", "JSON"]) .save_file() .await .as_ref() @@ -86,24 +85,25 @@ impl Project { .ok_or(Error::DialogClosed)? }; - let contents = serde_json::to_string(&self.clone())?; + let contents = serde_json::to_string(&self)?; tokio::fs::write(&path, contents).await?; Ok(path) } - pub fn app_code(&self) -> Result<String, Error> { + pub fn app_code(&self) -> Result<String> { match self.element_tree { Some(ref element_tree) => { let (imports, view) = element_tree.codegen(); - let mut app_code = format!("use iced::{{widget::{{{imports}}},Element}};"); + 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::application("{}", State::update, State::view).theme(iced::Theme::{}).run() + iced::application("{}", State::update, State::view).theme(State::theme).run() }} #[derive(Default)] @@ -115,6 +115,10 @@ impl Project { impl State {{ fn update(&mut self, _message: Message) {{}} + fn theme(&self) -> iced::Theme {{ + iced::Theme::{} + }} + fn view(&self) -> Element<Message> {{ {view}.into() }} diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs index 08d7ba3..d4d1a6c 100755 --- a/iced_builder/src/types/rendered_element.rs +++ b/iced_builder/src/types/rendered_element.rs @@ -1,40 +1,38 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; +use blob_uuid::random_blob; 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; +use super::ElementName; +use crate::types::Message; +use crate::Result; #[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>>, + child_elements: Option<Vec<RenderedElement>>, + name: ElementName, + options: BTreeMap<String, Option<String>>, } impl RenderedElement { fn new(name: ElementName) -> Self { - let gen = StringGenerator::default(); Self { - id: gen.next_id(), + id: random_blob(), child_elements: None, name, - options: IndexMap::new(), + options: BTreeMap::new(), } } fn with(name: ElementName, child_elements: Vec<RenderedElement>) -> Self { - let gen = StringGenerator::default(); Self { - id: gen.next_id(), + id: random_blob(), child_elements: Some(child_elements), name, - options: IndexMap::new(), + options: BTreeMap::new(), } } @@ -58,7 +56,10 @@ impl RenderedElement { } } - pub fn find_parent(&mut self, child_element: &RenderedElement) -> Option<&mut Self> { + 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() { @@ -94,9 +95,12 @@ impl RenderedElement { } 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); + 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); } } } @@ -109,7 +113,9 @@ impl RenderedElement { 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) { + 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()); @@ -121,17 +127,21 @@ impl RenderedElement { &self, element_tree: Option<&mut RenderedElement>, action: Action, - ) -> Result<(), Error> { + ) -> Result<()> { 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) => { - let old_parent = element_tree.find_parent(self).unwrap(); - old_parent.remove(self); + element_tree.remove(self); let new_parent = element_tree.find_by_id(id).unwrap(); new_parent.push_front(self); @@ -139,8 +149,7 @@ impl RenderedElement { Ok(()) } Action::InsertAfter(parent_id, target_id) => { - let old_parent = element_tree.find_parent(self).unwrap(); - old_parent.remove(self); + element_tree.remove(self); let new_parent = element_tree.find_by_id(parent_id).unwrap(); new_parent.insert_after(target_id, self); @@ -150,18 +159,19 @@ impl RenderedElement { } } - fn preset_options(mut self, options: Vec<&str>) -> Self { + fn preset_options<'a>(mut self, options: &[&'a str]) -> Self { for opt in options { - self.options.insert(opt.to_owned(), None); + let _ = self.options.insert(opt.to_string(), None); } self } - pub fn option<'a>(&mut self, option: &'a str, value: &'a str) -> Self { - self.options + 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.clone() + self } pub fn as_element<'a>(self) -> Element<'a, Message> { @@ -174,16 +184,21 @@ impl RenderedElement { } iced_drop::droppable( widget::container( - widget::column![widget::text(self.name.clone().to_string()), children] - .width(Length::Fill) - .spacing(10), + 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)) + .on_drop(move |point, rect| { + Message::MoveElement(self.clone(), point, rect) + }) .into() } @@ -299,11 +314,66 @@ impl std::fmt::Display for RenderedElement { } } +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_iter( + child_elements.into_iter().map(|el| el.into()), + ) + .padding(20) + .into(), + ElementName::Column => widget::Column::from_iter( + child_elements.into_iter().map(|el| el.into()), + ) + .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 { AddNew, PushFront(Id), InsertAfter(Id, Id), + Drop, Stop, } @@ -317,12 +387,16 @@ impl Action { 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()].clone(); - if ids.len() > 2 && ids[ids.clone().len() - 1] == element_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 @@ -337,7 +411,8 @@ impl Action { // Element IS a parent but ISN'T a non-empty container match element.is_parent() - && !(element.name == ElementName::Container && !element.is_empty()) + && !(element.name == ElementName::Container + && !element.is_empty()) { true => { action = Self::PushFront(id); @@ -368,7 +443,7 @@ impl Action { } pub fn text(text: &str) -> RenderedElement { - RenderedElement::new(ElementName::Text(text.to_owned())).preset_options(vec