From f5c35e48c480355036778d26aacde498e5c15e68 Mon Sep 17 00:00:00 2001 From: pml68 Date: Sun, 22 Sep 2024 23:53:02 +0200 Subject: feat: restructure project, start drag and drop --- iced_builder/src/codegen/mod.rs | 158 ++++++++++++++ iced_builder/src/lib.rs | 0 iced_builder/src/main.rs | 326 +++++++++++++++++++++++++++++ iced_builder/src/types/mod.rs | 54 +++++ iced_builder/src/types/rendered_element.rs | 47 +++++ 5 files changed, 585 insertions(+) create mode 100644 iced_builder/src/codegen/mod.rs create mode 100644 iced_builder/src/lib.rs create mode 100644 iced_builder/src/main.rs create mode 100644 iced_builder/src/types/mod.rs create mode 100644 iced_builder/src/types/rendered_element.rs (limited to 'iced_builder/src') diff --git a/iced_builder/src/codegen/mod.rs b/iced_builder/src/codegen/mod.rs new file mode 100644 index 0000000..88c0c61 --- /dev/null +++ b/iced_builder/src/codegen/mod.rs @@ -0,0 +1,158 @@ +use rust_format::{Config, Edition, Formatter, RustFmt}; + +use crate::types::{rendered_element::RenderedElement, ElementName}; + +impl RenderedElement { + fn props_codegen(&self) -> String { + let mut props_string = String::new(); + + for (k, v) in self.props.clone() { + props_string = format!("{props_string}.{k}({v})"); + } + + props_string + } + + fn codegen(&self) -> (String, String) { + let mut imports = String::new(); + let mut view = String::new(); + let props = self.props_codegen(); + + let mut elements = String::new(); + + match self.name { + ElementName::Column | ElementName::Row | ElementName::Container => { + for element in &self.child_elements { + let (c_imports, children) = element.codegen(); + imports = format!("{imports}{c_imports}"); + elements = format!("{elements}{},", children); + } + } + _ => {} + } + + match &self.name { + ElementName::Container => { + imports = format!("{imports}container,"); + view = if self.child_elements.len() < 2 { + format!("{view}\ncontainer({elements}){props}") + } else { + format!("{view}\ncontainer(){props}") + }; + } + ElementName::Row => { + imports = format!("{imports}row,"); + view = format!("{view}\nrow![{elements}]{props}"); + } + ElementName::Column => { + imports = format!("{imports}column,"); + view = format!("{view}\ncolumn![{elements}]{props}"); + } + ElementName::Text(string) => { + imports = format!("{imports}text,"); + view = format!( + "{view}\ntext(\"{}\"){props}", + if *string == String::new() { + "New Text" + } else { + string + } + ); + } + ElementName::Button(string) => { + imports = format!("{imports}button,"); + view = format!( + "{view}\nbutton(\"{}\"){props}", + if *string == String::new() { + "New Button" + } else { + string + } + ); + } + ElementName::Image(path) => { + imports = format!("{imports}image,"); + view = format!("{view}\nimage(\"{path}\"){props}"); + } + ElementName::SVG(path) => { + imports = format!("{imports}svg,"); + view = format!("{view}\nsvg(\"{path}\"){props}"); + } + } + + (imports, view) + } + + pub fn app_code( + &self, + title: &str, + theme: Option, + ) -> Result> { + let (imports, view) = self.codegen(); + let mut app_code = format!("use iced::{{widget::{{{imports}}},Sandbox,Settings,Element}};"); + + app_code = format!( + r#"{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 {{ + {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() -> String { + let mut text1 = RenderedElement::new(ElementName::Text("wow")); + text1.set_property("height", "120.5"); + text1.set_property("width", "230"); + + let element = RenderedElement::new(ElementName::Container).push(RenderedElement::from_vec( + ElementName::Row, + vec![ + text1, + RenderedElement::new(ElementName::Text("heh")), + RenderedElement::new(ElementName::SVG( + "/mnt/drive_d/git/obs-website/src/lib/assets/bars-solid.svg", + )), + ], + )); + + element.app_code("new app", None).unwrap() + } +} diff --git a/iced_builder/src/lib.rs b/iced_builder/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs new file mode 100644 index 0000000..e966614 --- /dev/null +++ b/iced_builder/src/main.rs @@ -0,0 +1,326 @@ +mod codegen; +mod types; + +use iced::{ + clipboard, executor, + highlighter::{self, Highlighter}, + theme, + widget::{ + button, column, container, + pane_grid::{self, Pane, PaneGrid}, + row, text, text_editor, tooltip, Column, Space, + }, + Alignment, Application, Color, Command, Element, Font, Length, Settings, +}; +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() + }) +} + +struct App { + is_saved: bool, + current_project: Option, + dark_theme: bool, + pane_state: pane_grid::State, + focus: Option, + designer_state: DesignerState, + element_list: Vec, + editor_content: text_editor::Content, +} + +#[derive(Debug, Clone)] +enum Message { + ToggleTheme, + CopyCode, + 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) { + 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_saved: true, + current_project: None, + dark_theme: true, + pane_state: state, + focus: None, + designer_state: DesignerState { + designer_content: vec![], + designer_page: DesignerPage::Designer, + }, + element_list: types::ElementName::ALL.to_vec(), + editor_content: text_editor::Content::with_text(&RenderedElement::test()), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + let saved_state = if self.is_saved { "" } else { " *" }; + + let project_name = match &self.current_project { + Some(n) => format!(" - {n}"), + None => "".to_owned(), + }; + + format!("iced Builder{project_name}{saved_state}") + } + + fn theme(&self) -> iced::Theme { + if self.dark_theme { + theme::Theme::CatppuccinMocha + } else { + theme::Theme::CatppuccinLatte + } + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::ToggleTheme => self.dark_theme = !self.dark_theme, + Message::CopyCode => return clipboard::write(self.editor_content.text()), + Message::Resized(pane_grid::ResizeEvent { split, ratio }) => { + self.pane_state.resize(split, ratio); + } + Message::Clicked(pane) => { + self.focus = Some(pane); + } + Message::Drop(name, point, _) => { + return iced_drop::zones_on_point( + move |zones| Message::HandleZones(name, zones), + point, + None, + None, + ) + .into() + } + Message::HandleZones(name, zones) => { + println!("{:?}\n{name}", zones); + } + Message::PaneDragged(pane_grid::DragEvent::Dropped { pane, target }) => { + self.pane_state.drop(pane, target); + } + Message::PaneDragged(_) => {} + } + + Command::none() + } + + fn view(&self) -> Element { + let header = row![button("Toggle Theme") + .on_press(Message::ToggleTheme) + .padding(5)] + .width(200); + let pane_grid = PaneGrid::new(&self.pane_state, |id, pane, _is_maximized| { + let is_focused = Some(id) == self.focus; + match pane { + Panes::Designer => match self.designer_state.designer_page { + DesignerPage::Designer => { + let content = container("") + .id(iced::widget::container::Id::new("drop_zone")) + .height(Length::Fill) + .width(Length::Fill); + let title = text("Designer").style(if is_focused { + PANE_ID_COLOR_FOCUSED + } else { + PANE_ID_COLOR_UNFOCUSED + }); + let title_bar = pane_grid::TitleBar::new(title) + .padding(10) + .style(style::title_bar); + pane_grid::Content::new(content) + .title_bar(title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) + } + DesignerPage::CodeView => { + let title = row![ + text("Generated Code").style(if is_focused { + PANE_ID_COLOR_FOCUSED + } else { + PANE_ID_COLOR_UNFOCUSED + }), + Space::with_width(Length::Fill), + tooltip( + button( + container( + text('\u{0e801}').font(Font::with_name("editor-icons")) + ) + .width(30) + .center_x() + ) + .on_press(Message::CopyCode), + "Copy code to clipboard", + tooltip::Position::Left + ) + ]; + let title_bar = pane_grid::TitleBar::new(title) + .padding(10) + .style(style::title_bar); + pane_grid::Content::new( + text_editor(&self.editor_content) + .highlight::( + highlighter::Settings { + theme: highlighter::Theme::Base16Mocha, + extension: "rs".to_string(), + }, + |highlight, _theme| highlight.to_format(), + ) + .height(Length::Fill) + .padding(20), + ) + .title_bar(title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) + } + }, + Panes::ElementList => { + let items_list = items_list_view(self.element_list.clone()); + let content = column![items_list] + .align_items(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_bar = pane_grid::TitleBar::new(title) + .padding(10) + .style(style::title_bar); + pane_grid::Content::new(content) + .title_bar(title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) + } + } + }) + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .on_resize(10, Message::Resized) + .on_click(Message::Clicked) + .on_drag(Message::PaneDragged); + + let content = Column::new() + .push(header) + .push(pane_grid) + .spacing(5) + .align_items(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) -> Element<'static, Message> { + let mut column = Column::new() + .spacing(20) + .align_items(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, point, rect)), + ); + } + + container(column).height(250.0).width(300).into() +} + +mod style { + use iced::widget::container; + use iced::{Border, Theme}; + + pub fn title_bar(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + text_color: Some(palette.background.strong.text), + background: Some(palette.background.strong.color.into()), + ..Default::default() + } + } + + pub fn pane_active(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + background: Some(palette.background.weak.color.into()), + border: Border { + width: 1.0, + color: palette.background.strong.color, + ..Border::default() + }, + ..Default::default() + } + } + + pub fn pane_focused(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + background: Some(palette.background.weak.color.into()), + border: Border { + width: 4.0, + color: palette.background.strong.color, + ..Border::default() + }, + ..Default::default() + } + } +} diff --git a/iced_builder/src/types/mod.rs b/iced_builder/src/types/mod.rs new file mode 100644 index 0000000..db06ffa --- /dev/null +++ b/iced_builder/src/types/mod.rs @@ -0,0 +1,54 @@ +pub mod rendered_element; + +use rendered_element::RenderedElement; + +pub struct DesignerState { + pub designer_content: Vec, + pub designer_page: DesignerPage, +} + +#[derive(Debug, Clone, Copy)] +pub enum ElementName { + Text(&'static str), + Button(&'static str), + SVG(&'static str), + Image(&'static str), + Container, + Row, + Column, +} + +impl ElementName { + pub const ALL: [Self; 7] = [ + Self::Text(""), + Self::Button(""), + Self::SVG(""), + Self::Image(""), + Self::Container, + Self::Row, + Self::Column, + ]; +} + +impl std::fmt::Display for ElementName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Text(_) => "Text", + Self::Button(_) => "Button", + Self::SVG(_) => "SVG", + Self::Image(_) => "Image", + Self::Container => "Container", + Self::Row => "Row", + Self::Column => "Column", + } + ) + } +} + +pub enum DesignerPage { + Designer, + CodeView, +} diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs new file mode 100644 index 0000000..f05594d --- /dev/null +++ b/iced_builder/src/types/rendered_element.rs @@ -0,0 +1,47 @@ +use std::collections::HashMap; + +use unique_id::{string::StringGenerator, Generator}; + +use iced::advanced::widget::Id; + +use super::ElementName; + +#[derive(Debug)] +pub struct RenderedElement { + pub id: Id, + pub child_elements: Vec, + pub name: ElementName, + pub props: HashMap<&'static str, &'static str>, +} + +impl RenderedElement { + pub fn new(name: ElementName) -> Self { + let gen = StringGenerator::default(); + Self { + id: Id::new(gen.next_id()), + child_elements: vec![], + name, + props: HashMap::new(), + } + } + + pub fn from_vec(name: ElementName, child_elements: Vec) -> Self { + let gen = StringGenerator::default(); + Self { + id: Id::new(gen.next_id()), + child_elements, + name, + props: HashMap::new(), + } + } + + pub fn push(mut self, element: RenderedElement) -> Self { + self.child_elements.push(element); + self + } + + pub fn set_property(&mut self, prop: &'static str, value: &'static str) { + let prop_ref = self.props.entry(prop).or_insert(value); + *prop_ref = value; + } +} -- cgit v1.2.3