summaryrefslogtreecommitdiff
path: root/iced_builder/src
diff options
context:
space:
mode:
authorPolesznyák Márk László <116908301+pml68@users.noreply.github.com>2024-12-26 00:12:06 +0100
committerGitHub <noreply@github.com>2024-12-26 00:12:06 +0100
commit0ae3ec6cc9babcab39c76f023606229a151916ab (patch)
treee92d0109599622984b2c485cc020951da288cec3 /iced_builder/src
parentMerge pull request #3 from pml68/refactor/internal-restructuring (diff)
parentfeat: add `tip` widget helper from `hecrj/icebreaker` (diff)
downloadiced-builder-0ae3ec6cc9babcab39c76f023606229a151916ab.tar.gz
Merge pull request #4 from pml68/feat/playground
Playground done **for now**
Diffstat (limited to '')
-rw-r--r--iced_builder/src/dialogs.rs21
-rw-r--r--iced_builder/src/error.rs44
-rw-r--r--iced_builder/src/icon.rs23
-rw-r--r--iced_builder/src/lib.rs84
-rw-r--r--iced_builder/src/main.rs270
-rw-r--r--iced_builder/src/panes.rs (renamed from iced_builder/src/views/mod.rs)0
-rw-r--r--iced_builder/src/panes/code_view.rs (renamed from iced_builder/src/views/code_view.rs)25
-rw-r--r--iced_builder/src/panes/designer_view.rs (renamed from iced_builder/src/views/designer_view.rs)19
-rw-r--r--iced_builder/src/panes/element_list.rs (renamed from iced_builder/src/views/element_list.rs)25
-rw-r--r--iced_builder/src/panes/style.rs (renamed from iced_builder/src/views/style.rs)0
-rw-r--r--iced_builder/src/types.rs48
-rw-r--r--iced_builder/src/types/element_name.rs8
-rw-r--r--iced_builder/src/types/mod.rs9
-rw-r--r--iced_builder/src/types/project.rs26
-rwxr-xr-xiced_builder/src/types/rendered_element.rs154
-rw-r--r--iced_builder/src/widget.rs21
16 files changed, 497 insertions, 280 deletions
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![
+ RenderedElement::new(ElementName::Text(text.to_owned())).preset_options(&[
"size",
"line_height",
"width",
@@ -400,5 +475,8 @@ pub fn row(child_elements: Option<Vec<RenderedElement>>) -> RenderedElement {
}
pub fn column(child_elements: Option<Vec<RenderedElement>>) -> RenderedElement {
- RenderedElement::with(ElementName::Column, child_elements.unwrap_or_default())
+ RenderedElement::with(
+ ElementName::Column,
+ child_elements.unwrap_or_default(),
+ )
}
diff --git a/iced_builder/src/widget.rs b/iced_builder/src/widget.rs
new file mode 100644
index 0000000..ed2073a
--- /dev/null
+++ b/iced_builder/src/widget.rs
@@ -0,0 +1,21 @@
+use iced::widget::{container, text, tooltip};
+use iced::Element;
+
+pub mod tip {
+ pub use super::tooltip::Position;
+}
+
+pub fn tip<'a, Message: 'a>(
+ target: impl Into<Element<'a, Message>>,
+ tip: &'a str,
+ position: tip::Position,
+) -> Element<'a, Message> {
+ tooltip(
+ target,
+ container(text(tip).size(14))
+ .padding(5)
+ .style(container::rounded_box),
+ position,
+ )
+ .into()
+}