Dialog redesign and content revamp (#1409)

* Revamp the content and design of dialogs

* Add the Licenses dialog
This commit is contained in:
Keavon Chambers 2023-09-01 01:58:20 -07:00 committed by GitHub
parent 5acb2cff06
commit a112ab27cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 798 additions and 453 deletions

View file

@ -8,15 +8,19 @@
<h1 align="center">Redefining state-of-the-art graphics editing</h1>
**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

View file

@ -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)]

View file

@ -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 {

View file

@ -29,12 +29,16 @@ pub enum DialogMessage {
RequestAboutGraphiteDialog,
RequestAboutGraphiteDialogWithLocalizedCommitDate {
localized_commit_date: String,
localized_commit_year: String,
},
RequestComingSoonDialog {
issue: Option<i32>,
},
RequestDemoArtworkDialog,
RequestExportDialog,
RequestLicensesDialogWithLocalizedCommitDate {
localized_commit_year: String,
},
RequestNewDocumentDialog,
RequestPreferencesDialog,
}

View file

@ -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<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessag
#[remain::sorted]
match message {
#[remain::unsorted]
DialogMessage::ExportDialog(message) => 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<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessag
.collect();
self.export_dialog = ExportDialogMessageHandler {
file_name: document.name.clone(),
scale_factor: 1.,
artboards,
has_selection: document.selected_layers().next().is_some(),
..Default::default()
};
self.export_dialog.send_layout(responses, LayoutTarget::DialogDetails);
responses.add(FrontendMessage::DisplayDialog { icon: "File".to_string() });
self.export_dialog.send_dialog_to_frontend(responses);
}
}
DialogMessage::RequestLicensesDialogWithLocalizedCommitDate { localized_commit_year } => {
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);
}
}
}

View file

@ -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),

View file

@ -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<ExportDialogMessage, ()> for ExportDialogMessageHandler {
fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque<Message>, _data: ()) {
impl MessageHandler<ExportDialogMessage, &PortfolioMessageHandler> for ExportDialogMessageHandler {
fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque<Message>, 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<ExportDialogMessage, ()> 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 },
]))
}
}

View file

@ -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;

View file

@ -42,26 +42,47 @@ impl MessageHandler<NewDocumentDialogMessage, ()> 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 },
]))
}
}

View file

@ -11,19 +11,18 @@ impl MessageHandler<PreferencesDialogMessage, &PreferencesMessageHandler> 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<Message>, 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<Message>, 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<Message>, 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<Message>, layout_target: LayoutTarget) {
responses.add(LayoutMessage::SendLayout {
layout: self.layout_buttons(),
layout_target,
});
}
pub fn send_dialog_to_frontend(&self, responses: &mut VecDeque<Message>, 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(),
});
}
}

View file

@ -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::<Vec<_>>();
// 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()],
},
]))
}
}

View file

@ -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<String>,
}
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] },
]))
}
}

View file

@ -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 },
]))
}
}

View file

@ -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<i32>,
}
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))
}
}

View file

@ -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 }]))
}
}

View file

@ -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()],
},
]))
}
}

View file

@ -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()],
},
]))
}
}

View file

@ -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;

View file

@ -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<WidgetDiff>,
},
UpdateDialogColumn1 {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateDialogColumn2 {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,

View file

@ -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 },

View file

@ -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<Message>, 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<Message>, layout_target: LayoutTarget) {
responses.add(LayoutMessage::SendLayout {
layout: self.layout_column_2(),
layout_target,
});
}
fn send_dialog_to_frontend(&self, responses: &mut VecDeque<Message>) {
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 {

View file

@ -84,6 +84,9 @@ pub struct TextButton {
pub icon: Option<String>,
#[serde(rename = "noBackground")]
pub no_background: bool,
pub emphasized: bool,
#[serde(rename = "minWidth")]

View file

@ -140,8 +140,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
document_name: target_document.name.clone(),
document_id,
};
dialog.send_layout(responses, LayoutTarget::DialogDetails);
responses.add(FrontendMessage::DisplayDialog { icon: "File".to_string() });
dialog.send_dialog_to_frontend(responses);
// Select the document being closed
responses.add(PortfolioMessage::SelectDocument { document_id });
@ -598,6 +597,10 @@ impl PortfolioMessageHandler {
self.active_document_id
}
pub fn unsaved_document_names(&self) -> Vec<String> {
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()

View file

@ -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],
}]))
}
}

View file

@ -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(),
],
}]))
}

View file

@ -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.

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="m6,1c2.76,0,5,2.24,5,5s-2.24,5-5,5S1,8.76,1,6,3.24,1,6,1m0-1C2.68,0,0,2.69,0,6s2.68,6,6,6,6-2.68,6-6S9.31,0,6,0h0Zm2.47,8.47c.2-.2.2-.51,0-.71l-1.97-1.97v-2.79c0-.28-.22-.5-.5-.5s-.5.22-.5.5v3c0,.13.05.26.15.35l2.12,2.12c.1.1.23.15.35.15s.26-.05.35-.15z" />
</svg>

After

