diff options
| author | Polesznyák Márk László <116908301+pml68@users.noreply.github.com> | 2024-10-24 23:18:46 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-24 23:18:46 +0200 |
| commit | b351dd45dcd4b4c9142f069c62b51159c00922bf (patch) | |
| tree | d2f24b449c3f82a9f844ce35198bad351c2ca8af /iced_builder/src/main.rs | |
| parent | Merge pull request #1 from pml68/feat/codegen (diff) | |
| parent | feat: implement d&d for existing elements (diff) | |
| download | iced-builder-b351dd45dcd4b4c9142f069c62b51159c00922bf.tar.gz | |
Merge pull request #2 from pml68/feat/drag-and-drop
Drag & Drop done
Diffstat (limited to '')
| -rw-r--r-- | iced_builder/src/main.rs | 415 |
1 files changed, 415 insertions, 0 deletions
diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs new file mode 100644 index 0000000..fc7f18c --- /dev/null +++ b/iced_builder/src/main.rs @@ -0,0 +1,415 @@ +use std::path::PathBuf; + +use iced::{ + advanced::widget::Id, + clipboard, highlighter, keyboard, + widget::{ + button, column, container, + pane_grid::{self, Pane, PaneGrid}, + row, text, text_editor, themer, tooltip, Column, Space, + }, + Alignment, Element, Font, Length, Settings, Task, Theme, +}; +use iced_builder::types::{ + element_name::ElementName, project::Project, rendered_element::ActionKind, DesignerPage, +}; +use iced_builder::Message; +use iced_drop::droppable; + +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) + .subscription(App::subscription) + .run_with(App::new) +} + +struct App { + is_dirty: bool, + is_loading: bool, + project_path: Option<PathBuf>, + project: Project, + dark_theme: bool, + pane_state: pane_grid::State<Panes>, + focus: Option<Pane>, + designer_page: DesignerPage, + element_list: Vec<ElementName>, + editor_content: text_editor::Content, +} + +#[derive(Clone, Copy, Debug)] +enum Panes { + Designer, + ElementList, +} + +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)), + }); + ( + Self { + is_dirty: false, + is_loading: false, + project_path: None, + project: Project::new(), + dark_theme: true, + pane_state: state, + focus: None, + designer_page: DesignerPage::Designer, + element_list: ElementName::ALL.to_vec(), + editor_content: text_editor::Content::new(), + }, + Task::none(), + ) + } + + fn title(&self) -> String { + let saved_state = if !self.is_dirty { "" } else { " *" }; + + let project_name = match &self.project.title { + Some(n) => { + format!( + " - {}", + if n.len() > 60 { + format!("...{}", &n[n.len() - 40..]) + } else { + n.to_owned() + } + ) + } + None => "".to_owned(), + }; + + 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::SwitchPage(page) => self.designer_page = page, + Message::EditorAction(action) => { + if let text_editor::Action::Scroll { lines: _ } = action { + self.editor_content.perform(action); + } + } + 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); + } + Message::DropNewElement(name, point, _) => { + return iced_drop::zones_on_point( + move |zones| Message::HandleNew(name.clone(), zones), + point, + None, + None, + ) + .into() + } + Message::HandleNew(name, zones) => { + let ids: Vec<Id> = zones.into_iter().map(|z| z.0).collect(); + if ids.len() > 0 { + let action = ActionKind::new(ids, &mut self.project.content.clone(), None); + let result = name.handle_action(self.project.content.as_mut(), action); + if let Ok(Some(ref element)) = result { + self.project.content = Some(element.clone()); + } + println!("{:?}", result); + } + + return Task::done(Message::RefreshEditorContent); + } + Message::MoveElement(element, point, _) => { + return iced_drop::zones_on_point( + move |zones| Message::HandleMove(element.clone(), zones), + point, + None, + None, + ) + .into() + } + Message::HandleMove(element, zones) => { + let ids: Vec<Id> = zones.into_iter().map(|z| z.0).collect(); + if ids.len() > 0 { + let action = ActionKind::new( + ids, + &mut self.project.content.clone(), + Some(element.get_id()), + ); + let result = element.handle_action(self.project.content.as_mut(), action); + + println!("{result:?}"); + } + + return Task::done(Message::RefreshEditorContent); + } + Message::PaneResized(pane_grid::ResizeEvent { split, ratio }) => { + self.pane_state.resize(split, ratio); + } + Message::PaneClicked(pane) => { + self.focus = Some(pane); + } + Message::PaneDragged(pane_grid::DragEvent::Dropped { pane, target }) => { + self.pane_state.drop(pane, target); + } + Message::PaneDragged(_) => {} + Message::NewFile => { + if !self.is_loading { + self.project = Project::new(); + self.project_path = None; + self.editor_content = text_editor::Content::new(); + } + } + Message::OpenFile => { + if !self.is_loading { + self.is_loading = true; + + return Task::perform(Project::from_file(), Message::FileOpened); + } + } + Message::FileOpened(result) => { + self.is_loading = false; + self.is_dirty = false; + + if let Ok((path, project)) = result { + self.project = project.clone(); + self.project_path = Some(path); + self.editor_content = text_editor::Content::with_text( + &project.app_code().unwrap_or_else(|err| err.to_string()), + ); + } + } + Message::SaveFile => { + if !self.is_loading { + self.is_loading = true; + + return Task::perform( + self.project + .clone() + .write_to_file(self.project_path.clone()), + Message::FileSaved, + ); + } + } + Message::FileSaved(result) => { + self.is_loading = false; + + if let Ok(path) = result { + self.project_path = Some(path); + self.is_dirty = false; + } + } + } + + Task::none() + } + + fn subscription(&self) -> iced::Subscription<Message> { + keyboard::on_key_press(|key, modifiers| match key.as_ref() { + keyboard::Key::Character("o") if modifiers.command() => Some(Message::OpenFile), + keyboard::Key::Character("s") if modifiers.command() => Some(Message::SaveFile), + 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)] + .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 => { + let el_tree = match self.project.content.clone() { + Some(tree) => tree.as_element(), + None => text("Open a project or begin creating one").into(), + }; + let content = container(themer(self.project.get_theme(), el_tree)) + .id(iced::widget::container::Id::new("drop_zone")) + .height(Length::Fill) + .width(Length::Fill); + let title = row![ + text("Designer"), + Space::with_width(Length::Fill), + button("Switch to Code view") + .on_press(Message::SwitchPage(DesignerPage::CodeView)), + ] + .align_y(Alignment::Center); + let title_bar = pane_grid::TitleBar::new(title) + .padding(10) + .style(style::title_bar); + 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"), + Space::with_width(Length::Fill), + tooltip( + button( + container( + text('\u{0e801}').font(Font::with_name("editor-icons")) + ) + .center_x(30) + ) + .on_press(Message::CopyCode), + "Copy code to clipboard", + tooltip::Position::FollowCursor + ), + Space::with_width(20), + button("Switch to Designer view") + .on_press(Message::SwitchPage(DesignerPage::Designer)) + ] + .align_y(Alignment::Center); + let title_bar = pane_grid::TitleBar::new(title) + .padding(10) + .style(style::title_bar); + pane_grid::Content::new( + text_editor(&self.editor_content) + .on_action(Message::EditorAction) + .highlight( + "rs", + if self.dark_theme { + highlighter::Theme::SolarizedDark + } else { + highlighter::Theme::InspiredGitHub + }, + ) + .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_x(Alignment::Center) + .height(Length::Fill) + .width(Length::Fill); + let title = text("Element List"); + 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::PaneResized) + .on_click(Message::PaneClicked) + .on_drag(Message::PaneDragged); + + let content = Column::new() + .push(header) + .push(pane_grid) + .spacing(5) + .align_x(Alignment::Center) + .width(Length::Fill); + + container(content).height(Length::Fill).into() + } +} + +fn items_list_view<'a>(items: Vec<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)), + ); + } + + container(column) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +mod style { + use iced::widget::container::Style; + use iced::{Border, Theme}; + + pub fn title_bar(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + text_color: Some(palette.background.strong.text), + background: Some(palette.background.strong.color.into()), + ..Default::default() + } + } + + pub fn pane_active(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + 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) -> Style { + let palette = theme.extended_palette(); + + Style { + background: Some(palette.background.weak.color.into()), + border: Border { + width: 4.0, + color: palette.background.strong.color, + ..Border::default() + }, + ..Default::default() + } + } +} |
