summaryrefslogtreecommitdiff
path: root/iced_builder
diff options
context:
space:
mode:
Diffstat (limited to 'iced_builder')
-rw-r--r--iced_builder/Cargo.toml21
-rw-r--r--iced_builder/fonts/icons.ttfbin0 -> 6352 bytes
-rw-r--r--iced_builder/src/codegen/mod.rs158
-rw-r--r--iced_builder/src/lib.rs0
-rw-r--r--iced_builder/src/main.rs326
-rw-r--r--iced_builder/src/types/mod.rs54
-rw-r--r--iced_builder/src/types/rendered_element.rs47
7 files changed, 606 insertions, 0 deletions
diff --git a/iced_builder/Cargo.toml b/iced_builder/Cargo.toml
new file mode 100644
index 0000000..febff28
--- /dev/null
+++ b/iced_builder/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "iced_builder"
+description = "GUI builder for iced, built with iced."
+version = "0.1.0"
+edition = "2021"
+authors = ["pml68 <contact@pml68.me>"]
+repository = "https://github.com/pml68/iced-builder"
+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_drop = { path = "../iced_drop" }
+tokio = { version = "1.40.0", features = ["fs"] }
+rust-format = "0.3.4"
+unique_id = "0.1.5"
+
+[[bin]]
+name = "iced-builder"
+path = "src/main.rs"
diff --git a/iced_builder/fonts/icons.ttf b/iced_builder/fonts/icons.ttf
new file mode 100644
index 0000000..393c692
--- /dev/null
+++ b/iced_builder/fonts/icons.ttf
Binary files differ
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<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#"{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() -> 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
--- /dev/null
+++ b/iced_builder/src/lib.rs
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<String>,
+ dark_theme: bool,
+ pane_state: pane_grid::State<Panes>,
+ focus: Option<Pane>,
+ designer_state: DesignerState,
+ element_list: Vec<types::ElementName>,
+ 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<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)),
+ });
+ (
+ 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<Message> {
+ 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<Message> {
+ 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>(
+ 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<types::ElementName>) -> 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<RenderedElement>,
+ 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<RenderedElement>,
+ 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<RenderedElement>) -> 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;
+ }
+}