Width:  |  Height:  |  Size: 336 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="m6,1c2.76,0,5,2.24,5,5s-2.24,5-5,5S1,8.76,1,6c0-2.76,2.24-5,5-5m0-1C2.69,0,0,2.69,0,6s2.69,6,6,6,6-2.69,6-6S9.31,0,6,0m2.83,3.88l-.71-.71-2.12,2.12-2.12-2.12-.71.71,2.12,2.12-2.12,2.12.71.71,2.12-2.12,2.12,2.12.71-.71-2.12-2.12,2.12-2.12z" />
</svg>

After

Width:  |  Height:  |  Size: 321 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="m12,1.63C11.94.7,11.17,0,10.25,0h-6.25C2.9,0,2,.9,2,2v6.5H0v1.75c0,.92.7,1.69,1.62,1.75h6.12c.95-.05,1.7-.8,1.75-1.75v-6.25h2.5V1.63ZM1.63,11c-.37-.06-.63-.38-.62-.75v-.75h5v.75c0,.26.07.52.19.75H1.63Zm6.12,0c-.39-.05-.7-.36-.75-.75v-1.75h-4V2c0-.55.45-1,1-1h4.71c-.14.31-.21.66-.21,1v8.25c-.05.39-.36.7-.75.75m3.25-8h-1.5v-1c0-.3.07-1,.75-1,.37,0,.69.26.75.62v1.37Zm-3.75-.5h-3c-.14,0-.25.11-.25.25s.11.25.25.25h3c.14,0,.25-.11.25-.25s-.11-.25-.25-.25m0,2h-3c-.14,0-.25.11-.25.25s.11.25.25.25h3c.14,0,.25-.11.25-.25s-.11-.25-.25-.25m0,2h-3c-.14,0-.25.11-.25.25s.11.25.25.25h3c.14,0,.25-.11.25-.25s-.11-.25-.25-.25z" />
</svg>

After

Width:  |  Height:  |  Size: 698 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m2,1h6v2H2V1Zm0,12h7v2H2v-2ZM1,5h3v2H1v-2Zm-1,4h5v2H0v-2Zm7,0h4v2h-4v-2ZM10,1h4v2h-4V1Zm1,12h3v2h-3v-2ZM6,5h9v2H6v-2Zm7,4h3v2h-3v-2z" />
</svg>

After

Width:  |  Height:  |  Size: 215 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m6,7H2c-.55,0-1-.45-1-1V2c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1ZM2,2v4h4V2H2Zm4,13H2c-.55,0-1-.45-1-1v-4c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1Zm-4-5v4h4v-4H2Zm12-3h-4c-.55,0-1-.45-1-1V2c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1Zm-4-5v4h4V2h-4Zm4,13h-4c-.55,0-1-.45-1-1v-4c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1Zm-4-5v4h4v-4h-4z" />
</svg>

After

Width:  |  Height:  |  Size: 457 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m16,2c0-1.1-.9-2-2-2H5.5c-1.38,0-2.5,1.12-2.5,2.5v9.5H0v2c0,1.1.9,2,2,2h9c1.1,0,2-.9,2-2V5h3v-3ZM2,15c-.55,0-1-.45-1-1v-1h8v1c0,.35.09.7.27,1H2Zm9,0c-.55,0-1-.45-1-1v-2h-6V2.5c0-.83.67-1.5,1.5-1.5h6.82c-.22.47-.33.98-.32,1.5v11.5c0,.55-.45,1-1,1m4-11h-2v-1.5c0-1.5.75-1.5,1-1.5.55,0,1,.45,1,1v2Zm-4.5-1h-5c-.28,0-.5.22-.5.5s.22.5.5.5h5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5m0,3h-5c-.28,0-.5.22-.5.5s.22.5.5.5h5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5m0,3h-5c-.28,0-.5.22-.5.5s.22.5.5.5h5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5z" />
</svg>

After

Width:  |  Height:  |  Size: 589 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m4.56,15.92c1.01.06,1.28.08,2.53.08,2.95,0,3.89.02,6.12-.08.23-1.32.23-2.68,0-4-.44-2.54.53-2.46,1.4-4.45,1.54-3.49,1.39-4.97,1.39-4.97.07-.76-.48-1.43-1.24-1.51-.75.08-1.3.75-1.24,1.51-.14,1.73-1.05,3.29-2.49,4.26-2.36,1.66-5.5.45-8.39,2.83C.67,11.21.51,15.02.49,15.51c.82.19,1.65.29,2.49.32.08-1.09.35-2.16.8-3.16,0,0,.75.61.77,3.25ZM7.65,0c1.74,0,3.15,1.41,3.15,3.15s-1.41,3.15-3.15,3.15-3.15-1.41-3.15-3.15S5.91,0,7.65,0z" />
</svg>

After

