summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorPolesznyák Márk László <116908301+pml68@users.noreply.github.com>2025-01-11 23:13:07 +0100
committerGitHub <noreply@github.com>2025-01-11 23:13:07 +0100
commit103699beeb8bdce38bc5803cbe038e74cbc20e40 (patch)
treeb79e13b3decc778cc7c66af7187c647ae0a21a52 /src/main.rs
parentMerge pull request #4 from pml68/feat/playground (diff)
parentrefactor: remove iced_drop & workspace (diff)
downloadiced-builder-103699beeb8bdce38bc5803cbe038e74cbc20e40.tar.gz
Merge pull request #5 from pml68/feat/config
Config done
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs382
1 files changed, 382 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..5b95b94
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,382 @@
+#![feature(test)]
+mod config;
+mod dialogs;
+mod environment;
+mod error;
+mod icon;
+mod panes;
+mod theme;
+mod types;
+mod widget;
+
+use std::path::PathBuf;
+
+use config::Config;
+use dialogs::{error_dialog, unsaved_changes_dialog, warning_dialog};
+use error::Error;
+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::transition::Easing;
+use iced_anim::{Animated, Animation};
+use panes::{code_view, designer_view, element_list};
+use tokio::runtime;
+use types::{Action, DesignerPage, ElementName, Message, Project};
+
+//pub(crate) type Result<T> = core::result::Result<T, Error>;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let config_load = {
+ let rt = runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()?;
+
+ rt.block_on(Config::load())
+ };
+
+ iced::application(App::title, App::update, App::view)
+ .font(icon::FONT)
+ .theme(|state| state.theme.value().clone())
+ .subscription(App::subscription)
+ .run_with(move || App::new(config_load))?;
+
+ Ok(())
+}
+
+struct App {
+ is_dirty: bool,
+ is_loading: bool,
+ project_path: Option<PathBuf>,
+ project: Project,
+ config: Config,
+ theme: Animated<Theme>,
+ pane_state: pane_grid::State<Panes>,
+ focus: Option<Pane>,
+ designer_page: DesignerPage,
+ element_list: &'static [ElementName],
+ editor_content: text_editor::Content,
+}
+
+#[derive(Clone, Copy, Debug)]
+enum Panes {
+ Designer,
+ ElementList,
+}
+
+impl App {
+ fn new(config_load: Result<Config, Error>) -> (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 config = config_load.unwrap_or_default();
+ let theme = config.selected_theme();
+
+ let mut task = Task::none();
+
+ if let Some(path) = config.last_project.clone() {
+ if path.exists() && path.is_file() {
+ task = Task::perform(
+ Project::from_path(path, config.clone()),
+ Message::FileOpened,
+ );
+ } else {
+ warning_dialog(format!(
+ "The file {} does not exist, or isn't a file.",
+ path.to_string_lossy()
+ ));
+ }
+ }
+
+ (
+ Self {
+ is_dirty: false,
+ is_loading: false,
+ project_path: None,
+ project: Project::new(),
+ config,
+ theme: Animated::new(theme, Easing::EASE_IN),
+ pane_state: state,
+ focus: None,
+ designer_page: DesignerPage::DesignerView,
+ element_list: ElementName::ALL,
+ editor_content: text_editor::Content::new(),
+ },
+ task,
+ )
+ }
+
+ 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 => String::new(),
+ };
+
+ format!("iced Builder{project_name}{saved_state}")
+ }
+
+ fn update(&mut self, message: Message) -> Task<Message> {
+ match message {
+ 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 {
+ self.editor_content.perform(action);
+ }
+ }
+ Message::RefreshEditorContent => {
+ match self.project.app_code(&self.config) {
+ 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(
+ move |zones| Message::HandleNew(name.clone(), zones),
+ point,
+ None,
+ None,
+ )
+ }
+ Message::HandleNew(name, zones) => {
+ let ids: Vec<Id> = zones.into_iter().map(|z| z.0).collect();
+ if !ids.is_empty() {
+ let eltree_clone = self.project.element_tree.clone();
+ let action = Action::new(&ids, &eltree_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()),
+ _ => {}
+ }
+
+ self.is_dirty = true;
+ 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,
+ )
+ }
+ Message::HandleMove(element, zones) => {
+ let ids: Vec<Id> = zones.into_iter().map(|z| z.0).collect();
+ if !ids.is_empty() {
+ let eltree_clone = self.project.element_tree.clone();
+ let action = Action::new(
+ &ids,
+ &eltree_clone,
+ Some(element.get_id()),
+ );
+ let result = element.handle_action(
+ self.project.element_tree.as_mut(),
+ action,
+ );
+ if let Err(error) = result {
+ error_dialog(error.to_string());
+ }
+
+ self.is_dirty = true;
+ 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 {
+ if !self.is_dirty {
+ self.project = Project::new();
+ self.project_path = None;
+ self.editor_content = text_editor::Content::new();
+ } else if 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 {
+ if !self.is_dirty {
+ self.is_loading = true;
+
+ return Task::perform(
+ Project::from_file(self.config.clone()),
+ Message::FileOpened,
+ );
+ } else if 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_file(self.config.clone()), Message::FileOpened);
+ }
+ }
+ }
+ Message::FileOpened(result) => {
+ self.is_loading = false;
+ self.is_dirty = false;
+
+ match result {
+ Ok((path, project)) => {
+ self.project = project;
+ self.project_path = Some(path);
+ self.editor_content = text_editor::Content::with_text(
+ &self
+ .project
+ .app_code(&self.config)
+ .unwrap_or_else(|err| err.to_string()),
+ );
+ }
+ Err(error) => error_dialog(error.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::SaveFileAs => {
+ if !self.is_loading {
+ self.is_loading = true;
+
+ return Task::perform(
+ self.project.clone().write_to_file(None),
+ Message::FileSaved,
+ );
+ }
+ }
+ Message::FileSaved(result) => {
+ self.is_loading = false;
+
+ match result {
+ Ok(path) => {
+ self.project_path = Some(path);
+ self.is_dirty = false;
+ }
+ Err(error) => error_dialog(error.to_string()),
+ }
+ }
+ }
+
+ Task::none()
+ }
+
+ fn subscription(&self) -> iced::Subscription<Message> {
+ 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
+ }
+ })
+ }
+
+ fn view(&self) -> Element<'_, Message> {
+ let header = row![pick_list(
+ self.config.theme.all.clone(),
+ 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::DesignerView => designer_view::view(
+ &self.project.element_tree,
+ self.project.get_theme(&self.config),
+ is_focused,
+ ),
+ DesignerPage::CodeView => code_view::view(
+ &self.editor_content,
+ self.theme.target().clone(),
+ 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);
+
+ let content = Column::new()
+ .push(header)
+ .push(pane_grid)
+ .spacing(5)
+ .align_x(Alignment::Center)
+ .width(Length::Fill);
+
+ Animation::new(&self.theme, container(content).height(Length::Fill))
+ .on_update(Message::ToggleTheme)
+ .into()
+ }
+}