diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/appearance.rs | 46 | ||||
| -rw-r--r-- | src/config.rs | 46 | ||||
| -rw-r--r-- | src/dialogs.rs | 8 | ||||
| -rw-r--r-- | src/icon.rs | 10 | ||||
| -rw-r--r-- | src/main.rs | 109 | ||||
| -rw-r--r-- | src/options.rs | 2 | ||||
| -rw-r--r-- | src/panes/code_view.rs | 87 | ||||
| -rw-r--r-- | src/panes/designer_view.rs | 53 | ||||
| -rw-r--r-- | src/panes/element_list.rs | 16 | ||||
| -rw-r--r-- | src/panes/style.rs | 23 | ||||
| -rw-r--r-- | src/theme.rs | 425 | ||||
| -rw-r--r-- | src/types.rs | 8 | ||||
| -rw-r--r-- | src/types/project.rs | 70 | ||||
| -rwxr-xr-x | src/types/rendered_element.rs | 158 | ||||
| -rw-r--r-- | src/widget.rs | 18 |
15 files changed, 386 insertions, 693 deletions
diff --git a/src/appearance.rs b/src/appearance.rs new file mode 100644 index 0000000..78e782d --- /dev/null +++ b/src/appearance.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use material_theme::Theme; + +pub fn iced_theme_from_str(theme_name: &str) -> iced::Theme { + match theme_name { + "Light" => iced::Theme::Light, + "Dark" => iced::Theme::Dark, + "Dracula" => iced::Theme::Dracula, + "Nord" => iced::Theme::Nord, + "Solarized Light" => iced::Theme::SolarizedLight, + "Solarized Dark" => iced::Theme::SolarizedDark, + "Gruvbox Light" => iced::Theme::GruvboxLight, + "Gruvbox Dark" => iced::Theme::GruvboxDark, + "Catppuccin Latte" => iced::Theme::CatppuccinLatte, + "Catppuccin Frappé" => iced::Theme::CatppuccinFrappe, + "Catppuccin Macchiato" => iced::Theme::CatppuccinMacchiato, + "Catppuccin Mocha" => iced::Theme::CatppuccinMocha, + "Tokyo Night" => iced::Theme::TokyoNight, + "Tokyo Night Storm" => iced::Theme::TokyoNightStorm, + "Tokyo Night Light" => iced::Theme::TokyoNightLight, + "Kanagawa Wave" => iced::Theme::KanagawaWave, + "Kanagawa Dragon" => iced::Theme::KanagawaDragon, + "Kanagawa Lotus" => iced::Theme::KanagawaLotus, + "Moonfly" => iced::Theme::Moonfly, + "Nightfly" => iced::Theme::Nightfly, + "Oxocarbon" => iced::Theme::Oxocarbon, + "Ferra" => iced::Theme::Ferra, + _ => iced::Theme::default(), + } +} + +#[derive(Debug, Clone)] +pub struct Appearance { + pub selected: Theme, + pub all: Arc<[Theme]>, +} + +impl Default for Appearance { + fn default() -> Self { + Self { + selected: Theme::default(), + all: Theme::ALL.into(), + } + } +} diff --git a/src/config.rs b/src/config.rs index 1da1239..369a505 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,24 +1,34 @@ // (c) 2022-2024 Cory Forsstrom, Casper Rogild Storm, Calvin Lee, Andrew Baldwin, Reza Alizadeh Majd // (c) 2024-2025 Polesznyák Márk László -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use material_theme::Theme; use serde::Deserialize; use tokio_stream::StreamExt; use tokio_stream::wrappers::ReadDirStream; -use crate::theme::{Appearance, Theme, theme_from_str, theme_index}; +use crate::appearance::Appearance; use crate::{Error, environment}; #[derive(Debug, Clone, Default)] pub struct Config { - pub theme: Appearance, - pub last_project: Option<PathBuf>, + appearance: Appearance, + last_project: Option<PathBuf>, } impl Config { - pub fn selected_theme(&self) -> iced::Theme { - self.theme.selected.clone() + pub fn selected_theme(&self) -> Theme { + self.appearance.selected.clone() + } + + pub fn themes(&self) -> Arc<[Theme]> { + self.appearance.all.clone() + } + + pub fn last_project(&self) -> Option<&Path> { + self.last_project.as_deref() } pub fn config_dir() -> PathBuf { @@ -67,15 +77,18 @@ impl Config { last_project, } = toml::from_str(content.as_ref())?; - let theme = Self::load_theme(theme).await.unwrap_or_default(); + let appearance = + Self::load_appearance(&theme).await.unwrap_or_default(); Ok(Self { - theme, + appearance, last_project, }) } - pub async fn load_theme(theme_name: String) -> Result<Appearance, Error> { + pub async fn load_appearance( + theme_name: &str, + ) -> Result<Appearance, Error> { use tokio::fs; let read_entry = async move |entry: fs::DirEntry| { @@ -83,15 +96,16 @@ impl Config { let theme: Theme = toml::from_str(content.as_ref()).ok()?; - Some(iced::Theme::from(theme)) + Some(theme) }; - let mut selected = Theme::default().into(); - let mut all = iced::Theme::ALL.to_owned(); - all.push(Theme::default().into()); + let mut selected = Theme::default(); + let mut all = Theme::ALL.to_owned(); - if theme_index(&theme_name, iced::Theme::ALL).is_some() { - selected = theme_from_str(None, &theme_name); + if let Some(index) = + Theme::ALL.iter().position(|t| t.name() == theme_name) + { + selected = Theme::ALL[index].clone(); } let mut stream = @@ -102,7 +116,7 @@ impl Config { }; if let Some(theme) = read_entry(entry).await { - if theme.to_string() == theme_name { + if theme.name() == theme_name { selected = theme.clone(); } all.push(theme); diff --git a/src/dialogs.rs b/src/dialogs.rs index 08513fd..c1933ec 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -1,19 +1,19 @@ -use iced::{Element, Task}; +use iced::Task; use iced_dialog::button; use crate::Message; -use crate::types::{DialogAction, DialogButtons}; +use crate::types::{DialogAction, DialogButtons, Element}; pub const UNSAVED_CHANGES_TITLE: &str = "Unsaved changes"; pub const WARNING_TITLE: &str = "Heads up!"; pub const ERROR_TITLE: &str = "Oops! Something went wrong."; pub fn ok_button<'a>() -> Element<'a, Message> { - button("Ok").on_press(Message::DialogOk).into() + button("Ok", Message::DialogOk).into() } pub fn cancel_button<'a>() -> Element<'a, Message> { - button("Cancel").on_press(Message::DialogCancel).into() + button("Cancel", Message::DialogCancel).into() } pub fn error_dialog(description: impl Into<String>) -> Task<Message> { diff --git a/src/icon.rs b/src/icon.rs index 9dc0a89..d218943 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,8 +1,10 @@ // Generated automatically by iced_fontello at build time. // Do not edit manually. Source: ../fonts/icons.toml -// 02c7558d187cdc056fdd0e6a638ef805fa10f5955f834575e51d75acd35bc70e -use iced::widget::{text, Text}; +// 915ea6b0646871c0f04350f201f27f28881b61f3bd6ef292a415d67a211739c1 use iced::Font; +use iced::widget::text; + +use crate::widget::Text; pub const FONT: &[u8] = include_bytes!("../fonts/icons.ttf"); @@ -18,6 +20,10 @@ pub fn save<'a>() -> Text<'a> { icon("\u{1F4BE}") } +pub fn switch<'a>() -> Text<'a> { + icon("\u{21C6}") +} + fn icon(codepoint: &str) -> Text<'_> { text(codepoint).font(Font::with_name("icons")) } diff --git a/src/main.rs b/src/main.rs index d5715ef..1ac1d67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod appearance; mod config; mod dialogs; mod environment; @@ -6,7 +7,6 @@ mod error; mod icon; mod options; mod panes; -mod theme; mod types; mod values; mod widget; @@ -24,14 +24,14 @@ use iced::advanced::widget::Id; use iced::widget::{ Column, container, pane_grid, pick_list, row, text, text_editor, }; -use iced::{Alignment, Element, Length, Task, Theme, clipboard, keyboard}; +use iced::{Alignment, Length, Task, clipboard, keyboard}; use iced_anim::transition::Easing; use iced_anim::{Animated, Animation}; use iced_dialog::dialog::Dialog; +use material_theme::Theme; use panes::{code_view, designer_view, element_list}; -use tokio::runtime; use types::{ - Action, DesignerPane, DialogAction, DialogButtons, ElementName, Message, + Action, DesignerPane, DialogAction, DialogButtons, Element, Message, Project, }; @@ -47,23 +47,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { return Ok(()); } - let config_load = { - let rt = runtime::Builder::new_current_thread() - .enable_all() - .build()?; + iced::application( + IcedBuilder::init, + IcedBuilder::update, + IcedBuilder::view, + ) + .title(IcedBuilder::title) + .font(icon::FONT) + .theme(|state| state.theme.value().clone()) + .subscription(IcedBuilder::subscription) + .antialiasing(true) + .centered() + .run()?; - 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 { +struct IcedBuilder { is_dirty: bool, is_loading: bool, project_path: Option<PathBuf>, @@ -78,7 +78,6 @@ struct App { dialog_content: String, dialog_buttons: DialogButtons, dialog_action: DialogAction, - element_list: &'static [ElementName], editor_content: text_editor::Content, } @@ -88,8 +87,8 @@ enum Panes { ElementList, } -impl App { - fn new(config_load: Result<Config, Error>) -> (Self, Task<Message>) { +impl IcedBuilder { + fn init() -> (Self, Task<Message>) { let state = pane_grid::State::with_configuration( pane_grid::Configuration::Split { axis: pane_grid::Axis::Vertical, @@ -99,25 +98,9 @@ impl App { }, ); - let config = Arc::new(config_load.unwrap_or_default()); + let config = Arc::new(Config::default()); let theme = config.selected_theme(); - let task = if let Some(path) = config.last_project.clone() { - if path.exists() && path.is_file() { - 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() - )) - } - } else { - Task::none() - }; - ( Self { is_dirty: false, @@ -134,10 +117,9 @@ impl App { dialog_content: String::new(), dialog_buttons: DialogButtons::None, dialog_action: DialogAction::None, - element_list: ElementName::ALL, editor_content: text_editor::Content::new(), }, - task, + Task::perform(Config::load(), Message::ConfigLoad), ) } @@ -163,6 +145,29 @@ impl App { fn update(&mut self, message: Message) -> Task<Message> { match message { + Message::ConfigLoad(result) => match result { + Ok(config) => { + self.config = Arc::new(config); + self.theme.update(self.config.selected_theme().into()); + + return if let Some(path) = self.config.last_project() { + if path.exists() && path.is_file() { + Task::perform( + Project::from_path(path.to_owned()), + Message::FileOpened, + ) + } else { + warning_dialog(format!( + "The file {} does not exist, or isn't a file.", + path.to_string_lossy() + )) + } + } else { + Task::none() + }; + } + Err(error) => return error_dialog(error), + }, Message::SwitchTheme(event) => self.theme.update(event), Message::CopyCode => { return clipboard::write(self.editor_content.text()); @@ -173,15 +178,13 @@ impl App { 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) => return error_dialog(error), + Message::RefreshEditorContent => match self.project.app_code() { + Ok(code) => { + self.editor_content = + text_editor::Content::with_text(&code); } - } + Err(error) => return error_dialog(error), + }, Message::DropNewElement(name, point, _) => { return iced_drop::zones_on_point( move |zones| Message::HandleNew(name.clone(), zones), @@ -277,7 +280,7 @@ impl App { self.is_dirty = false; self.is_loading = true; return Task::perform( - Project::from_file(self.config.clone()), + Project::from_file(), Message::FileOpened, ) .chain(close_dialog_task); @@ -307,7 +310,7 @@ impl App { self.is_loading = true; return Task::perform( - Project::from_file(self.config.clone()), + Project::from_file(), Message::FileOpened, ); } else { @@ -392,7 +395,7 @@ impl App { fn view(&self) -> Element<'_, Message> { let header = row![pick_list( - self.config.theme.all.clone(), + self.config.themes(), Some(self.theme.target()), |theme| Message::SwitchTheme(theme.into()) )] @@ -405,16 +408,14 @@ impl App { Panes::Designer => match &self.designer_page { DesignerPane::DesignerView => designer_view::view( self.project.element_tree.as_ref(), - self.project.get_theme(&self.config), + self.project.get_theme(), is_focused, ), DesignerPane::CodeView => { code_view::view(&self.editor_content, is_focused) } }, - Panes::ElementList => { - element_list::view(self.element_list, is_focused) - } + Panes::ElementList => element_list::view(is_focused), } }, ) diff --git a/src/options.rs b/src/options.rs index 2dc25d7..931182a 100644 --- a/src/options.rs +++ b/src/options.rs @@ -258,7 +258,7 @@ impl<Message> ApplyOptions for Row<'_, Message> { } } -impl<Handle> ApplyOptions for Image<Handle> { +impl<Handle> ApplyOptions for Image<'_, Handle> { fn apply_options(self, options: BTreeMap<String, Option<String>>) -> Self { let mut image = self; diff --git a/src/panes/code_view.rs b/src/panes/code_view.rs index 551347c..5999b8f 100644 --- a/src/panes/code_view.rs +++ b/src/panes/code_view.rs @@ -1,69 +1,90 @@ use iced::advanced::text::highlighter::Format; -use iced::widget::{Space, button, pane_grid, row, text, text_editor}; -use iced::{Alignment, Background, Border, Font, Length, Theme}; +use iced::border::Radius; +use iced::widget::{button, pane_grid, row, text, text_editor}; +use iced::{Alignment, Border, Font, Length}; use iced_custom_highlighter::{Highlight, Highlighter, Scope, Settings}; +use material_theme::Theme; use super::style; use crate::icon; use crate::types::{DesignerPane, Message}; use crate::widget::tip; +// TODO: implement a highlight style for the material theme fn highlight_style(theme: &Theme, scope: &Scope) -> Format<Font> { + let theme = if theme.is_dark() { + iced::Theme::SolarizedDark + } else { + iced::Theme::SolarizedLight + }; + match scope { Scope::Custom { .. } | Scope::Other => Format { color: Some(theme.extended_palette().primary.strong.color), font: None, }, - _ => Highlight::default_style(theme, scope), + _ => Highlight::default_style(&theme, scope), } } pub fn view( editor_content: &text_editor::Content, is_focused: bool, -) -> pane_grid::Content<'_, Message> { - let title = row![ - text("Generated Code"), - Space::with_width(Length::Fill), - tip( - button(icon::copy()) - .on_press(Message::CopyCode) - .padding([2, 7]) - .style(button::text), - "Copy", - tip::Position::FollowCursor - ), - Space::with_width(20), - button("Switch to Designer view") - .on_press(Message::SwitchPage(DesignerPane::DesignerView)) - ] - .align_y(Alignment::Center); - let title_bar = pane_grid::TitleBar::new(title) +) -> pane_grid::Content<'_, Message, Theme> { + let title_bar = pane_grid::TitleBar::new(text("Generated Code").center()) + .controls(pane_grid::Controls::dynamic( + row![ + tip( + button(icon::copy()) + .on_press(Message::CopyCode) + .padding([2, 7]) + .style(material_theme::button::text), + "Copy", + tip::Position::FollowCursor + ), + button("Switch to Designer view") + .on_press(Message::SwitchPage(DesignerPane::DesignerView)) + ] + .spacing(20) + .align_y(Alignment::Center), + row![ + tip( + button(icon::copy()) + .on_press(Message::CopyCode) + .padding([2, 7]) + .style(material_theme::button::text), + "Copy", + tip::Position::FollowCursor + ), + button(icon::switch()) + .on_press(Message::SwitchPage(DesignerPane::DesignerView)) + ] + .spacing(20) + .align_y(Alignment::Center), + )) .padding(10) .style(style::title_bar); + pane_grid::Content::new( text_editor(editor_content) .on_action(Message::EditorAction) .font(Font::MONOSPACE) - .highlight_with::<Highlighter>( + .highlight_with::<Highlighter<Theme>>( Settings::new(vec![], highlight_style, "rs"), Highlight::to_format, ) .style(|theme, _| { - let palette = theme.extended_palette(); + let style = material_theme::text_editor::default( + theme, + text_editor::Status::Active, + ); + text_editor::Style { - background: Background::Color( - palette.background.base.color, - ), border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, + radius: Radius::default(), + ..style.border }, - icon: palette.background.weak.text, - placeholder: palette.background.strong.color, - value: palette.background.base.text, - selection: palette.primary.weak.color, + ..style } }) .height(Length::Fill) diff --git a/src/panes/designer_view.rs b/src/panes/designer_view.rs index 6340f73..0255b40 100644 --- a/src/panes/designer_view.rs +++ b/src/panes/designer_view.rs @@ -1,32 +1,55 @@ -use iced::widget::{Space, button, container, pane_grid, row, text, themer}; -use iced::{Alignment, Element, Length}; +use iced::widget::{ + button, center, container, pane_grid, responsive, row, text, themer, +}; +use iced::{Alignment, Length}; +use material_theme::Theme; use super::style; +use crate::icon; use crate::types::{DesignerPane, Message, RenderedElement}; pub fn view<'a>( - element_tree: Option<&RenderedElement>, + element_tree: Option<&'a RenderedElement>, designer_theme: iced::Theme, is_focused: bool, -) -> pane_grid::Content<'a, Message> { - let el_tree: Element<'a, Message> = match element_tree { - Some(tree) => tree.clone().into(), - None => text("Open a project or begin creating one").into(), +) -> pane_grid::Content<'a, Message, Theme> { + let el_tree: iced::Element<'a, Message> = match element_tree { + Some(tree) => responsive(|size| { + center( + container(tree.clone()) + .style(|theme| { + container::background(theme.palette().background) + }) + .height(size.height * 0.5) + .width(size.height * 0.8), + ) + .into() + }) + .into(), + None => center("Open a project or begin creating one").into(), }; + let content = container(themer(designer_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(DesignerPane::CodeView)), - ] - .align_y(Alignment::Center); - let title_bar = pane_grid::TitleBar::new(title) + + let title_bar = pane_grid::TitleBar::new(text("Designer").center()) + .controls(pane_grid::Controls::dynamic( + row![ + button("Switch to Code view") + .on_press(Message::SwitchPage(DesignerPane::CodeView),) + ] + .align_y(Alignment::Center), + row![ + button(icon::switch()) + .on_press(Message::SwitchPage(DesignerPane::CodeView),) + ] + .align_y(Alignment::Center), + )) .padding(10) .style(style::title_bar); + pane_grid::Content::new(content) .title_bar(title_bar) .style(if is_focused { diff --git a/src/panes/element_list.rs b/src/panes/element_list.rs index 10eea66..0e5dbfe 100644 --- a/src/panes/element_list.rs +++ b/src/panes/element_list.rs @@ -1,17 +1,18 @@ use iced::widget::{Column, column, container, pane_grid, text}; -use iced::{Alignment, Element, Length}; +use iced::{Alignment, Length}; use iced_drop::droppable; +use material_theme::Theme; use super::style; -use crate::types::{ElementName, Message}; +use crate::types::{Element, ElementName, Message}; -fn items_list_view(items: &[ElementName]) -> Element<'_, Message> { +fn items_list_view<'a>() -> Element<'a, Message> { let mut column = Column::new() .spacing(20) .align_x(Alignment::Center) .width(Length::Fill); - for item in items { + for item in ElementName::ALL { column = column.push( droppable(text(item.clone().to_string())).on_drop(|point, rect| { Message::DropNewElement(item.clone(), point, rect) @@ -25,11 +26,8 @@ fn items_list_view(items: &[ElementName]) -> Element<'_, Message> { .into() } -pub fn view( - element_list: &[ElementName], - is_focused: bool, -) -> pane_grid::Content<'_, Message> { - let items_list = items_list_view(element_list); +pub fn view<'a>(is_focused: bool) -> pane_grid::Content<'a, Message, Theme> { + let items_list = items_list_view(); let content = column![items_list] .align_x(Alignment::Center) .height(Length::Fill) diff --git a/src/panes/style.rs b/src/panes/style.rs index 1eefb2d..acca6f9 100644 --- a/src/panes/style.rs +++ b/src/panes/style.rs @@ -1,24 +1,25 @@ use iced::widget::container::Style; -use iced::{Border, Theme}; +use iced::{Background, Border}; +use material_theme::Theme; pub fn title_bar(theme: &Theme) -> Style { - let palette = theme.extended_palette(); + let surface = theme.colors().surface; Style { - text_color: Some(palette.background.strong.text), - background: Some(palette.background.strong.color.into()), + text_color: Some(surface.on_surface), + background: Some(Background::Color(surface.surface_container.high)), ..Default::default() } } pub fn pane_active(theme: &Theme) -> Style { - let palette = theme.extended_palette(); + let surface = theme.colors().surface; Style { - background: Some(palette.background.weak.color.into()), + background: Some(Background::Color(surface.surface_container.low)), border: Border { width: 1.0, - color: palette.background.strong.color, + color: surface.surface_container.high, ..Border::default() }, ..Default::default() @@ -26,13 +27,13 @@ pub fn pane_active(theme: &Theme) -> Style { } pub fn pane_focused(theme: &Theme) -> Style { - let palette = theme.extended_palette(); + let surface = theme.colors().surface; Style { - background: Some(palette.background.weak.color.into()), + background: Some(Background::Color(surface.surface_container.low)), border: Border { - width: 4.0, - color: palette.background.strong.color, + width: 2.0, + color: surface.surface_container.high, ..Border::default() }, ..Default::default() diff --git a/src/theme.rs b/src/theme.rs deleted file mode 100644 index a072714..0000000 --- a/src/theme.rs +++ /dev/null @@ -1,425 +0,0 @@ -use std::sync::Arc; - -use iced::Color; -use iced::theme::palette::Extended; - -use crate::config::Config; - -const DEFAULT_THEME_CONTENT: &str = - include_str!("../assets/themes/rose_pine.toml"); - -pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option<usize> { - slice - .iter() - .position(|theme| theme.to_string() == theme_name) -} - -pub fn theme_from_str( - config: Option<&Config>, - theme_name: &str, -) -> iced::Theme { - match theme_name { - "Light" => iced::Theme::Light, - "Dark" => iced::Theme::Dark, - "Dracula" => iced::Theme::Dracula, - "Nord" => iced::Theme::Nord, - "Solarized Light" => iced::Theme::SolarizedLight, - "Solarized Dark" => iced::Theme::SolarizedDark, - "Gruvbox Light" => iced::Theme::GruvboxLight, - "Gruvbox Dark" => iced::Theme::GruvboxDark, - "Catppuccin Latte" => iced::Theme::CatppuccinLatte, - "Catppuccin Frappé" => iced::Theme::CatppuccinFrappe, - "Catppuccin Macchiato" => iced::Theme::CatppuccinMacchiato, - "Catppuccin Mocha" => iced::Theme::CatppuccinMocha, - "Tokyo Night" => iced::Theme::TokyoNight, - "Tokyo Night Storm" => iced::Theme::TokyoNightStorm, - "Tokyo Night Light" => iced::Theme::TokyoNightLight, - "Kanagawa Wave" => iced::Theme::KanagawaWave, - "Kanagawa Dragon" => iced::Theme::KanagawaDragon, - "Kanagawa Lotus" => iced::Theme::KanagawaLotus, - "Moonfly" => iced::Theme::Moonfly, - "Nightfly" => iced::Theme::Nightfly, - "Oxocarbon" => iced::Theme::Oxocarbon, - "Ferra" => iced::Theme::Ferra, - _ => { - if let Some(config) = config { - if theme_name == config.theme.selected.to_string() { - config.theme.selected.clone() - } else if let Some(index) = - theme_index(theme_name, &config.theme.all) - { - config.theme.all[index].clone() - } else { - iced::Theme::default() - } - } else { - iced::Theme::default() - } - } - } -} - -fn palette_to_string(palette: &iced::theme::Palette) -> String { - format!( - r"Palette {{ - background: color!(0x{}), - text: color!(0x{}), - primary: color!(0x{}), - success: color!(0x{}), - danger: color!(0x{}), - warning: color!(0x{}), - }}", - color_to_hex(palette.background), - color_to_hex(palette.text), - color_to_hex(palette.primary), - color_to_hex(palette.success), - color_to_hex(palette.danger), - color_to_hex(palette.warning), - ) -} - -fn extended_to_string(extended: &Extended) -> String { - format!( - r" -Extended{{background:Background{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},primary:Primary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},secondary:Secondary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},success:Success{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},danger:Danger{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},warning:Warning{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},is_dark:true,}}", - color_to_hex(extended.background.base.color), - color_to_hex(extended.background.base.text), - color_to_hex(extended.background.weak.color), - color_to_hex(extended.background.weak.text), - color_to_hex(extended.background.strong.color), - color_to_hex(extended.background.strong.text), - color_to_hex(extended.primary.base.color), - color_to_hex(extended.primary.base.text), - color_to_hex(extended.primary.weak.color), - color_to_hex(extended.primary.weak.text), - color_to_hex(extended.primary.strong.color), - color_to_hex(extended.primary.strong.text), - color_to_hex(extended.secondary.base.color), - color_to_hex(extended.secondary.base.text), - color_to_hex(extended.secondary.weak.color), - color_to_hex(extended.secondary.weak.text), - color_to_hex(extended.secondary.strong.color), - color_to_hex(extended.secondary.strong.text), - color_to_hex(extended.success.base.color), - color_to_hex(extended.success.base.text), - color_to_hex(extended.success.weak.color), - color_to_hex(extended.success.weak.text), - color_to_hex(extended.success.strong.color), - color_to_hex(extended.success.strong.text), - color_to_hex(extended.danger.base.color), - color_to_hex(extended.danger.base.text), - color_to_hex(extended.danger.weak.color), - color_to_hex(extended.danger.weak.text), - color_to_hex(extended.danger.strong.color), - color_to_hex(extended.danger.strong.text), - color_to_hex(extended.warning.base.color), - color_to_hex(extended.warning.base.text), - color_to_hex(extended.warning.weak.color), - color_to_hex(extended.warning.weak.text), - color_to_hex(extended.warning.strong.color), - color_to_hex(extended.warning.strong.text), - ) -} - -pub fn theme_to_string(theme: &iced::Theme) -> String { - let palette = theme.palette(); - let extended = theme.extended_palette(); - - let generated_extended = Extended::generate(palette); - - if &generated_extended == extended { - format!( - r#"custom( - "{}".to_string(), - {} - )"#, - theme, - palette_to_string(&palette) - ) - } else { - format!( - r#"custom_with_fn( - "{}".to_string(), - {}, - |_| {} - )"#, - theme, - palette_to_string(&palette), - extended_to_string(extended) - ) - } -} - -fn color_to_hex(color: Color) -> String { - use std::fmt::Write; - - let mut hex = String::with_capacity(12); - - let [r, g, b, a] = color.into_rgba8(); - - let _ = write!(&mut hex, "{:02X}", r); - let _ = write!(&mut hex, "{:02X}", g); - let _ = write!(&mut hex, "{:02X}", b); - - if a < u8::MAX { - let _ = write!(&mut hex, ", {:.2}", a as f32 / 255.0); - } - - hex -} - -#[derive(Debug, Clone)] -pub struct Appearance { - pub selected: iced::Theme, - pub all: Arc<[iced::Theme]>, -} - -impl Default for Appearance { - fn default() -> Self { - Self { - selected: Theme::default().into(), - all: { - let mut themes = iced::Theme::ALL.to_owned(); - themes.push(Theme::default().into()); - themes.into() - }, - } - } -} - -#[derive(Debug, serde::Deserialize)] -pub struct Theme { - name: String, - palette: ThemePalette, - dark: Option<bool>, - #[serde(flatten)] - extended: Option<ExtendedThemePalette>, -} - -impl From<Theme> for iced::Theme { - fn from(value: Theme) -> Self { - iced::Theme::custom_with_fn( - value.name.clone(), - value.palette.clone().into(), - |_| value.into(), - ) - } -} - -impl Default for Theme { - fn default() -> Self { - toml::from_str(DEFAULT_THEME_CONTENT).expect("parse default theme") - } -} - -#[derive(Debug, Clone, serde::Deserialize)] -pub struct ThemePalette { - #[serde(with = "color_serde")] - background: Color, - #[serde(with = "color_serde")] - text: Color, - #[serde(with = "color_serde")] - primary: Color, - #[serde(with = "color_serde")] - success: Color, - #[serde(with = "color_serde")] - danger: Color, - #[serde(with = "color_serde")] - warning: Color, -} - -impl Default for ThemePalette { - fn default() -> Self { - let palette = iced::Theme::default().palette(); - Self { - background: palette.background, - text: palette.text, - primary: palette.primary, - success: palette.success, - danger: palette.danger, - warning: palette.warning, - } - } -} - -impl From<ThemePalette> for iced::theme::Palette { - fn from(palette: ThemePalette) -> Self { - iced::theme::Palette { - background: palette.background, - text: palette.text, - primary: palette.primary, - success: palette.success, - danger: palette.danger, - warning: palette.warning, - } - } -} - -impl From<Theme> for Extended { - fn from(theme: Theme) -> Self { - let mut extended = Extended::generate(theme.palette.into()); - - if let Some(is_dark) = theme.dark { - extended.is_dark = is_dark; - } - - if let Some(extended_palette) = theme.extended { - if let Some(background) = extended_palette.background { - if let Some(base) = background.base { - extended.background.base = base.into(); - } - if let Some(weak) = background.weak { - extended.background.weak = weak.into(); - } - if let Some(strong) = background.strong { - extended.background.strong = strong.into(); - } - } - - if let Some(primary) = extended_palette.primary { - if let Some(base) = primary.base { - extended.primary.base = base.into(); - } - if let Some(weak) = primary.weak { - extended.primary.weak = weak.into(); - } - if let Some(strong) = primary.strong { - extended.primary.strong = strong.into(); - } - } - - if let Some(secondary) = extended_palette.secondary { - if let Some(base) = secondary.base { - extended.secondary.base = base.into(); - } - if let Some(weak) = secondary.weak { - extended.secondary.weak = weak.into(); - } - if let Some(strong) = secondary.strong { - extended.secondary.strong = strong.into(); - } - } - - if let Some(success) = extended_palette.success { - if let Some(base) = success.base { - extended.success.base = base.into(); - } - if let Some(weak) = success.weak { - extended.success.weak = weak.into(); - } - if let Some(strong) = success.strong { - extended.success.strong = strong.into(); - } - } - - if let Some(danger) = extended_palette.danger { - if let Some(base) = danger.base { - extended.danger.base = base.into(); - } - if let Some(weak) = danger.weak { - extended.danger.weak = weak.into(); - } - if let Some(strong) = danger.strong { - extended.danger.strong = strong.into(); - } - } - - if let Some(warning) = extended_palette.warning { - if let Some(base) = warning.base { - extended.warning.base = base.into(); - } - if let Some(weak) = warning.weak { - extended.warning.weak = weak.into(); - } - if let Some(strong) = warning.strong { - extended.warning.strong = strong.into(); - } - } - } - - extended - } -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ExtendedThemePalette { - background: Option<ThemeBackground>, - primary: Option<ThemePrimary>, - secondary: Option<ThemeSecondary>, - success: Option<ThemeSuccess>, - danger: Option<ThemeDanger>, - warning: Option<ThemeWarning>, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemeBackground { - base: Option<ThemePair>, - weak: Option<ThemePair>, - strong: Option<ThemePair>, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemePrimary { - base: Option<ThemePair>, - weak: Option<ThemePair>, - strong: Option<ThemePair>, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemeSecondary { - base: Option<ThemePair>, - weak: Option<ThemePair>, - strong: Option<ThemePair>, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemeSuccess { - base: Option<ThemePair>, - weak: Option<ThemePair>, - strong: Option<ThemePair>, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemeDanger { - base: Option<ThemePair>, - weak: Option<ThemePair>, - strong: Option<ThemePair>, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemeWarning { - base: Option<ThemePair>, - weak: Option<ThemePair>, - strong: Option<ThemePair>, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemePair { - #[serde(with = "color_serde")] - color: Color, - #[serde(with = "color_serde")] - text: Color, -} - -impl From<ThemePair> for iced::theme::palette::Pair { - fn from(pair: ThemePair) -> Self { - Self { - color: pair.color, - text: pair.text, - } - } -} - -mod color_serde { - use iced::Color; - use serde::{Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result<Color, D::Error> - where - D: Deserializer<'de>, - { - Ok(String::deserialize(deserializer) - .map(|hex| Color::parse(&hex))? - .unwrap_or(Color::TRANSPARENT)) - } -} diff --git a/src/types.rs b/src/types.rs index a7fae1c..608f285 100644 --- a/src/types.rs +++ b/src/types.rs @@ -8,14 +8,20 @@ pub use element_name::ElementName; use iced::advanced::widget::Id; use iced::widget::{pane_grid, text_editor}; use iced_anim::Event; +use material_theme::Theme; pub use project::Project; pub use rendered_element::*; use crate::Error; +use crate::config::Config; +pub type Element<'a, Message> = iced::Element<'a, Message, Theme>; + +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum Message { - SwitchTheme(Event<iced::Theme>), + ConfigLoad(Result<Config, Error>), + SwitchTheme(Event<Theme>), CopyCode, SwitchPage(DesignerPane), EditorAction(text_editor::Action), diff --git a/src/types/project.rs b/src/types/project.rs index 721783e..11789ac 100644 --- a/src/types/project.rs +++ b/src/types/project.rs @@ -1,24 +1,19 @@ use std::path::{Path, PathBuf}; -use std::sync::Arc; extern crate fxhash; -use fxhash::FxHashMap; use iced::Theme; use rust_format::{Edition, Formatter, RustFmt}; use serde::{Deserialize, Serialize}; use super::rendered_element::RenderedElement; use crate::Error; -use crate::config::Config; -use crate::theme::{theme_from_str, theme_index, theme_to_string}; +use crate::appearance::iced_theme_from_str; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Project { pub title: Option<String>, pub theme: Option<String>, pub element_tree: Option<RenderedElement>, - #[serde(skip)] - theme_cache: FxHashMap<String, String>, } impl Default for Project { @@ -33,45 +28,24 @@ impl Project { title: None, theme: None, element_tree: None, - theme_cache: FxHashMap::default(), } } - pub fn get_theme(&self, config: &Config) -> Theme { + pub fn get_theme(&self) -> Theme { match &self.theme { - Some(theme) => theme_from_str(Some(config), theme), - None => Theme::default(), + Some(theme) => iced_theme_from_str(theme), + None => iced::Theme::default(), } } - fn theme_code(&mut self, theme: &Theme) -> String { - let theme_name = theme.to_string(); - if theme_index(&theme_name, Theme::ALL).is_none() { - (*self - .theme_cache - .entry(theme_name) - .or_insert(theme_to_string(theme))) - .to_string() - } else { - theme_name.replace(" ", "") - } - } - - pub async fn from_path( - path: PathBuf, - config: Arc<Config>, - ) -> Result<(PathBuf, Self), Error> { + pub async fn from_path(path: PathBuf) -> Result<(PathBuf, Self), Error> { let contents = tokio::fs::read_to_string(&path).await?; - let mut project: Self = serde_json::from_str(&contents)?; - - let _ = project.theme_code(&project.get_theme(&config)); + let project: Self = serde_json::from_str(&contents)?; Ok((path, project)) } - pub async fn from_file( - config: Arc<Config>, - ) -> Result<(PathBuf, Self), Error> { + pub async fn from_file() -> Result<(PathBuf, Self), Error> { let picked_file = rfd::AsyncFileDialog::new() .set_title("Open a JSON file...") .add_filter("*.json, *.JSON", &["json", "JSON"]) @@ -81,7 +55,7 @@ impl Project { let path = picked_file.path().to_owned(); - Self::from_path(path, config).await + Self::from_path(path).await } pub async fn write_to_file( @@ -108,28 +82,21 @@ impl Project { Ok(path) } - pub fn app_code(&mut self, config: &Config) -> Result<String, Error> { - match self.element_tree { + pub fn app_code(&mut self) -> Result<String, Error> { + use iced::debug; + let codegen = debug::time("Code Generation"); + + let result = match self.element_tree { Some(ref element_tree) => { let (imports, view) = element_tree.codegen(); - let theme = self.get_theme(config); - let theme_code = self.theme_code(&theme); - let mut theme_imports = ""; - if theme_index(&theme.to_string(), Theme::ALL).is_none() { - if theme_code.contains("Extended") { - theme_imports = "use iced::{{color,theme::{{Palette,palette::{{Extended,Background,Primary,Secondary,Success,Danger,Warning,Pair}}}}}};\n"; - } else { - theme_imports = "use iced::{{color,theme::Palette}};\n"; - } - } + let theme = self.get_theme(); let app_code = format!( r#"// Automatically generated by iced Builder use iced::{{widget::{{{imports}}},Element}}; -{theme_imports} fn main() -> iced::Result {{ - iced::application("{}", State::update, State::view).theme(State::theme).run() + iced::application(State::default, State::update, State::view).title("{}").theme(State::theme).run() }} #[derive(Default)] @@ -153,7 +120,7 @@ impl State {{ Some(ref t) => t, None => "New app", }, - theme_code + theme.to_string().replace(" ", "") ); let config = rust_format::Config::new_str() .edition(Edition::Rust2021) @@ -163,6 +130,9 @@ impl State {{ Ok(rustfmt.format_str(app_code)?) } None => Err("No element tree present".into()), - } + }; + + codegen.finish(); + result } } diff --git a/src/types/rendered_element.rs b/src/types/rendered_element.rs index 0a78dcd..020aa46 100755 --- a/src/types/rendered_element.rs +++ b/src/types/rendered_element.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use iced::advanced::widget::Id; -use iced::{Element, Length, widget}; +use iced::{Element, widget}; use serde::{Deserialize, Serialize}; use crate::Error; @@ -171,34 +171,6 @@ impl RenderedElement { self } - pub fn text_view<'a>(self) -> Element<'a, Message> { - let mut children = widget::column![]; - - if let Some(els) = self.child_elements.clone() { - for el in els { - children = children.push(el.clone().text_view()); - } - } - iced_drop::droppable( - widget::container( - widget::column![ - widget::text(self.name.clone().to_string()), - children - ] - .width(Length::Fill) - .spacing(10), - ) - .padding(10) - .style(widget::container::bordered_box), - ) - .id(self.id().clone()) - .drag_hide(true) - .on_drop(move |point, rect| { - Message::MoveElement(self.clone(), point, rect) - }) - .into() - } - pub fn codegen(&self) -> (String, String) { let mut imports = String::new(); let mut view = String::new(); @@ -223,7 +195,14 @@ impl RenderedElement { match &self.name { ElementName::Container => { imports = format!("{imports}container,"); - view = format!("{view}\ncontainer({elements}){options}"); + view = format!( + "{view}\ncontainer({}){options}", + if elements.is_empty() { + String::from("\"\"") + } else { + elements.to_string() + } + ); } ElementName::Row => { imports = format!("{imports}row,"); @@ -237,7 +216,7 @@ impl RenderedElement { imports = format!("{imports}text,"); view = format!( "{view}\ntext(\"{}\"){options}", - if *string == String::new() { + if string.is_empty() { "New Text" } else { string @@ -248,7 +227,7 @@ impl RenderedElement { imports = format!("{imports}button,"); view = format!( "{view}\nbutton(\"{}\"){options}", - if *string == String::new() { + if string.is_empty() { "New Button" } else { string @@ -317,53 +296,98 @@ impl<'a> From<RenderedElement> for Element<'a, Message> { let child_elements = copy.child_elements.unwrap_or_default(); let content: Element<'a, Message> = match copy.name { - ElementName::Text(s) => { - if s.is_empty() { - widget::text("New Text").apply_options(copy.options).into() - } else { - widget::text(s).apply_options(copy.options).into() - } - } - ElementName::Button(s) => { - if s.is_empty() { - widget::button(widget::text("New Button")) - .apply_options(copy.options) - .into() - } else { - widget::button(widget::text(s)) - .apply_options(copy.options) - .into() - } + ElementName::Text(s) => if s.is_empty() { + widget::text("New Text") + } else { + widget::text(s) } + .apply_options(copy.options) + .into(), + ElementName::Button(s) => widget::button(if s.is_empty() { + widget::text("New Button") + } else { + widget::text(s) + }) + .apply_options(copy.options) + .into(), ElementName::Svg(p) => { widget::svg(p).apply_options(copy.options).into() } ElementName::Image(p) => { widget::image(p).apply_options(copy.options).into() } - ElementName::Container => { - widget::container(if child_elements.len() == 1 { - child_elements[0].clone().into() - } else { - Element::from("") - }) - .apply_options(copy.options) - .padding(20) - .into() + ElementName::Container => if child_elements.len() == 1 { + widget::container(child_elements[0].clone()) + } else { + widget::container("New Container").style( + |theme: &iced::Theme| widget::container::Style { + border: iced::Border { + color: theme.palette().text, + + width: 2.0, + radius: 4.into(), + }, + ..Default::default() + }, + ) } - ElementName::Row => widget::Row::from_vec( - child_elements.into_iter().map(Into::into).collect(), - ) - .padding(20) - .apply_options(copy.options) - .into(), - ElementName::Column => widget::Column::from_vec( - child_elements.into_iter().map(Into::into).collect(), - ) .padding(20) .apply_options(copy.options) .into(), + ElementName::Row => { + if !child_elements.is_empty() { + widget::Row::with_children( + child_elements.into_iter().map(Into::into), + ) + .padding(20) + .apply_options(copy.options) + .into() + } else { + widget::container( + widget::row!["New Row"] + .padding(20) + .apply_options(copy.options), + ) + .style(|theme: &iced::Theme| widget::container::Style { + border: iced::Border { + color: theme.palette().text, + + width: 2.0, + radius: 4.into(), + }, + ..Default::default() + }) + .into() + } + } + ElementName::Column => { + if !child_elements.is_empty() { + widget::Column::with_children( + child_elements.into_iter().map(Into::into), + ) + .padding(20) + .apply_options(copy.options) + .into() + } else { + widget::container( + widget::column!["New Column"] + .padding(20) + .apply_options(copy.options), + ) + .style(|theme: &iced::Theme| widget::container::Style { + border: iced::Border { + color: theme.palette().text, + + width: 2.0, + radius: 4.into(), + }, + ..Default::default() + }) + .into() + } + } }; + iced_drop::droppable(content) .id(value.id().clone()) .drag_hide(true) diff --git a/src/widget.rs b/src/widget.rs index f1eb0f3..859d25e 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,5 +1,7 @@ -use iced::Element; -use iced::widget::{container, text, tooltip}; +use iced::widget::{self, container, text, tooltip}; +use material_theme::Theme; + +use crate::types::Element; pub mod tip { pub use super::tooltip::Position; @@ -12,10 +14,16 @@ pub fn tip<'a, Message: 'a>( ) -> Element<'a, Message> { tooltip( target, - container(text(tip).size(14)) - .padding(5) - .style(container::rounded_box), + container(text(tip).size(14)).padding(5).style(|theme| { + let base = material_theme::container::surface_container_low(theme); + container::Style { + border: iced::border::rounded(4), + ..base + } + }), position, ) .into() } + +pub type Text<'a> = widget::Text<'a, Theme>; |