Width:  |  Height:  |  Size: 508 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m8,0C3.59,0,0,3.59,0,8s3.59,8,8,8,8-3.59,8-8S12.41,0,8,0Zm6.1,5h-2.39c-.26-1.33-.69-2.47-1.24-3.33,1.58.62,2.88,1.82,3.63,3.33Zm.7,3c0,.7-.11,1.37-.31,2h-2.62c.08-.64.12-1.31.12-2s-.04-1.36-.12-2h2.62c.2.63.31,1.3.31,2Zm-13.6,0c0-.7.11-1.37.31-2h2.62c-.08.64-.12,1.31-.12,2s.04,1.36.12,2H1.51c-.2-.63-.31-1.3-.31-2Zm3.9,0c0-.71.06-1.37.14-2h5.52c.08.63.14,1.29.14,2s-.06,1.37-.14,2h-5.52c-.08-.63-.14-1.29-.14-2ZM7.49,1.23c.17-.01.34-.03.51-.03s.34.01.51.03c.84.41,1.65,1.79,2.08,3.77h-5.17c.43-1.98,1.24-3.37,2.08-3.77Zm-1.96.44c-.55.86-.98,2-1.24,3.33H1.9c.75-1.51,2.05-2.71,3.63-3.33ZM1.9,11h2.39c.26,1.33.69,2.47,1.24,3.33-1.58-.62-2.88-1.82-3.63-3.33Zm6.61,3.78c-.17.01-.34.03-.51.03s-.34-.01-.51-.03c-.84-.4-1.65-1.79-2.08-3.78h5.17c-.43,1.98-1.24,3.37-2.08,3.78Zm1.96-.44c.55-.86.98-2,1.24-3.33h2.39c-.75,1.51-2.05,2.71-3.63,3.33z" />
</svg>

After

Width:  |  Height:  |  Size: 920 B

View file

@ -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

View file

