diff --git a/README.md b/README.md index 914484224..1ff501b66 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,19 @@

Redefining state-of-the-art graphics editing

-**Graphite** is an in-development raster and vector graphics package that's free and open source. It is powered by a node graph compositing engine that fuses layers with nodes, providing a fully non-destructive editing experience. +**Graphite** is an in-development raster and vector graphics package that's free and open source. It is powered by a node graph compositing engine that fuses layers with nodes, providing a fully nondestructive editing experience. -Graphite is a lightweight vector graphics editor that runs in your browser. Its nascent node-based compositor lets you apply raster effects and co-create amazing art with AI in a non-destructive workflow. Fully-featured raster image editing and a native desktop application are the current focus of development and will be made available in the coming months. +Presently, Graphite is a lightweight vector graphics editor that runs in your browser. Its node-based compositor lets you apply image effects and co-create art with generative AI. -Launch the latest alpha release of the [Graphite editor](https://editor.graphite.rs) and learn more on the [project website](https://graphite.rs/). +Photo editing, digital painting, desktop publishing, VFX compositing, and motion graphics are additional competencies planned on the [roadmap](https://graphite.rs/features/#roadmap) to make Graphite a highly versatile content creation tool. + +Launch the latest alpha release of the [Graphite editor](https://editor.graphite.rs) and learn more at the [project website](https://graphite.rs/). ⭐ Please remember to star this project here on GitHub! ⭐ -![Vector artwork: "Valley of Spires"](https://static.graphite.rs/content/index/gui-demo-valley-of-spires.png) +[![Vector artwork: "Valley of Spires"](https://static.graphite.rs/content/index/gui-demo-valley-of-spires__2.png)](https://editor.graphite.rs/#demo/valley-of-spires) + +*[Click here](https://editor.graphite.rs/#demo/valley-of-spires) to open this artwork and explore it yourself.* ## Discord community diff --git a/editor/src/application.rs b/editor/src/application.rs index fa254bab7..8da743251 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -37,32 +37,17 @@ pub const GRAPHITE_GIT_COMMIT_DATE: &str = env!("GRAPHITE_GIT_COMMIT_DATE"); pub const GRAPHITE_GIT_COMMIT_HASH: &str = env!("GRAPHITE_GIT_COMMIT_HASH"); pub const GRAPHITE_GIT_COMMIT_BRANCH: &str = env!("GRAPHITE_GIT_COMMIT_BRANCH"); -pub fn release_series() -> String { - format!("Release Series: {}", GRAPHITE_RELEASE_SERIES) -} - -pub fn commit_info() -> String { - format!("{}\n{}\n{}", commit_timestamp(), commit_hash(), commit_branch()) -} - pub fn commit_info_localized(localized_commit_date: &str) -> String { - format!("{}\n{}\n{}", commit_timestamp_localized(localized_commit_date), commit_hash(), commit_branch()) -} - -pub fn commit_timestamp() -> String { - format!("Date: {}", GRAPHITE_GIT_COMMIT_DATE) -} - -pub fn commit_timestamp_localized(localized_commit_date: &str) -> String { - format!("Date: {}", localized_commit_date) -} - -pub fn commit_hash() -> String { - format!("Hash: {}", &GRAPHITE_GIT_COMMIT_HASH[..8]) -} - -pub fn commit_branch() -> String { - format!("Branch: {}", GRAPHITE_GIT_COMMIT_BRANCH) + format!( + "Release Series: {}\n\ + Branch: {}\n\ + Hash: {}\n\ + {}", + GRAPHITE_RELEASE_SERIES, + GRAPHITE_GIT_COMMIT_BRANCH, + &GRAPHITE_GIT_COMMIT_HASH[..8], + localized_commit_date + ) } #[cfg(test)] diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 97bfbe672..8d7ddc165 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -553,15 +553,18 @@ mod test { let print_problem_to_terminal_on_failure = |value: &String| { println!(); println!("-------------------------------------------------"); - println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file."); - println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`"); - println!("After bumping this version number, update the documents in `/demo-artwork` by editing their JSON to"); - println!("ensure they remain compatible with both the bumped version number and the serialization format change."); + println!("Failed test due to receiving a DisplayDialogError while loading a Graphite demo file."); + println!(); + println!("That probably means the document serialization format changed. In that case, you need to bump the constant value"); + println!("`GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`, then update the documents in `/demo-artwork` by editing"); + println!("their JSON to ensure they remain compatible with both the bumped version number and the serialization format changes."); + println!(); println!("DisplayDialogError details:"); println!(); println!("Description: {}", value); println!("-------------------------------------------------"); println!(); + panic!() }; @@ -581,7 +584,7 @@ mod test { for response in responses { // Check for the existence of the file format incompatibility warning dialog after opening the test file - if let FrontendMessage::UpdateDialogDetails { layout_target: _, diff } = response { + if let FrontendMessage::UpdateDialogColumn1 { layout_target: _, diff } = response { if let DiffUpdate::SubLayout(sub_layout) = &diff[0].new_value { if let LayoutGroup::Row { widgets } = &sub_layout[0] { if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget { diff --git a/editor/src/messages/dialog/dialog_message.rs b/editor/src/messages/dialog/dialog_message.rs index 1fae00d5f..47947f9c0 100644 --- a/editor/src/messages/dialog/dialog_message.rs +++ b/editor/src/messages/dialog/dialog_message.rs @@ -29,12 +29,16 @@ pub enum DialogMessage { RequestAboutGraphiteDialog, RequestAboutGraphiteDialogWithLocalizedCommitDate { localized_commit_date: String, + localized_commit_year: String, }, RequestComingSoonDialog { issue: Option, }, RequestDemoArtworkDialog, RequestExportDialog, + RequestLicensesDialogWithLocalizedCommitDate { + localized_commit_year: String, + }, RequestNewDocumentDialog, RequestPreferencesDialog, } diff --git a/editor/src/messages/dialog/dialog_message_handler.rs b/editor/src/messages/dialog/dialog_message_handler.rs index f0c11e3fc..c7c2ae49c 100644 --- a/editor/src/messages/dialog/dialog_message_handler.rs +++ b/editor/src/messages/dialog/dialog_message_handler.rs @@ -1,4 +1,4 @@ -use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog}; +use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog}; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; @@ -16,48 +16,54 @@ impl MessageHandler self.export_dialog.process_message(message, responses, ()), + DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, responses, portfolio), #[remain::unsorted] DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, ()), #[remain::unsorted] DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, preferences), DialogMessage::CloseAllDocumentsWithConfirmation => { - let dialog = simple_dialogs::CloseAllDocumentsDialog; - dialog.send_layout(responses, LayoutTarget::DialogDetails); - responses.add(FrontendMessage::DisplayDialog { icon: "Copy".to_string() }); + let dialog = simple_dialogs::CloseAllDocumentsDialog { + unsaved_document_names: portfolio.unsaved_document_names(), + }; + dialog.send_dialog_to_frontend(responses); } DialogMessage::CloseDialogAndThen { followups } => { - responses.add(FrontendMessage::DisplayDialogDismiss); for message in followups.into_iter() { responses.add(message); } + + // This come after followups, so that the followups (which can cause the dialog to open) happen first, then we close it afterwards. + // If it comes before, the dialog reopens (and appears to not close at all). + responses.add(FrontendMessage::DisplayDialogDismiss); } DialogMessage::DisplayDialogError { title, description } => { let dialog = simple_dialogs::ErrorDialog { title, description }; - dialog.send_layout(responses, LayoutTarget::DialogDetails); - responses.add(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }); + dialog.send_dialog_to_frontend(responses); } DialogMessage::RequestAboutGraphiteDialog => { responses.add(FrontendMessage::TriggerAboutGraphiteLocalizedCommitDate { commit_date: env!("GRAPHITE_GIT_COMMIT_DATE").into(), }); } - DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate { localized_commit_date } => { - let about_graphite = AboutGraphiteDialog { localized_commit_date }; + DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate { + localized_commit_date, + localized_commit_year, + } => { + let dialog = AboutGraphiteDialog { + localized_commit_date, + localized_commit_year, + }; - about_graphite.send_layout(responses, LayoutTarget::DialogDetails); - responses.add(FrontendMessage::DisplayDialog { icon: "GraphiteLogo".to_string() }); + dialog.send_dialog_to_frontend(responses); } DialogMessage::RequestComingSoonDialog { issue } => { - let coming_soon = ComingSoonDialog { issue }; - coming_soon.send_layout(responses, LayoutTarget::DialogDetails); - responses.add(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }); + let dialog = ComingSoonDialog { issue }; + dialog.send_dialog_to_frontend(responses); } DialogMessage::RequestDemoArtworkDialog => { - let demo_artwork_dialog = DemoArtworkDialog; - demo_artwork_dialog.send_layout(responses, LayoutTarget::DialogDetails); - responses.add(FrontendMessage::DisplayDialog { icon: "Image".to_string() }); + let dialog = DemoArtworkDialog; + dialog.send_dialog_to_frontend(responses); } DialogMessage::RequestExportDialog => { if let Some(document) = portfolio.active_document() { @@ -83,29 +89,30 @@ impl MessageHandler { + let dialog = LicensesDialog { localized_commit_year }; + + dialog.send_dialog_to_frontend(responses); + } DialogMessage::RequestNewDocumentDialog => { self.new_document_dialog = NewDocumentDialogMessageHandler { name: portfolio.generate_new_document_name(), infinite: false, dimensions: glam::UVec2::new(1920, 1080), }; - self.new_document_dialog.send_layout(responses, LayoutTarget::DialogDetails); - responses.add(FrontendMessage::DisplayDialog { icon: "File".to_string() }); + self.new_document_dialog.send_dialog_to_frontend(responses); } DialogMessage::RequestPreferencesDialog => { self.preferences_dialog = PreferencesDialogMessageHandler {}; - self.preferences_dialog.send_layout(responses, LayoutTarget::DialogDetails, preferences); - responses.add(FrontendMessage::DisplayDialog { icon: "Settings".to_string() }); + self.preferences_dialog.send_dialog_to_frontend(responses, preferences); } } } diff --git a/editor/src/messages/dialog/export_dialog/export_dialog_message.rs b/editor/src/messages/dialog/export_dialog/export_dialog_message.rs index eabb6a168..599d078f4 100644 --- a/editor/src/messages/dialog/export_dialog/export_dialog_message.rs +++ b/editor/src/messages/dialog/export_dialog/export_dialog_message.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; #[impl_message(Message, DialogMessage, ExportDialog)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum ExportDialogMessage { - FileName(String), FileType(FileType), ScaleFactor(f64), TransparentBackground(bool), diff --git a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs index 23e48c29d..830bdffbb 100644 --- a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs +++ b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs @@ -7,7 +7,6 @@ use document_legacy::LayerId; /// A dialog to allow users to customize their file export. #[derive(Debug, Clone, Default)] pub struct ExportDialogMessageHandler { - pub file_name: String, pub file_type: FileType, pub scale_factor: f64, pub bounds: ExportBounds, @@ -16,17 +15,16 @@ pub struct ExportDialogMessageHandler { pub has_selection: bool, } -impl MessageHandler for ExportDialogMessageHandler { - fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque, _data: ()) { +impl MessageHandler for ExportDialogMessageHandler { + fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque, portfolio: &PortfolioMessageHandler) { match message { - ExportDialogMessage::FileName(name) => self.file_name = name, ExportDialogMessage::FileType(export_type) => self.file_type = export_type, ExportDialogMessage::ScaleFactor(factor) => self.scale_factor = factor, ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background, ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area, ExportDialogMessage::Submit => responses.add_front(DocumentMessage::ExportDocument { - file_name: self.file_name.clone(), + file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(), file_type: self.file_type, scale_factor: self.scale_factor, bounds: self.bounds, @@ -34,33 +32,60 @@ impl MessageHandler for ExportDialogMessageHandler { }), } - self.send_layout(responses, LayoutTarget::DialogDetails); + self.send_dialog_to_frontend(responses); } advertise_actions! {ExportDialogUpdate;} } -impl LayoutHolder for ExportDialogMessageHandler { - fn layout(&self) -> Layout { - let file_name = vec![ - TextLabel::new("File Name").table_align(true).widget_holder(), - Separator::new(SeparatorType::Unrelated).widget_holder(), - TextInput::new(&self.file_name) - .on_update(|text_input: &TextInput| ExportDialogMessage::FileName(text_input.value.clone()).into()) +impl DialogLayoutHolder for ExportDialogMessageHandler { + const ICON: &'static str = "File"; + const TITLE: &'static str = "Export"; + + fn layout_buttons(&self) -> Layout { + let widgets = vec![ + TextButton::new("Export") + .emphasized(true) + .on_update(|_| { + DialogMessage::CloseDialogAndThen { + followups: vec![ExportDialogMessage::Submit.into()], + } + .into() + }) .widget_holder(), + TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), ]; + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} + +impl LayoutHolder for ExportDialogMessageHandler { + fn layout(&self) -> Layout { let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")] .into_iter() .map(|(val, name)| RadioEntryData::new(name).on_update(move |_| ExportDialogMessage::FileType(val).into())) .collect(); let export_type = vec![ - TextLabel::new("File Type").table_align(true).widget_holder(), + TextLabel::new("File Type").table_align(true).min_width(100).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), RadioInput::new(entries).selected_index(self.file_type as u32).widget_holder(), ]; + let resolution = vec![ + TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + NumberInput::new(Some(self.scale_factor)) + .unit("") + .min(0.) + .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) + .disabled(self.file_type == FileType::Svg) + .on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into()) + .min_width(200) + .widget_holder(), + ]; + let artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string(), false)); let mut export_area_options = vec![ (ExportBounds::AllArtwork, "All Artwork".to_string(), false), @@ -74,13 +99,13 @@ impl LayoutHolder for ExportDialogMessageHandler { .collect()]; let export_area = vec![ - TextLabel::new("Bounds").table_align(true).widget_holder(), + TextLabel::new("Bounds").table_align(true).min_width(100).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(), ]; let transparent_background = vec![ - TextLabel::new("Transparency").table_align(true).widget_holder(), + TextLabel::new("Transparency").table_align(true).min_width(100).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), CheckboxInput::new(self.transparent_background) .disabled(self.file_type == FileType::Jpg) @@ -88,42 +113,11 @@ impl LayoutHolder for ExportDialogMessageHandler { .widget_holder(), ]; - let resolution = vec![ - TextLabel::new("Scale Factor").table_align(true).widget_holder(), - Separator::new(SeparatorType::Unrelated).widget_holder(), - NumberInput::new(Some(self.scale_factor)) - .unit(" ") - .min(0.) - .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) - .disabled(self.file_type == FileType::Svg) - .on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into()) - .widget_holder(), - ]; - - let button_widgets = vec![ - TextButton::new("Export") - .min_width(96) - .emphasized(true) - .on_update(|_| { - DialogMessage::CloseDialogAndThen { - followups: vec![ExportDialogMessage::Submit.into()], - } - .into() - }) - .widget_holder(), - TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), - ]; - Layout::WidgetLayout(WidgetLayout::new(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Export").bold(true).widget_holder()], - }, - LayoutGroup::Row { widgets: file_name }, LayoutGroup::Row { widgets: export_type }, LayoutGroup::Row { widgets: resolution }, LayoutGroup::Row { widgets: export_area }, LayoutGroup::Row { widgets: transparent_background }, - LayoutGroup::Row { widgets: button_widgets }, ])) } } diff --git a/editor/src/messages/dialog/mod.rs b/editor/src/messages/dialog/mod.rs index cdf3f4ed2..3237552e6 100644 --- a/editor/src/messages/dialog/mod.rs +++ b/editor/src/messages/dialog/mod.rs @@ -1,8 +1,8 @@ //! Handles modal dialogs that appear as floating menus in the center of the editor window. //! -//! Dialogs are represented as structs that implement the `LayoutHolder` trait. +//! Dialogs are represented as structs that implement the `DialogLayoutHolder` trait. //! -//! To open a dialog, call the function `register_properties` on the dialog struct with `responses` and the `LayoutTarget::DialogDetails` enum variant. +//! To open a dialog, call the function `send_dialog_to_frontend()` on the dialog struct. //! Then dialog can be opened by sending the `FrontendMessage::DisplayDialog` message; mod dialog_message; diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index a81f5e41b..cc608bf23 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -42,26 +42,47 @@ impl MessageHandler for NewDocumentDialogMessageHa } } - self.send_layout(responses, LayoutTarget::DialogDetails); + self.send_dialog_to_frontend(responses); } advertise_actions! {NewDocumentDialogUpdate;} } +impl DialogLayoutHolder for NewDocumentDialogMessageHandler { + const ICON: &'static str = "File"; + const TITLE: &'static str = "New Document"; + + fn layout_buttons(&self) -> Layout { + let widgets = vec![ + TextButton::new("OK") + .emphasized(true) + .on_update(|_| { + DialogMessage::CloseDialogAndThen { + followups: vec![NewDocumentDialogMessage::Submit.into()], + } + .into() + }) + .widget_holder(), + TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), + ]; + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} + impl LayoutHolder for NewDocumentDialogMessageHandler { fn layout(&self) -> Layout { - let title = vec![TextLabel::new("New Document").bold(true).widget_holder()]; - let name = vec![ - TextLabel::new("Name").table_align(true).widget_holder(), + TextLabel::new("Name").table_align(true).min_width(90).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), TextInput::new(&self.name) .on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name(text_input.value.clone()).into()) + .min_width(204) // Matches the 100px of both NumberInputs below + the 4px of the Unrelated-type separator .widget_holder(), ]; let infinite = vec![ - TextLabel::new("Infinite Canvas").table_align(true).widget_holder(), + TextLabel::new("Infinite Canvas").table_align(true).min_width(90).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), CheckboxInput::new(self.infinite) .on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into()) @@ -69,7 +90,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler { ]; let scale = vec![ - TextLabel::new("Dimensions").table_align(true).widget_holder(), + TextLabel::new("Dimensions").table_align(true).min_width(90).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), NumberInput::new(Some(self.dimensions.x as f64)) .label("W") @@ -94,26 +115,10 @@ impl LayoutHolder for NewDocumentDialogMessageHandler { .widget_holder(), ]; - let button_widgets = vec![ - TextButton::new("OK") - .min_width(96) - .emphasized(true) - .on_update(|_| { - DialogMessage::CloseDialogAndThen { - followups: vec![NewDocumentDialogMessage::Submit.into()], - } - .into() - }) - .widget_holder(), - TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), - ]; - Layout::WidgetLayout(WidgetLayout::new(vec![ - LayoutGroup::Row { widgets: title }, LayoutGroup::Row { widgets: name }, LayoutGroup::Row { widgets: infinite }, LayoutGroup::Row { widgets: scale }, - LayoutGroup::Row { widgets: button_widgets }, ])) } } diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index 102ddc2e1..8f06c2b16 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -11,19 +11,18 @@ impl MessageHandler for Pr PreferencesDialogMessage::Confirm => {} } - self.send_layout(responses, LayoutTarget::DialogDetails, preferences); + self.send_dialog_to_frontend(responses, preferences); } advertise_actions! {PreferencesDialogUpdate;} } +// This doesn't actually implement the `DialogLayoutHolder` trait like the other dialog message handlers. +// That's because we need to give `send_layout` the `preferences` argument, which is not part of the trait. +// However, it's important to keep the methods in sync with those from the trait for consistency. impl PreferencesDialogMessageHandler { - pub fn send_layout(&self, responses: &mut VecDeque, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) { - responses.add(LayoutMessage::SendLayout { - layout: self.layout(preferences), - layout_target, - }) - } + const ICON: &'static str = "Settings"; + const TITLE: &'static str = "Editor Preferences"; fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout { let zoom_with_scroll = vec![ @@ -64,9 +63,32 @@ impl PreferencesDialogMessageHandler { .widget_holder(), ]; - let button_widgets = vec![ - TextButton::new("Ok") - .min_width(96) + Layout::WidgetLayout(WidgetLayout::new(vec![ + LayoutGroup::Row { widgets: zoom_with_scroll }, + LayoutGroup::Row { widgets: imaginate_server_hostname }, + LayoutGroup::Row { widgets: imaginate_refresh_frequency }, + ])) + } + pub fn send_layout(&self, responses: &mut VecDeque, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) { + responses.add(LayoutMessage::SendLayout { + layout: self.layout(preferences), + layout_target, + }) + } + + fn layout_column_2(&self) -> Layout { + Layout::default() + } + fn send_layout_column_2(&self, responses: &mut VecDeque, layout_target: LayoutTarget) { + responses.add(LayoutMessage::SendLayout { + layout: self.layout_column_2(), + layout_target, + }); + } + + fn layout_buttons(&self) -> Layout { + let widgets = vec![ + TextButton::new("OK") .emphasized(true) .on_update(|_| { DialogMessage::CloseDialogAndThen { @@ -75,20 +97,25 @@ impl PreferencesDialogMessageHandler { .into() }) .widget_holder(), - TextButton::new("Reset to Defaults") - .min_width(96) - .on_update(|_| PreferencesMessage::ResetToDefaults.into()) - .widget_holder(), + TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_holder(), ]; - Layout::WidgetLayout(WidgetLayout::new(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Editor Preferences").bold(true).widget_holder()], - }, - LayoutGroup::Row { widgets: zoom_with_scroll }, - LayoutGroup::Row { widgets: imaginate_server_hostname }, - LayoutGroup::Row { widgets: imaginate_refresh_frequency }, - LayoutGroup::Row { widgets: button_widgets }, - ])) + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } + fn send_layout_buttons(&self, responses: &mut VecDeque, layout_target: LayoutTarget) { + responses.add(LayoutMessage::SendLayout { + layout: self.layout_buttons(), + layout_target, + }); + } + + pub fn send_dialog_to_frontend(&self, responses: &mut VecDeque, preferences: &PreferencesMessageHandler) { + self.send_layout(responses, LayoutTarget::DialogColumn1, preferences); + self.send_layout_column_2(responses, LayoutTarget::DialogColumn2); + self.send_layout_buttons(responses, LayoutTarget::DialogButtons); + responses.add(FrontendMessage::DisplayDialog { + icon: Self::ICON.into(), + title: Self::TITLE.into(), + }); } } diff --git a/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs b/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs index 6f3abf47d..e07d33a61 100644 --- a/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs @@ -1,35 +1,71 @@ -use crate::application::{commit_info_localized, release_series}; +use crate::application::commit_info_localized; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; /// A dialog for displaying information on [BuildMetadata] viewable via *Help* > *About Graphite* in the menu bar. pub struct AboutGraphiteDialog { pub localized_commit_date: String, + pub localized_commit_year: String, +} + +impl DialogLayoutHolder for AboutGraphiteDialog { + const ICON: &'static str = "GraphiteLogo"; + const TITLE: &'static str = "About Graphite"; + + fn layout_buttons(&self) -> Layout { + let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()]; + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } + + fn layout_column_2(&self) -> Layout { + let links = [ + ("Website", "Website", "https://graphite.rs"), + ("Volunteer", "Volunteer", "https://graphite.rs/volunteer/"), + ("Credits", "Credits", "https://github.com/GraphiteEditor/Graphite/graphs/contributors"), + ]; + let mut widgets = links + .into_iter() + .map(|(icon, label, url)| { + TextButton::new(label) + .icon(Some(icon.into())) + .no_background(true) + .on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into()) + .widget_holder() + }) + .collect::>(); + + // Cloning here and below seems to be necessary to appease the borrow checker, as far as I can tell. + let localized_commit_year = self.localized_commit_year.clone(); + widgets.push( + TextButton::new("Licenses") + .icon(Some("License".into())) + .no_background(true) + .on_update(move |_| { + DialogMessage::RequestLicensesDialogWithLocalizedCommitDate { + localized_commit_year: localized_commit_year.clone(), + } + .into() + }) + .widget_holder(), + ); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }])) + } } impl LayoutHolder for AboutGraphiteDialog { fn layout(&self) -> Layout { - let links = [ - ("Website", "https://graphite.rs"), - ("Credits", "https://github.com/GraphiteEditor/Graphite/graphs/contributors"), - ("License", "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/LICENSE.txt"), - ("Third-Party Licenses", "/third-party-licenses.txt"), - ]; - let link_widgets = links - .into_iter() - .map(|(label, url)| TextButton::new(label).on_update(|_| FrontendMessage::TriggerVisitLink { url: url.to_string() }.into()).widget_holder()) - .collect(); Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![TextLabel::new("Graphite".to_string()).bold(true).widget_holder()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new(release_series()).widget_holder()], + widgets: vec![TextLabel::new("About this release").bold(true).widget_holder()], }, LayoutGroup::Row { widgets: vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_holder()], }, - LayoutGroup::Row { widgets: link_widgets }, + LayoutGroup::Row { + widgets: vec![TextLabel::new(format!("Copyright © {} Graphite contributors", self.localized_commit_year)).widget_holder()], + }, ])) } } diff --git a/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs b/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs index 2c6cec668..3eb18129e 100644 --- a/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs @@ -2,30 +2,43 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; /// A dialog for confirming the closing of all documents viewable via `file -> close all` in the menu bar. -pub struct CloseAllDocumentsDialog; +pub struct CloseAllDocumentsDialog { + pub unsaved_document_names: Vec, +} + +impl DialogLayoutHolder for CloseAllDocumentsDialog { + const ICON: &'static str = "Warning"; + const TITLE: &'static str = "Closing All Documents"; + + fn layout_buttons(&self) -> Layout { + let widgets = vec![ + TextButton::new("Discard All") + .emphasized(true) + .on_update(|_| { + DialogMessage::CloseDialogAndThen { + followups: vec![PortfolioMessage::CloseAllDocuments.into()], + } + .into() + }) + .widget_holder(), + TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), + ]; + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} impl LayoutHolder for CloseAllDocumentsDialog { fn layout(&self) -> Layout { - let discard = TextButton::new("Discard All") - .min_width(96) - .emphasized(true) - .on_update(|_| { - DialogMessage::CloseDialogAndThen { - followups: vec![PortfolioMessage::CloseAllDocuments.into()], - } - .into() - }) - .widget_holder(); - let cancel = TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(); + let unsaved_list = "• ".to_string() + &self.unsaved_document_names.join("\n• "); Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![TextLabel::new("Close all documents?").multiline(true).widget_holder()], + widgets: vec![TextLabel::new("Save documents before closing them?").bold(true).multiline(true).widget_holder()], }, LayoutGroup::Row { - widgets: vec![TextLabel::new("Unsaved work will be lost!").multiline(true).widget_holder()], + widgets: vec![TextLabel::new(format!("Documents with unsaved changes:\n{unsaved_list}")).multiline(true).widget_holder()], }, - LayoutGroup::Row { widgets: vec![discard, cancel] }, ])) } } diff --git a/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs b/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs index e3efd0fc7..62b80a9e6 100644 --- a/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs @@ -8,13 +8,14 @@ pub struct CloseDocumentDialog { pub document_id: u64, } -impl LayoutHolder for CloseDocumentDialog { - fn layout(&self) -> Layout { - let document_id = self.document_id; +impl DialogLayoutHolder for CloseDocumentDialog { + const ICON: &'static str = "Warning"; + const TITLE: &'static str = "Closing Document"; - let button_widgets = vec![ + fn layout_buttons(&self) -> Layout { + let document_id = self.document_id; + let widgets = vec![ TextButton::new("Save") - .min_width(96) .emphasized(true) .on_update(|_| { DialogMessage::CloseDialogAndThen { @@ -24,7 +25,6 @@ impl LayoutHolder for CloseDocumentDialog { }) .widget_holder(), TextButton::new("Discard") - .min_width(96) .on_update(move |_| { DialogMessage::CloseDialogAndThen { followups: vec![BroadcastEvent::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()], @@ -32,17 +32,22 @@ impl LayoutHolder for CloseDocumentDialog { .into() }) .widget_holder(), - TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), + TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(), ]; + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} + +impl LayoutHolder for CloseDocumentDialog { + fn layout(&self) -> Layout { Layout::WidgetLayout(WidgetLayout::new(vec![ LayoutGroup::Row { - widgets: vec![TextLabel::new("Save changes before closing?").bold(true).widget_holder()], + widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_holder()], }, LayoutGroup::Row { - widgets: vec![TextLabel::new(&self.document_name).multiline(true).widget_holder()], + widgets: vec![TextLabel::new(format!("\"{}\" has unsaved changes", self.document_name)).multiline(true).widget_holder()], }, - LayoutGroup::Row { widgets: button_widgets }, ])) } } diff --git a/editor/src/messages/dialog/simple_dialogs/coming_soon_dialog.rs b/editor/src/messages/dialog/simple_dialogs/coming_soon_dialog.rs index c2b71a46b..aff7f2244 100644 --- a/editor/src/messages/dialog/simple_dialogs/coming_soon_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/coming_soon_dialog.rs @@ -1,45 +1,46 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; -use std::fmt::Write; - /// A dialog to notify users of an unfinished issue, optionally with an issue number. pub struct ComingSoonDialog { pub issue: Option, } -impl LayoutHolder for ComingSoonDialog { - fn layout(&self) -> Layout { - let mut details = "This feature is not implemented yet".to_string(); +impl DialogLayoutHolder for ComingSoonDialog { + const ICON: &'static str = "Delay"; + const TITLE: &'static str = "Coming Soon"; - let mut buttons = vec![TextButton::new("OK") - .emphasized(true) - .min_width(96) - .on_update(|_| FrontendMessage::DisplayDialogDismiss.into()) - .widget_holder()]; + fn layout_buttons(&self) -> Layout { + let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()]; - if let Some(issue) = self.issue { - let _ = write!(details, "— but you can help add it!\nSee issue #{issue} on GitHub."); - buttons.push( - TextButton::new(format!("Issue #{issue}")) - .min_width(96) - .on_update(move |_| { - FrontendMessage::TriggerVisitLink { - url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"), - } - .into() - }) - .widget_holder(), - ); - } - Layout::WidgetLayout(WidgetLayout::new(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Coming soon").bold(true).widget_holder()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new(details).multiline(true).widget_holder()], - }, - LayoutGroup::Row { widgets: buttons }, - ])) + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} + +impl LayoutHolder for ComingSoonDialog { + fn layout(&self) -> Layout { + let header = vec![TextLabel::new("You've stumbled upon a placeholder").bold(true).widget_holder()]; + let row1 = vec![TextLabel::new("This feature is not implemented yet.").widget_holder()]; + + let mut rows = vec![LayoutGroup::Row { widgets: header }, LayoutGroup::Row { widgets: row1 }]; + + if let Some(issue) = self.issue { + let row2 = vec![TextLabel::new("But you can help build it! Visit its issue:").widget_holder()]; + let row3 = vec![TextButton::new(format!("GitHub Issue #{issue}")) + .icon(Some("Website".into())) + .no_background(true) + .on_update(move |_| { + FrontendMessage::TriggerVisitLink { + url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"), + } + .into() + }) + .widget_holder()]; + + rows.push(LayoutGroup::Row { widgets: row2 }); + rows.push(LayoutGroup::Row { widgets: row3 }); + } + + Layout::WidgetLayout(WidgetLayout::new(rows)) } } diff --git a/editor/src/messages/dialog/simple_dialogs/demo_artwork_dialog.rs b/editor/src/messages/dialog/simple_dialogs/demo_artwork_dialog.rs index 53b47442c..946456aab 100644 --- a/editor/src/messages/dialog/simple_dialogs/demo_artwork_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/demo_artwork_dialog.rs @@ -4,27 +4,38 @@ use crate::messages::prelude::*; /// A dialog to let the user browse a gallery of demo artwork that can be opened. pub struct DemoArtworkDialog; +const ARTWORK: [(&str, &str, &str); 2] = [ + ( + "Valley of Spires", + "ThumbnailValleyOfSpires", + "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/valley-of-spires.graphite", + ), + ( + "Just a Potted Cactus", + "ThumbnailJustAPottedCactus", + "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/just-a-potted-cactus.graphite", + ), +]; + +impl DialogLayoutHolder for DemoArtworkDialog { + const ICON: &'static str = "Image"; + const TITLE: &'static str = "Demo Artwork"; + + fn layout_buttons(&self) -> Layout { + let widgets = vec![TextButton::new("Close").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()]; + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} + impl LayoutHolder for DemoArtworkDialog { fn layout(&self) -> Layout { - let artwork = [ - ( - "Valley of Spires", - "ThumbnailValleyOfSpires", - "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/valley-of-spires.graphite", - ), - ( - "Just a Potted Cactus", - "ThumbnailJustAPottedCactus", - "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/just-a-potted-cactus.graphite", - ), - ]; - - let image_widgets = artwork + let images = ARTWORK .into_iter() .map(|(_, thumbnail, _)| ImageLabel::new(thumbnail.to_string()).width(Some("256px".into())).widget_holder()) .collect(); - let button_widgets = artwork + let buttons = ARTWORK .into_iter() .map(|(label, _, url)| { TextButton::new(label) @@ -39,12 +50,6 @@ impl LayoutHolder for DemoArtworkDialog { }) .collect(); - Layout::WidgetLayout(WidgetLayout::new(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Demo Artwork".to_string()).bold(true).widget_holder()], - }, - LayoutGroup::Row { widgets: image_widgets }, - LayoutGroup::Row { widgets: button_widgets }, - ])) + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: images }, LayoutGroup::Row { widgets: buttons }])) } } diff --git a/editor/src/messages/dialog/simple_dialogs/error_dialog.rs b/editor/src/messages/dialog/simple_dialogs/error_dialog.rs index fd85278a3..cf9447541 100644 --- a/editor/src/messages/dialog/simple_dialogs/error_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/error_dialog.rs @@ -7,6 +7,17 @@ pub struct ErrorDialog { pub description: String, } +impl DialogLayoutHolder for ErrorDialog { + const ICON: &'static str = "Warning"; + const TITLE: &'static str = "Error"; + + fn layout_buttons(&self) -> Layout { + let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()]; + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} + impl LayoutHolder for ErrorDialog { fn layout(&self) -> Layout { Layout::WidgetLayout(WidgetLayout::new(vec![ @@ -16,13 +27,6 @@ impl LayoutHolder for ErrorDialog { LayoutGroup::Row { widgets: vec![TextLabel::new(&self.description).multiline(true).widget_holder()], }, - LayoutGroup::Row { - widgets: vec![TextButton::new("OK") - .emphasized(true) - .min_width(96) - .on_update(|_| FrontendMessage::DisplayDialogDismiss.into()) - .widget_holder()], - }, ])) } } diff --git a/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs b/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs new file mode 100644 index 000000000..d1cb8c568 --- /dev/null +++ b/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs @@ -0,0 +1,63 @@ +use crate::messages::layout::utility_types::widget_prelude::*; +use crate::messages::prelude::*; + +pub struct LicensesDialog { + pub localized_commit_year: String, +} + +impl DialogLayoutHolder for LicensesDialog { + const ICON: &'static str = "License12px"; + const TITLE: &'static str = "Licenses"; + + fn layout_buttons(&self) -> Layout { + let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()]; + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } + + fn layout_column_2(&self) -> Layout { + let icons_license_link = "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md"; + let links = [ + ("GraphiteLogo", "Graphite Logo", "https://graphite.rs/logo/"), + ("IconsGrid", "Graphite Icons", icons_license_link), + ("License", "Graphite License", "https://graphite.rs/license/"), + ("License", "Other Licenses", "/third-party-licenses.txt"), + ]; + let widgets = links + .into_iter() + .map(|(icon, label, url)| { + TextButton::new(label) + .icon(Some(icon.into())) + .no_background(true) + .on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into()) + .widget_holder() + }) + .collect(); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }])) + } +} + +impl LayoutHolder for LicensesDialog { + fn layout(&self) -> Layout { + let description = concat!( + "The Graphite logo and brand identity are copyright © [YEAR]\nGraphite Labs, LLC. See \"Graphite Logo\" for usage policy.", + "\n\n", + "The Graphite editor's icons and design assets are copyright\n© [YEAR] Graphite Labs, LLC. See \"Graphite Icons\" for details.", + "\n\n", + "Graphite code is copyright © [YEAR] Graphite contributors\nand is made available under the Apache 2.0 license. See\n\"Graphite License\" for details.", + "\n\n", + "Graphite is distributed with third-party open source code\ndependencies. See \"Other Licenses\" for details.", + ) + .replace("[YEAR]", &self.localized_commit_year); + + Layout::WidgetLayout(WidgetLayout::new(vec![ + LayoutGroup::Row { + widgets: vec![TextLabel::new("Graphite is free, open source software").bold(true).widget_holder()], + }, + LayoutGroup::Row { + widgets: vec![TextLabel::new(description).multiline(true).widget_holder()], + }, + ])) + } +} diff --git a/editor/src/messages/dialog/simple_dialogs/mod.rs b/editor/src/messages/dialog/simple_dialogs/mod.rs index c977de7e2..31cb57064 100644 --- a/editor/src/messages/dialog/simple_dialogs/mod.rs +++ b/editor/src/messages/dialog/simple_dialogs/mod.rs @@ -4,6 +4,7 @@ mod close_document_dialog; mod coming_soon_dialog; mod demo_artwork_dialog; mod error_dialog; +mod licenses_dialog; pub use about_graphite_dialog::AboutGraphiteDialog; pub use close_all_documents_dialog::CloseAllDocumentsDialog; @@ -11,3 +12,4 @@ pub use close_document_dialog::CloseDocumentDialog; pub use coming_soon_dialog::ComingSoonDialog; pub use demo_artwork_dialog::DemoArtworkDialog; pub use error_dialog::ErrorDialog; +pub use licenses_dialog::LicensesDialog; diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index f43b172c2..76fb424bf 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -18,14 +18,13 @@ use serde::{Deserialize, Serialize}; pub enum FrontendMessage { // Display prefix: make the frontend show something, like a dialog DisplayDialog { + title: String, icon: String, }, DisplayDialogDismiss, DisplayDialogPanic { #[serde(rename = "panicInfo")] panic_info: String, - header: String, - description: String, }, DisplayEditableTextbox { text: String, @@ -122,7 +121,17 @@ pub enum FrontendMessage { #[serde(rename = "documentId")] document_id: u64, }, - UpdateDialogDetails { + UpdateDialogButtons { + #[serde(rename = "layoutTarget")] + layout_target: LayoutTarget, + diff: Vec, + }, + UpdateDialogColumn1 { + #[serde(rename = "layoutTarget")] + layout_target: LayoutTarget, + diff: Vec, + }, + UpdateDialogColumn2 { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, diff: Vec, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index b2500a148..aefc1a253 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -289,7 +289,9 @@ impl LayoutMessageHandler { #[remain::sorted] let message = match layout_target { - LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails { layout_target, diff }, + LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff }, + LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff }, + LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff }, LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff }, LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff }, LayoutTarget::GraphViewOverlayButton => FrontendMessage::UpdateGraphViewOverlayButtonLayout { layout_target, diff }, diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 336b8491b..b3cffff98 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -15,8 +15,12 @@ use std::sync::Arc; #[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, Serialize, Deserialize, specta::Type)] #[repr(u8)] pub enum LayoutTarget { - /// Contains the contents of the dialog, including the title and action buttons. Must be shown with the `FrontendMessage::DisplayDialog` message. - DialogDetails, + /// Contains the action buttons at the bottom of the dialog. Must be shown with the `FrontendMessage::DisplayDialog` message. + DialogButtons, + /// Contains the contents of the dialog's primary column. Must be shown with the `FrontendMessage::DisplayDialog` message. + DialogColumn1, + /// Contains the contents of the dialog's secondary column (often blank). Must be shown with the `FrontendMessage::DisplayDialog` message. + DialogColumn2, /// Contains the widgets located directly above the canvas to the right, for example the zoom in and out buttons. DocumentBar, /// Contains the dropdown for design / select / guide mode found on the top left of the canvas. @@ -56,6 +60,40 @@ pub trait LayoutHolder { } } +/// Structs implementing this hold the layout (like [`LayoutHolder`]) for dialog content, but it also requires defining the dialog's title, icon, and action buttons. +pub trait DialogLayoutHolder: LayoutHolder { + const ICON: &'static str; + const TITLE: &'static str; + + fn layout_buttons(&self) -> Layout; + fn send_layout_buttons(&self, responses: &mut VecDeque, layout_target: LayoutTarget) { + responses.add(LayoutMessage::SendLayout { + layout: self.layout_buttons(), + layout_target, + }); + } + + fn layout_column_2(&self) -> Layout { + Layout::default() + } + fn send_layout_column_2(&self, responses: &mut VecDeque, layout_target: LayoutTarget) { + responses.add(LayoutMessage::SendLayout { + layout: self.layout_column_2(), + layout_target, + }); + } + + fn send_dialog_to_frontend(&self, responses: &mut VecDeque) { + self.send_layout(responses, LayoutTarget::DialogColumn1); + self.send_layout_column_2(responses, LayoutTarget::DialogColumn2); + self.send_layout_buttons(responses, LayoutTarget::DialogButtons); + responses.add(FrontendMessage::DisplayDialog { + icon: Self::ICON.into(), + title: Self::TITLE.into(), + }); + } +} + /// Wraps a choice of layout type. The chosen layout contains an arrangement of widgets mounted somewhere specific in the frontend. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, specta::Type)] pub enum Layout { diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index bd4d064ba..25ed64536 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -84,6 +84,9 @@ pub struct TextButton { pub icon: Option, + #[serde(rename = "noBackground")] + pub no_background: bool, + pub emphasized: bool, #[serde(rename = "minWidth")] diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 659a585df..e94dcb74f 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -140,8 +140,7 @@ impl MessageHandler Vec { + self.documents.values().filter(|document| !document.is_saved()).map(|document| document.name.clone()).collect() + } + pub fn generate_new_document_name(&self) -> String { let mut doc_title_numbers = self .ordered_document_iterator() diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 269f65388..919d7ba55 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -80,39 +80,40 @@ impl ToolMetadata for PathTool { impl LayoutHolder for PathTool { fn layout(&self) -> Layout { - if let Some(SingleSelectedPoint { coordinates: DVec2 { x, y }, .. }) = self.tool_data.single_selected_point { - let x_location = NumberInput::new(Some(x)) - .unit(" px") - .label("X") - .min_width(120) - .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) - .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) - .on_update(move |number_input: &NumberInput| { - let new_x = number_input.value.unwrap_or(x); - PathToolMessage::SelectedPointXChanged { new_x }.into() - }) - .widget_holder(); + let coordinates = self.tool_data.single_selected_point.as_ref().map(|point| point.coordinates); + let (x, y) = coordinates.map(|point| (Some(point.x), Some(point.y))).unwrap_or((None, None)); - let y_location = NumberInput::new(Some(y)) - .unit(" px") - .label("Y") - .min_width(120) - .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) - .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) - .on_update(move |number_input: &NumberInput| { - let new_y = number_input.value.unwrap_or(y); - PathToolMessage::SelectedPointYChanged { new_y }.into() - }) - .widget_holder(); + let x_location = NumberInput::new(x) + .unit(" px") + .label("X") + .min_width(120) + .disabled(x.is_none()) + .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) + .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) + .on_update(move |number_input: &NumberInput| { + let new_x = number_input.value.unwrap_or(x.unwrap()); + PathToolMessage::SelectedPointXChanged { new_x }.into() + }) + .widget_holder(); - let seperator = Separator::new(SeparatorType::Unrelated).widget_holder(); + let y_location = NumberInput::new(y) + .unit(" px") + .label("Y") + .min_width(120) + .disabled(y.is_none()) + .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) + .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) + .on_update(move |number_input: &NumberInput| { + let new_y = number_input.value.unwrap_or(y.unwrap()); + PathToolMessage::SelectedPointYChanged { new_y }.into() + }) + .widget_holder(); - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { - widgets: vec![x_location, seperator, y_location], - }])) - } else { - Layout::WidgetLayout(WidgetLayout::default()) - } + let seperator = Separator::new(SeparatorType::Related).widget_holder(); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { + widgets: vec![x_location, seperator, y_location], + }])) } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index de946e5b4..c6e713dd7 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -124,6 +124,8 @@ impl LayoutHolder for SelectTool { let selected_layers_count = self.tool_data.selected_layers_count; let deactivate_alignment = selected_layers_count < 2; + let deactivate_boolean_ops = selected_layers_count < 2; + let deactivate_flip = selected_layers_count < 1; let deactivate_pivot = selected_layers_count < 1; Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { @@ -207,41 +209,48 @@ impl LayoutHolder for SelectTool { }) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - PopoverButton::new("Align", "Coming soon").widget_holder(), + PopoverButton::new("Align", "Coming soon").disabled(deactivate_alignment).widget_holder(), Separator::new(SeparatorType::Section).widget_holder(), IconButton::new("FlipHorizontal", 24) .tooltip("Flip Horizontal") + .disabled(deactivate_flip) .on_update(|_| SelectToolMessage::FlipHorizontal.into()) .widget_holder(), IconButton::new("FlipVertical", 24) .tooltip("Flip Vertical") + .disabled(deactivate_flip) .on_update(|_| SelectToolMessage::FlipVertical.into()) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - PopoverButton::new("Flip", "Coming soon").widget_holder(), + PopoverButton::new("Flip", "Coming soon").disabled(deactivate_flip).widget_holder(), Separator::new(SeparatorType::Section).widget_holder(), IconButton::new("BooleanUnion", 24) - .tooltip("Boolean Union (coming soon)") + .tooltip("Coming Soon: Boolean Union") + .disabled(deactivate_boolean_ops) .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into()) .widget_holder(), IconButton::new("BooleanSubtractFront", 24) - .tooltip("Boolean Subtract Front (coming soon)") + .tooltip("Coming Soon: Boolean Subtract Front") + .disabled(deactivate_boolean_ops) .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into()) .widget_holder(), IconButton::new("BooleanSubtractBack", 24) - .tooltip("Boolean Subtract Back (coming soon)") + .tooltip("Coming Soon: Boolean Subtract Back") + .disabled(deactivate_boolean_ops) .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into()) .widget_holder(), IconButton::new("BooleanIntersect", 24) - .tooltip("Boolean Intersect (coming soon)") + .tooltip("Coming Soon: Boolean Intersect") + .disabled(deactivate_boolean_ops) .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into()) .widget_holder(), IconButton::new("BooleanDifference", 24) - .tooltip("Boolean Difference (coming soon)") + .tooltip("Coming Soon: Boolean Difference") + .disabled(deactivate_boolean_ops) .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into()) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - PopoverButton::new("Boolean", "Coming soon").widget_holder(), + PopoverButton::new("Boolean Operations", "Coming soon").disabled(deactivate_boolean_ops).widget_holder(), ], }])) } diff --git a/frontend/assets/LICENSE.md b/frontend/assets/LICENSE.md index 897464ccb..b2ef7217e 100644 --- a/frontend/assets/LICENSE.md +++ b/frontend/assets/LICENSE.md @@ -1,7 +1,5 @@ -Copyright (c) 2021-2023 Keavon Chambers +Copyright (c) 2021-2023 Graphite Labs, LLC. -The design assets in this directory (including SVG code for icons and logos) are NOT licensed under the Apache 2.0 license terms applied to other Graphite source code files. This directory and its entire contents are excluded from the Apache 2.0 source code license, and copyrights are held by the author for the creative works contained as files herein. +The design assets in this directory (including SVG code for icons and logos) are NOT licensed under the Apache 2.0 license terms applied to other Graphite source code files. This directory and its entire contents are excluded from the Apache 2.0 source code license, and full copyright is held by the rightsholder for the creative works contained as files herein. -Parties interested in using Graphite source code in a capacity that deploys the Graphite editor reference frontend are advised to substitute all assets and "Graphite" branding or otherwise arrange written permission from the rightsholder. The recommended use case for adopting Graphite open source code is to develop one's own unique frontend user interface implementation that integrates Graphite's backend technology. - -The author and rightsholder, Keavon Chambers, may be reached through the email address listed at https://graphite.rs/contact/ or https://keavon.com. +Parties interested in using Graphite source code in a capacity that deploys the Graphite editor reference frontend are advised to substitute all assets and "Graphite" branding or otherwise arrange written permission from the rightsholder (see https://graphite.rs/contact/ for contact info). The recommended use case for adopting Graphite open source code is to develop one's own unique frontend user interface implementation that integrates Graphite's backend technology. diff --git a/frontend/assets/icon-12px-solid/delay.svg b/frontend/assets/icon-12px-solid/delay.svg new file mode 100644 index 000000000..1767c89e3 --- /dev/null +++ b/frontend/assets/icon-12px-solid/delay.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/icon-12px-solid/failure.svg b/frontend/assets/icon-12px-solid/failure.svg new file mode 100644 index 000000000..c43014bce --- /dev/null +++ b/frontend/assets/icon-12px-solid/failure.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/icon-12px-solid/license-12px.svg b/frontend/assets/icon-12px-solid/license-12px.svg new file mode 100644 index 000000000..18b4d69bc --- /dev/null +++ b/frontend/assets/icon-12px-solid/license-12px.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/icon-16px-solid/credits.svg b/frontend/assets/icon-16px-solid/credits.svg new file mode 100644 index 000000000..b5e7a987c --- /dev/null +++ b/frontend/assets/icon-16px-solid/credits.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/icon-16px-solid/icons-grid.svg b/frontend/assets/icon-16px-solid/icons-grid.svg new file mode 100644 index 000000000..52452d6aa --- /dev/null +++ b/frontend/assets/icon-16px-solid/icons-grid.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/icon-16px-solid/license.svg b/frontend/assets/icon-16px-solid/license.svg new file mode 100644 index 000000000..44fbfa6b0 --- /dev/null +++ b/frontend/assets/icon-16px-solid/license.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/icon-16px-solid/volunteer.svg b/frontend/assets/icon-16px-solid/volunteer.svg new file mode 100644 index 000000000..4e178c78a --- /dev/null +++ b/frontend/assets/icon-16px-solid/volunteer.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/icon-16px-solid/website.svg b/frontend/assets/icon-16px-solid/website.svg new file mode 100644 index 000000000..521db204c --- /dev/null +++ b/frontend/assets/icon-16px-solid/website.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index b98c8877d..2b368797d 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -44,7 +44,7 @@ createPanicManager(editor, dialog); createPersistenceManager(editor, portfolio); let dragManagerDestructor = createDragManager(); - let inputManagerDestructor = createInputManager(editor, dialog, portfolio, fullscreen); + let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen); onMount(() => { // Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready diff --git a/frontend/src/components/floating-menus/DialogModal.svelte b/frontend/src/components/floating-menus/DialogModal.svelte index 84a59d1ed..dda5d0faf 100644 --- a/frontend/src/components/floating-menus/DialogModal.svelte +++ b/frontend/src/components/floating-menus/DialogModal.svelte @@ -1,11 +1,14 @@ + - - - - - - - {#if $dialog.widgets.layout.length > 0} - + + + + {$dialog.title} + + + + {#if $dialog.column1.layout.length > 0} + {/if} - {#if ($dialog.crashDialogButtons?.length || NaN) > 0} - - {#each $dialog.crashDialogButtons || [] as button, index (index)} - button.callback?.()} {...button.props} /> - {/each} - + {#if $dialog.panicDetails} +
+
The editor crashed — sorry about that
+
Please report this by filing an issue on GitHub:
+
window.open(githubUrl($dialog.panicDetails), "_blank")} />
+
Reload the editor to continue. If this occurs
immediately on repeated reloads, clear storage:
+
+ { + await wipeDocuments(); + window.location.reload(); + }} + /> +
+
{/if}
+ {#if $dialog.column2.layout.length > 0} + + + + {/if} +
+ + {#if $dialog.buttons.layout.length > 0} + + {/if} + {#if $dialog.panicDetails} + navigator.clipboard.writeText($dialog.panicDetails)} /> + window.location.reload()} /> + {/if}
@@ -50,32 +81,45 @@ > .floating-menu-container > .floating-menu-content { pointer-events: auto; - padding: 24px; + padding: 0; } - .icon-column { - margin-right: 24px; + .header-area, + .footer-area { + background: var(--color-1-nearblack); + } + + .header-area, + .footer-area, + .content { + padding: 16px 24px; + } + + .header-area { + border-radius: 4px 4px 0 0; .icon-label { - width: 80px; - height: 80px; + width: 24px; + height: 24px; + } - &.file, - &.copy { - width: 60px; - - svg { - width: 80px; - height: 80px; - margin: 0 -10px; - } - } + .text-label { + margin-left: 12px; + line-height: 24px; } } - .main-column { + .content { margin: -4px 0; + .column-1 + .column-2 { + margin-left: 48px; + + .text-button { + justify-content: left; + } + } + .details.text-label { -webkit-user-select: text; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release user-select: text; @@ -84,13 +128,22 @@ height: auto; } + .radio-input button { + flex-grow: 1; + } + + // Used by the "Open Demo Artwork" dialog .image-label { border-radius: 2px; } + } - .panic-buttons-row { - height: 32px; - align-items: center; + .footer-area { + border-radius: 0 0 4px 4px; + justify-content: right; + + .text-button { + min-width: 96px; } } } diff --git a/frontend/src/components/layout/FloatingMenu.svelte b/frontend/src/components/layout/FloatingMenu.svelte index 8201b6bdc..1013942f7 100644 --- a/frontend/src/components/layout/FloatingMenu.svelte +++ b/frontend/src/components/layout/FloatingMenu.svelte @@ -4,7 +4,7 @@