@ -1,11 +1,14 @@
<script lang="ts">
import { getContext, onMount } from "svelte";
import { githubUrl } from "@graphite/io-managers/panic";
import { wipeDocuments } from "@graphite/io-managers/persistence";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
import type { DialogState } from "@graphite/state-providers/dialog";
@ -20,24 +23,52 @@
});
</script>
<!-- TODO: Use https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog for improved accessibility -->
<FloatingMenu open={true} class="dialog-modal" type="Dialog" direction="Center" bind:this={self} data-dialog-modal>
<LayoutRow>
<LayoutCol class="icon-column">
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
</LayoutCol>
<LayoutCol class="main-column">
{#if $dialog.widgets.layout.length > 0}
<WidgetLayout layout={$dialog.widgets} class="details" />
<LayoutRow class="header-area">
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
<TextLabel>{$dialog.title}</TextLabel>
</LayoutRow>
<LayoutRow class="content">
<LayoutCol class="column-1">
{#if $dialog.column1.layout.length > 0}
<WidgetLayout layout={$dialog.column1} class="details" />
{/if}
{#if ($dialog.crashDialogButtons?.length || NaN) > 0}
<LayoutRow class="panic-buttons-row">
{#each $dialog.crashDialogButtons || [] as button, index (index)}
<TextButton action={() => button.callback?.()} {...button.props} />
{/each}
</LayoutRow>
{#if $dialog.panicDetails}
<div class="widget-layout details">
<div class="widget-row"><TextLabel bold={true}>The editor crashed sorry about that</TextLabel></div>
<div class="widget-row"><TextLabel>Please report this by filing an issue on GitHub:</TextLabel></div>
<div class="widget-row"><TextButton label="Report Bug" icon="Warning" noBackground={true} action={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
<div class="widget-row"><TextLabel multiline={true}>Reload the editor to continue. If this occurs<br />immediately on repeated reloads, clear storage:</TextLabel></div>
<div class="widget-row">
<TextButton
label="Clear Saved Documents"
icon="Trash"
noBackground={true}
action={async () => {
await wipeDocuments();
window.location.reload();
}}
/>
</div>
</div>
{/if}
</LayoutCol>
{#if $dialog.column2.layout.length > 0}
<LayoutCol class="column-2">
<WidgetLayout layout={$dialog.column2} class="details" />
</LayoutCol>
{/if}
</LayoutRow>
<LayoutRow class="footer-area">
{#if $dialog.buttons.layout.length > 0}
<WidgetLayout layout={$dialog.buttons} class="details" />
{/if}
{#if $dialog.panicDetails}
<TextButton label="Copy Error Log" action={() => navigator.clipboard.writeText($dialog.panicDetails)} />
<TextButton label="Reload" emphasized={true} action={() => window.location.reload()} />
{/if}
</LayoutRow>
</FloatingMenu>
@ -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;
}
}
}

View file

@ -4,7 +4,7 @@
</script>
<script lang="ts">
import { afterUpdate, createEventDispatcher, tick } from "svelte";
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@ -103,9 +103,42 @@
wasOpen = isOpen;
}
// Gets the client bounds of the elements and apply relevant styles to them
// TODO: Use DOM attribute bindings more whilst not causing recursive updates
onMount(() => {
// Measure the content and round up its width and height to the nearest even integer.
// This solves antialiasing issues when the content isn't cleanly divisible by 2 and gets translated by (-50%, -50%) causing all its content to be blurry.
const floatingMenuContentDiv = floatingMenuContent?.div();
if (type === "Dialog" && floatingMenuContentDiv) {
// TODO: Also use https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver to detect any changes which may affect the size of the content.
// TODO: The current method only notices when the dialog size increases but can't detect when it decreases.
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
let { width, height } = entry.contentRect;
width = Math.ceil(width);
if (width % 2 === 1) width += 1;
height = Math.ceil(height);
if (height % 2 === 1) height += 1;
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
floatingMenuContentDiv.style.setProperty("min-width", width === 0 ? "unset" : `${width}px`);
floatingMenuContentDiv.style.setProperty("min-height", height === 0 ? "unset" : `${height}px`);
});
});
resizeObserver.observe(floatingMenuContentDiv);
}
});
afterUpdate(() => {
// Remove the size constraint after the content updates so the resize observer can measure the content and reapply a newly calculated one
const floatingMenuContentDiv = floatingMenuContent?.div();
if (type === "Dialog" && floatingMenuContentDiv) {
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
floatingMenuContentDiv.style.setProperty("min-width", "unset");
floatingMenuContentDiv.style.setProperty("min-height", "unset");
}
// Gets the client bounds of the elements and apply relevant styles to them
// TODO: Use DOM attribute bindings more whilst not causing recursive updates
// Turning measuring on and off both causes the component to change, which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop)
if (!measuringOngoingGuard) positionAndStyleFloatingMenu();
});

View file

@ -69,13 +69,6 @@
editor.instance.updateLayout(layoutTarget, widgets[index].widgetId, value);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// function exclude<T extends Record<string, any>>(props: T, additional?: (keyof T)[]): Pick<T, Exclude<keyof T, "kind" | (typeof additional extends Array<infer K> ? K : never)>> {
// const exclusions = ["kind", ...(additional || [])];
// return Object.fromEntries(Object.entries(props).filter((entry) => !exclusions.includes(entry[0]))) as any;
// }
// TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283
function exclude<T extends object>(props: T, additional?: (keyof T)[]): Omit<T, typeof additional extends Array<infer K> ? K : never> {
const exclusions = ["kind", ...(additional || [])];

View file

@ -7,6 +7,7 @@
export let label: string;
export let icon: IconName | undefined = undefined;
export let emphasized = false;
export let noBackground = false;
export let minWidth = 0;
export let disabled = false;
export let tooltip: string | undefined = undefined;
@ -21,6 +22,7 @@
class="text-button"
class:emphasized
class:disabled
class:no-background={noBackground}
class:sharp-right-corners={sharpRightCorners}
style:min-width={minWidth > 0 ? `${minWidth}px` : ""}
title={tooltip}
@ -76,10 +78,26 @@
}
}
&.text-button + .text-button {
&.no-background {
&:not(:hover) {
background: none;
}
.icon-label {
margin-right: 4px;
}
}
.widget-row > & + .text-button,
.layout-row > & + .text-button {
margin-left: 8px;
}
.widget-column > & + .text-button,
.layout-column > & + .text-button {
margin-top: 8px;
}
.icon-label {
position: relative;
left: -4px;

View file

@ -120,7 +120,7 @@
<table>
<tr>
<td>
<TextButton label="New Document" icon="File" action={() => editor.instance.newDocumentDialog()} />
<TextButton label="New Document" icon="File" noBackground={true} action={() => editor.instance.newDocumentDialog()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
@ -128,7 +128,7 @@
</tr>
<tr>
<td>
<TextButton label="Open Document" icon="Folder" action={() => editor.instance.openDocument()} />
<TextButton label="Open Document" icon="Folder" noBackground={true} action={() => editor.instance.openDocument()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
@ -136,7 +136,7 @@
</tr>
<tr>
<td colspan="2">
<TextButton label="Open Demo Artwork" icon="Image" action={() => editor.instance.demoArtworkDialog()} />
<TextButton label="Open Demo Artwork" icon="Image" noBackground={true} action={() => editor.instance.demoArtworkDialog()} />
</td>
</tr>
</table>
@ -289,10 +289,6 @@
td {
padding: 0;
}
.text-button:not(:hover) {
background: none;
}
}
}
}

View file

@ -1,6 +1,7 @@
import { get } from "svelte/store";
import { type DialogState } from "@graphite/state-providers/dialog";
import { type DocumentState } from "@graphite/state-providers/document";
import { type FullscreenState } from "@graphite/state-providers/fullscreen";
import { type PortfolioState } from "@graphite/state-providers/portfolio";
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
@ -16,7 +17,7 @@ type EventListenerTarget = {
removeEventListener: typeof window.removeEventListener;
};
export function createInputManager(editor: Editor, dialog: DialogState, document: PortfolioState, fullscreen: FullscreenState): () => void {
export function createInputManager(editor: Editor, dialog: DialogState, portfolio: PortfolioState, document: DocumentState, fullscreen: FullscreenState): () => void {
const app = window.document.querySelector("[data-app-container]") as HTMLElement | undefined;
app?.focus();
@ -58,8 +59,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
// Keyboard events
async function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): Promise<boolean> {
// Don't redirect when a modal is covering the workspace
if (get(dialog).visible) return false;
// Don't redirect when a modal, or the overlaid graph, is covering the workspace
if (get(dialog).visible || get(document).graphViewOverlayOpen) return false;
const key = await getLocalizedScanCode(e);
@ -239,7 +240,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
}
async function onBeforeUnload(e: BeforeUnloadEvent): Promise<void> {
const activeDocument = get(document).documents[get(document).activeDocumentIndex];
const activeDocument = get(portfolio).documents[get(portfolio).activeDocumentIndex];
if (activeDocument && !activeDocument.isAutoSaved) editor.instance.triggerAutoSave(activeDocument.id);
// Skip the message if the editor crashed, since work is already lost
@ -248,7 +249,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
// Skip the message during development, since it's annoying when testing
if (await editor.instance.inDevelopmentMode()) return;
const allDocumentsSaved = get(document).documents.reduce((acc, doc) => acc && doc.isSaved, true);
const allDocumentsSaved = get(portfolio).documents.reduce((acc, doc) => acc && doc.isSaved, true);
if (!allDocumentsSaved) {
e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?";
e.preventDefault();

View file

@ -2,10 +2,10 @@ import { type Editor } from "@graphite/wasm-communication/editor";
import { TriggerAboutGraphiteLocalizedCommitDate } from "@graphite/wasm-communication/messages";
export function createLocalizationManager(editor: Editor): void {
function localizeTimestamp(utc: string): string {
function localizeTimestamp(utc: string): { timestamp: string, year: string } {
// Timestamp
const date = new Date(utc);
if (Number.isNaN(date.getTime())) return utc;
if (Number.isNaN(date.getTime())) return { timestamp: utc, year: `${new Date().getFullYear()}` };
const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
.formatToParts(new Date())
@ -14,12 +14,12 @@ export function createLocalizationManager(editor: Editor): void {
const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
const timeString = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
const timezoneNameString = timezoneName?.value;
return `${dateString} ${timeString} ${timezoneNameString}`;
return { timestamp: `${dateString} ${timeString} ${timezoneNameString}`, year: `${date.getFullYear()}` };
}
// Subscribe to process backend event
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (triggerAboutGraphiteLocalizedCommitDate) => {
const localized = localizeTimestamp(triggerAboutGraphiteLocalizedCommitDate.commitDate);
editor.instance.requestAboutGraphiteDialogWithLocalizedCommitDate(localized);
editor.instance.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
});
}

View file

@ -1,11 +1,8 @@
import { wipeDocuments } from "@graphite/io-managers/persistence";
import { type DialogState } from "@graphite/state-providers/dialog";
import { type IconName } from "@graphite/utility-functions/icons";
import { browserVersion, operatingSystem } from "@graphite/utility-functions/platform";
import { stripIndents } from "@graphite/utility-functions/strip-indents";
import { type Editor } from "@graphite/wasm-communication/editor";
import type { TextLabel } from "@graphite/wasm-communication/messages";
import { type TextButtonWidget, type WidgetLayout, Widget, DisplayDialogPanic } from "@graphite/wasm-communication/messages";
import { DisplayDialogPanic } from "@graphite/wasm-communication/messages";
export function createPanicManager(editor: Editor, dialogState: DialogState): void {
// Code panic dialog and console error
@ -19,45 +16,11 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
// eslint-disable-next-line no-console
console.error(panicDetails);
const crashDialog = prepareCrashDialog(displayDialogPanic.header, displayDialogPanic.description, panicDetails);
dialogState.createCrashDialog(...crashDialog);
dialogState.createCrashDialog(panicDetails);
});
}
function prepareCrashDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
const headerLabel: TextLabel = { kind: "TextLabel", value: header, disabled: false, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" };
const detailsLabel: TextLabel = { kind: "TextLabel", value: details, disabled: false, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" };
const widgets: WidgetLayout = {
layout: [{ rowWidgets: [new Widget(headerLabel, 0n)] }, { rowWidgets: [new Widget(detailsLabel, 1n)] }],
layoutTarget: undefined,
};
const reloadButton: TextButtonWidget = {
callback: async () => window.location.reload(),
props: { kind: "TextButton", label: "Reload", emphasized: true, minWidth: 96 },
};
const copyErrorLogButton: TextButtonWidget = {
callback: async () => navigator.clipboard.writeText(panicDetails),
props: { kind: "TextButton", label: "Copy Error Log", emphasized: false, minWidth: 96 },
};
const reportOnGithubButton: TextButtonWidget = {
callback: async () => window.open(githubUrl(panicDetails), "_blank"),
props: { kind: "TextButton", label: "Report Bug", emphasized: false, minWidth: 96 },
};
const clearPersistedDataButton: TextButtonWidget = {
callback: async () => {
await wipeDocuments();
window.location.reload();
},
props: { kind: "TextButton", label: "Clear Saved Data", emphasized: false, minWidth: 96 },
};
const crashDialogButtons = [reloadButton, copyErrorLogButton, reportOnGithubButton, clearPersistedDataButton];
return ["Warning", widgets, crashDialogButtons];
}
function githubUrl(panicDetails: string): string {
export function githubUrl(panicDetails: string): string {
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
let body = stripIndents`

View file

@ -2,23 +2,25 @@ import {writable} from "svelte/store";
import { type IconName } from "@graphite/utility-functions/icons";
import { type Editor } from "@graphite/wasm-communication/editor";
import { type TextButtonWidget, type WidgetLayout, defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogDetails, patchWidgetLayout } from "@graphite/wasm-communication/messages";
import { defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogButtons, UpdateDialogColumn1, UpdateDialogColumn2, patchWidgetLayout } from "@graphite/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createDialogState(editor: Editor) {
const { subscribe, update } = writable({
visible: false,
title: "",
icon: "" as IconName,
widgets: defaultWidgetLayout(),
buttons: defaultWidgetLayout(),
column1: defaultWidgetLayout(),
column2: defaultWidgetLayout(),
// Special case for the crash dialog because we cannot handle button widget callbacks from Rust once the editor instance has panicked
crashDialogButtons: undefined as undefined | TextButtonWidget[],
panicDetails: "",
});
function dismissDialog(): void {
update((state) => {
// Disallow dismissing the crash dialog since it can confuse users why the app stopped responding if they dismiss it without realizing what it means
if (!state.crashDialogButtons) state.visible = false;
if (state.panicDetails === "") state.visible = false;
return state;
});
@ -26,12 +28,18 @@ export function createDialogState(editor: Editor) {
// Creates a crash dialog from JS once the editor has panicked.
// Normal dialogs are created in the Rust backend, but for the crash dialog, the editor instance has panicked so it cannot respond to widget callbacks.
function createCrashDialog(icon: IconName, widgets: WidgetLayout, crashDialogButtons: TextButtonWidget[]): void {
function createCrashDialog(panicDetails: string): void {
update((state) => {
state.visible = true;
state.icon = icon;
state.widgets = widgets;
state.crashDialogButtons = crashDialogButtons;
state.icon = "Failure";
state.title = "Crash";
state.panicDetails = panicDetails;
state.column1 = defaultWidgetLayout();
state.column2 = defaultWidgetLayout();
state.buttons = defaultWidgetLayout();
return state;
});
}
@ -40,15 +48,31 @@ export function createDialogState(editor: Editor) {
editor.subscriptions.subscribeJsMessage(DisplayDialog, (displayDialog) => {
update((state) => {
state.visible = true;
state.title = displayDialog.title;
state.icon = displayDialog.icon;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateDialogDetails, (updateDialogDetails) => {
editor.subscriptions.subscribeJsMessage(UpdateDialogButtons, (updateDialogButtons) => {
update((state) => {
patchWidgetLayout(state.widgets, updateDialogDetails);
patchWidgetLayout(state.buttons, updateDialogButtons);
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn1, (updateDialogColumn1) => {
update((state) => {
patchWidgetLayout(state.column1, updateDialogColumn1);
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn2, (updateDialogColumn2) => {
update((state) => {
patchWidgetLayout(state.column2, updateDialogColumn2);
state.crashDialogButtons = undefined;
return state;
});
});
@ -57,7 +81,7 @@ export function createDialogState(editor: Editor) {
return {
subscribe,
dismissDialog,
createCrashDialog: createCrashDialog,
createCrashDialog,
};
}
export type DialogState = ReturnType<typeof createDialogState>;

View file

@ -1,4 +1,3 @@
import {tick} from "svelte";
import {writable} from "svelte/store";
import { type Editor } from "@graphite/wasm-communication/editor";

View file

@ -90,10 +90,15 @@ export function createPortfolioState(editor: Editor) {
const backgroundColor = mime.endsWith("jpeg") ? "white" : undefined;
// Rasterize the SVG to an image file
const blob = await rasterizeSVG(svg, size.x, size.y, mime, backgroundColor);
try {
const blob = await rasterizeSVG(svg, size.x, size.y, mime, backgroundColor);
// Have the browser download the file to the user's disk
downloadFileBlob(name, blob);
} catch {
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
}
// Have the browser download the file to the user's disk
downloadFileBlob(name, blob);
});
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
updateImageData.imageData.forEach(async (element) => {

View file

@ -11,9 +11,11 @@ const GRAPHICS = {
import Add from "@graphite-frontend/assets/icon-12px-solid/add.svg";
import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.svg";
import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg";
import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg";
import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg";
import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg";
import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg";
import Failure from "@graphite-frontend/assets/icon-12px-solid/failure.svg";
import FullscreenEnter from "@graphite-frontend/assets/icon-12px-solid/fullscreen-enter.svg";
import FullscreenExit from "@graphite-frontend/assets/icon-12px-solid/fullscreen-exit.svg";
import Grid from "@graphite-frontend/assets/icon-12px-solid/grid.svg";
@ -30,6 +32,7 @@ import KeyboardOption from "@graphite-frontend/assets/icon-12px-solid/keyboard-o
import KeyboardShift from "@graphite-frontend/assets/icon-12px-solid/keyboard-shift.svg";
import KeyboardSpace from "@graphite-frontend/assets/icon-12px-solid/keyboard-space.svg";
import KeyboardTab from "@graphite-frontend/assets/icon-12px-solid/keyboard-tab.svg";
import License12px from "@graphite-frontend/assets/icon-12px-solid/license-12px.svg";
import Link from "@graphite-frontend/assets/icon-12px-solid/link.svg";
import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg";
import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg";
@ -47,9 +50,11 @@ const SOLID_12PX = {
Add: { svg: Add, size: 12 },
Checkmark: { svg: Checkmark, size: 12 },
CloseX: { svg: CloseX, size: 12 },
Delay: { svg: Delay, size: 12 },
DropdownArrow: { svg: DropdownArrow, size: 12 },
Edit12px: { svg: Edit12px, size: 12 },
Empty12px: { svg: Empty12px, size: 12 },
Failure: { svg: Failure, size: 12 },
FullscreenEnter: { svg: FullscreenEnter, size: 12 },
FullscreenExit: { svg: FullscreenExit, size: 12 },
Grid: { svg: Grid, size: 12 },
@ -66,6 +71,7 @@ const SOLID_12PX = {
KeyboardShift: { svg: KeyboardShift, size: 12 },
KeyboardSpace: { svg: KeyboardSpace, size: 12 },
KeyboardTab: { svg: KeyboardTab, size: 12 },
License12px: { svg: License12px, size: 12 },
Link: { svg: Link, size: 12 },
Overlays: { svg: Overlays, size: 12 },
Remove: { svg: Remove, size: 12 },
@ -95,6 +101,7 @@ import BooleanUnion from "@graphite-frontend/assets/icon-16px-solid/boolean-unio
import CheckboxChecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-checked.svg";
import CheckboxUnchecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-unchecked.svg";
import Copy from "@graphite-frontend/assets/icon-16px-solid/copy.svg";
import Credits from "@graphite-frontend/assets/icon-16px-solid/credits.svg";
import CustomColor from "@graphite-frontend/assets/icon-16px-solid/custom-color.svg";
import Edit from "@graphite-frontend/assets/icon-16px-solid/edit.svg";
import Eyedropper from "@graphite-frontend/assets/icon-16px-solid/eyedropper.svg";
@ -107,13 +114,15 @@ import Folder from "@graphite-frontend/assets/icon-16px-solid/folder.svg";
import GraphiteLogo from "@graphite-frontend/assets/icon-16px-solid/graphite-logo.svg";
import GraphViewClosed from "@graphite-frontend/assets/icon-16px-solid/graph-view-closed.svg";
import GraphViewOpen from "@graphite-frontend/assets/icon-16px-solid/graph-view-open.svg";
import IconsGrid from "@graphite-frontend/assets/icon-16px-solid/icons-grid.svg";
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
import Layer from "@graphite-frontend/assets/icon-16px-solid/layer.svg";
import License from "@graphite-frontend/assets/icon-16px-solid/license.svg";
import NodeArtboard from "@graphite-frontend/assets/icon-16px-solid/node-artboard.svg";
import NodeBlur from "@graphite-frontend/assets/icon-16px-solid/node-blur.svg";
import NodeBrushwork from "@graphite-frontend/assets/icon-16px-solid/node-brushwork.svg";
import NodeColorCorrection from "@graphite-frontend/assets/icon-16px-solid/node-color-correction.svg";
import NodeGradient from "@graphite-frontend/assets/icon-16px-solid/node-gradient.svg";
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
import NodeImaginate from "@graphite-frontend/assets/icon-16px-solid/node-imaginate.svg";
import NodeMagicWand from "@graphite-frontend/assets/icon-16px-solid/node-magic-wand.svg";
import NodeMask from "@graphite-frontend/assets/icon-16px-solid/node-mask.svg";
@ -137,6 +146,8 @@ import ViewModePixels from "@graphite-frontend/assets/icon-16px-solid/view-mode-
import ViewportDesignMode from "@graphite-frontend/assets/icon-16px-solid/viewport-design-mode.svg";
import ViewportGuideMode from "@graphite-frontend/assets/icon-16px-solid/viewport-guide-mode.svg";
import ViewportSelectMode from "@graphite-frontend/assets/icon-16px-solid/viewport-select-mode.svg";
import Volunteer from "@graphite-frontend/assets/icon-16px-solid/volunteer.svg";
import Website from "@graphite-frontend/assets/icon-16px-solid/website.svg";
import WorkingColorsPrimary from "@graphite-frontend/assets/icon-16px-solid/working-colors-primary.svg";
import WorkingColorsSecondary from "@graphite-frontend/assets/icon-16px-solid/working-colors-secondary.svg";
import ZoomIn from "@graphite-frontend/assets/icon-16px-solid/zoom-in.svg";
@ -158,6 +169,7 @@ const SOLID_16PX = {
CheckboxChecked: { svg: CheckboxChecked, size: 16 },
CheckboxUnchecked: { svg: CheckboxUnchecked, size: 16 },
Copy: { svg: Copy, size: 16 },
Credits: { svg: Credits, size: 16 },
CustomColor: { svg: CustomColor, size: 16 },
Edit: { svg: Edit, size: 16 },
Eyedropper: { svg: Eyedropper, size: 16 },
@ -170,13 +182,15 @@ const SOLID_16PX = {
GraphiteLogo: { svg: GraphiteLogo, size: 16 },
GraphViewClosed: { svg: GraphViewClosed, size: 16 },
GraphViewOpen: { svg: GraphViewOpen, size: 16 },
IconsGrid: { svg: IconsGrid, size: 16 },
Image: { svg: Image, size: 16 },
Layer: { svg: Layer, size: 16 },
License: { svg: License, size: 16 },
NodeArtboard: { svg: NodeArtboard, size: 16 },
NodeBlur: { svg: NodeBlur, size: 16 },
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
NodeGradient: { svg: NodeGradient, size: 16 },
Image: { svg: Image, size: 16 },
NodeImaginate: { svg: NodeImaginate, size: 16 },
NodeMagicWand: { svg: NodeMagicWand, size: 16 },
NodeMask: { svg: NodeMask, size: 16 },
@ -200,6 +214,8 @@ const SOLID_16PX = {
ViewportDesignMode: { svg: ViewportDesignMode, size: 16 },
ViewportGuideMode: { svg: ViewportGuideMode, size: 16 },
ViewportSelectMode: { svg: ViewportSelectMode, size: 16 },
Volunteer: { svg: Volunteer, size: 16 },
Website: { svg: Website, size: 16 },
WorkingColorsPrimary: { svg: WorkingColorsPrimary, size: 16 },
WorkingColorsSecondary: { svg: WorkingColorsSecondary, size: 16 },
ZoomIn: { svg: ZoomIn, size: 16 },

View file

@ -40,6 +40,8 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
if (!width || !height) throw new Error("Width and height must be nonzero when given to rasterizeSVG()");
const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);
// Convert the canvas to an image of the correct MIME type

View file

@ -103,7 +103,7 @@ export function createEditor() {
instance.openDocumentFile(filename, content);
// Remove the hash fragment from the URL
window.location.hash = "";
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
} catch {}
})();

View file

@ -425,13 +425,10 @@ export class UpdateActiveDocument extends JsMessage {
export class DisplayDialogPanic extends JsMessage {
readonly panicInfo!: string;
readonly header!: string;
readonly description!: string;
}
export class DisplayDialog extends JsMessage {
readonly title!: string;
readonly icon!: IconName;
}
@ -1049,6 +1046,8 @@ export class TextButton extends WidgetProps {
emphasized!: boolean;
noBackground!: boolean;
minWidth!: number;
disabled!: boolean;
@ -1066,6 +1065,7 @@ export type TextButtonWidget = {
label: string;
icon?: IconName;
emphasized?: boolean;
noBackground?: boolean;
minWidth?: number;
disabled?: boolean;
tooltip?: string;
@ -1222,7 +1222,7 @@ export function defaultWidgetLayout(): WidgetLayout {
}
// Updates a widget layout based on a list of updates, giving the new layout by mutating the `layout` argument
export function patchWidgetLayout(/* mut */ layout: WidgetLayout, updates: WidgetDiffUpdate): void {
export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: WidgetDiffUpdate): void {
layout.layoutTarget = updates.layoutTarget;
updates.diff.forEach((update) => {
@ -1320,7 +1320,11 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup {
}
// WIDGET LAYOUTS
export class UpdateDialogDetails extends WidgetDiffUpdate { }
export class UpdateDialogButtons extends WidgetDiffUpdate { }
export class UpdateDialogColumn1 extends WidgetDiffUpdate { }
export class UpdateDialogColumn2 extends WidgetDiffUpdate { }
export class UpdateDocumentBarLayout extends WidgetDiffUpdate { }
@ -1407,7 +1411,9 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerViewportResize,
TriggerVisitLink,
UpdateActiveDocument,
UpdateDialogDetails,
UpdateDialogButtons,
UpdateDialogColumn1,
UpdateDialogColumn2,
UpdateDocumentArtboards,
UpdateDocumentArtwork,
UpdateDocumentBarLayout,

View file

@ -354,8 +354,11 @@ impl JsEditorHandle {
}
#[wasm_bindgen(js_name = requestAboutGraphiteDialogWithLocalizedCommitDate)]
pub fn request_about_graphite_dialog_with_localized_commit_date(&self, localized_commit_date: String) {
let message = DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate { localized_commit_date };
pub fn request_about_graphite_dialog_with_localized_commit_date(&self, localized_commit_date: String, localized_commit_year: String) {
let message = DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
localized_commit_date,
localized_commit_year,
};
self.dispatch(message);
}

View file

@ -8,24 +8,13 @@ use wasm_bindgen::prelude::*;
/// When a panic occurs, notify the user and log the error to the JS console before the backend dies
pub fn panic_hook(info: &panic::PanicInfo) {
let header = "The editor crashed — sorry about that";
let description = "
An internal error occurred. Please report this by filing an issue on GitHub.\n\
\n\
Reload the editor to continue. If this happens immediately on repeated reloads, clear saved data.
"
.trim();
error!("{}", info);
JS_EDITOR_HANDLES.with(|instances| {
instances.borrow_mut().values_mut().for_each(|instance| {
instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic {
panic_info: info.to_string(),
header: header.to_string(),
description: description.to_string(),
})
})
instances
.borrow_mut()
.values_mut()
.for_each(|instance| instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() }))
});
}