Dialog redesign and content revamp (#1409)
* Revamp the content and design of dialogs * Add the Licenses dialog
12
README.md
|
@ -8,15 +8,19 @@
|
||||||
|
|
||||||
<h1 align="center">Redefining state-of-the-art graphics editing</h1>
|
<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! ⭐
|
⭐ Please remember to star this project here on GitHub! ⭐
|
||||||
|
|
||||||

|
[](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
|
## Discord community
|
||||||
|
|
||||||
|
|
|
@ -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_HASH: &str = env!("GRAPHITE_GIT_COMMIT_HASH");
|
||||||
pub const GRAPHITE_GIT_COMMIT_BRANCH: &str = env!("GRAPHITE_GIT_COMMIT_BRANCH");
|
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 {
|
pub fn commit_info_localized(localized_commit_date: &str) -> String {
|
||||||
format!("{}\n{}\n{}", commit_timestamp_localized(localized_commit_date), commit_hash(), commit_branch())
|
format!(
|
||||||
}
|
"Release Series: {}\n\
|
||||||
|
Branch: {}\n\
|
||||||
pub fn commit_timestamp() -> String {
|
Hash: {}\n\
|
||||||
format!("Date: {}", GRAPHITE_GIT_COMMIT_DATE)
|
{}",
|
||||||
}
|
GRAPHITE_RELEASE_SERIES,
|
||||||
|
GRAPHITE_GIT_COMMIT_BRANCH,
|
||||||
pub fn commit_timestamp_localized(localized_commit_date: &str) -> String {
|
&GRAPHITE_GIT_COMMIT_HASH[..8],
|
||||||
format!("Date: {}", localized_commit_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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -553,15 +553,18 @@ mod test {
|
||||||
let print_problem_to_terminal_on_failure = |value: &String| {
|
let print_problem_to_terminal_on_failure = |value: &String| {
|
||||||
println!();
|
println!();
|
||||||
println!("-------------------------------------------------");
|
println!("-------------------------------------------------");
|
||||||
println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file.");
|
println!("Failed test due to receiving a DisplayDialogError while loading a Graphite demo file.");
|
||||||
println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`");
|
println!();
|
||||||
println!("After bumping this version number, update the documents in `/demo-artwork` by editing their JSON to");
|
println!("That probably means the document serialization format changed. In that case, you need to bump the constant value");
|
||||||
println!("ensure they remain compatible with both the bumped version number and the serialization format change.");
|
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!("DisplayDialogError details:");
|
||||||
println!();
|
println!();
|
||||||
println!("Description: {}", value);
|
println!("Description: {}", value);
|
||||||
println!("-------------------------------------------------");
|
println!("-------------------------------------------------");
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
panic!()
|
panic!()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -581,7 +584,7 @@ mod test {
|
||||||
|
|
||||||
for response in responses {
|
for response in responses {
|
||||||
// Check for the existence of the file format incompatibility warning dialog after opening the test file
|
// 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 DiffUpdate::SubLayout(sub_layout) = &diff[0].new_value {
|
||||||
if let LayoutGroup::Row { widgets } = &sub_layout[0] {
|
if let LayoutGroup::Row { widgets } = &sub_layout[0] {
|
||||||
if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget {
|
if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget {
|
||||||
|
|
|
@ -29,12 +29,16 @@ pub enum DialogMessage {
|
||||||
RequestAboutGraphiteDialog,
|
RequestAboutGraphiteDialog,
|
||||||
RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
||||||
localized_commit_date: String,
|
localized_commit_date: String,
|
||||||
|
localized_commit_year: String,
|
||||||
},
|
},
|
||||||
RequestComingSoonDialog {
|
RequestComingSoonDialog {
|
||||||
issue: Option<i32>,
|
issue: Option<i32>,
|
||||||
},
|
},
|
||||||
RequestDemoArtworkDialog,
|
RequestDemoArtworkDialog,
|
||||||
RequestExportDialog,
|
RequestExportDialog,
|
||||||
|
RequestLicensesDialogWithLocalizedCommitDate {
|
||||||
|
localized_commit_year: String,
|
||||||
|
},
|
||||||
RequestNewDocumentDialog,
|
RequestNewDocumentDialog,
|
||||||
RequestPreferencesDialog,
|
RequestPreferencesDialog,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
|
@ -16,48 +16,54 @@ impl MessageHandler<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessag
|
||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
match message {
|
match message {
|
||||||
#[remain::unsorted]
|
#[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]
|
#[remain::unsorted]
|
||||||
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, ()),
|
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, ()),
|
||||||
#[remain::unsorted]
|
#[remain::unsorted]
|
||||||
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, preferences),
|
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, preferences),
|
||||||
|
|
||||||
DialogMessage::CloseAllDocumentsWithConfirmation => {
|
DialogMessage::CloseAllDocumentsWithConfirmation => {
|
||||||
let dialog = simple_dialogs::CloseAllDocumentsDialog;
|
let dialog = simple_dialogs::CloseAllDocumentsDialog {
|
||||||
dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
unsaved_document_names: portfolio.unsaved_document_names(),
|
||||||
responses.add(FrontendMessage::DisplayDialog { icon: "Copy".to_string() });
|
};
|
||||||
|
dialog.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
DialogMessage::CloseDialogAndThen { followups } => {
|
DialogMessage::CloseDialogAndThen { followups } => {
|
||||||
responses.add(FrontendMessage::DisplayDialogDismiss);
|
|
||||||
for message in followups.into_iter() {
|
for message in followups.into_iter() {
|
||||||
responses.add(message);
|
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 } => {
|
DialogMessage::DisplayDialogError { title, description } => {
|
||||||
let dialog = simple_dialogs::ErrorDialog { title, description };
|
let dialog = simple_dialogs::ErrorDialog { title, description };
|
||||||
dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
responses.add(FrontendMessage::DisplayDialog { icon: "Warning".to_string() });
|
|
||||||
}
|
}
|
||||||
DialogMessage::RequestAboutGraphiteDialog => {
|
DialogMessage::RequestAboutGraphiteDialog => {
|
||||||
responses.add(FrontendMessage::TriggerAboutGraphiteLocalizedCommitDate {
|
responses.add(FrontendMessage::TriggerAboutGraphiteLocalizedCommitDate {
|
||||||
commit_date: env!("GRAPHITE_GIT_COMMIT_DATE").into(),
|
commit_date: env!("GRAPHITE_GIT_COMMIT_DATE").into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate { localized_commit_date } => {
|
DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
||||||
let about_graphite = AboutGraphiteDialog { localized_commit_date };
|
localized_commit_date,
|
||||||
|
localized_commit_year,
|
||||||
|
} => {
|
||||||
|
let dialog = AboutGraphiteDialog {
|
||||||
|
localized_commit_date,
|
||||||
|
localized_commit_year,
|
||||||
|
};
|
||||||
|
|
||||||
about_graphite.send_layout(responses, LayoutTarget::DialogDetails);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
responses.add(FrontendMessage::DisplayDialog { icon: "GraphiteLogo".to_string() });
|
|
||||||
}
|
}
|
||||||
DialogMessage::RequestComingSoonDialog { issue } => {
|
DialogMessage::RequestComingSoonDialog { issue } => {
|
||||||
let coming_soon = ComingSoonDialog { issue };
|
let dialog = ComingSoonDialog { issue };
|
||||||
coming_soon.send_layout(responses, LayoutTarget::DialogDetails);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
responses.add(FrontendMessage::DisplayDialog { icon: "Warning".to_string() });
|
|
||||||
}
|
}
|
||||||
DialogMessage::RequestDemoArtworkDialog => {
|
DialogMessage::RequestDemoArtworkDialog => {
|
||||||
let demo_artwork_dialog = DemoArtworkDialog;
|
let dialog = DemoArtworkDialog;
|
||||||
demo_artwork_dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
responses.add(FrontendMessage::DisplayDialog { icon: "Image".to_string() });
|
|
||||||
}
|
}
|
||||||
DialogMessage::RequestExportDialog => {
|
DialogMessage::RequestExportDialog => {
|
||||||
if let Some(document) = portfolio.active_document() {
|
if let Some(document) = portfolio.active_document() {
|
||||||
|
@ -83,29 +89,30 @@ impl MessageHandler<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessag
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.export_dialog = ExportDialogMessageHandler {
|
self.export_dialog = ExportDialogMessageHandler {
|
||||||
file_name: document.name.clone(),
|
|
||||||
scale_factor: 1.,
|
scale_factor: 1.,
|
||||||
artboards,
|
artboards,
|
||||||
has_selection: document.selected_layers().next().is_some(),
|
has_selection: document.selected_layers().next().is_some(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
self.export_dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
self.export_dialog.send_dialog_to_frontend(responses);
|
||||||
responses.add(FrontendMessage::DisplayDialog { icon: "File".to_string() });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DialogMessage::RequestLicensesDialogWithLocalizedCommitDate { localized_commit_year } => {
|
||||||
|
let dialog = LicensesDialog { localized_commit_year };
|
||||||
|
|
||||||
|
dialog.send_dialog_to_frontend(responses);
|
||||||
|
}
|
||||||
DialogMessage::RequestNewDocumentDialog => {
|
DialogMessage::RequestNewDocumentDialog => {
|
||||||
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
||||||
name: portfolio.generate_new_document_name(),
|
name: portfolio.generate_new_document_name(),
|
||||||
infinite: false,
|
infinite: false,
|
||||||
dimensions: glam::UVec2::new(1920, 1080),
|
dimensions: glam::UVec2::new(1920, 1080),
|
||||||
};
|
};
|
||||||
self.new_document_dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
self.new_document_dialog.send_dialog_to_frontend(responses);
|
||||||
responses.add(FrontendMessage::DisplayDialog { icon: "File".to_string() });
|
|
||||||
}
|
}
|
||||||
DialogMessage::RequestPreferencesDialog => {
|
DialogMessage::RequestPreferencesDialog => {
|
||||||
self.preferences_dialog = PreferencesDialogMessageHandler {};
|
self.preferences_dialog = PreferencesDialogMessageHandler {};
|
||||||
self.preferences_dialog.send_layout(responses, LayoutTarget::DialogDetails, preferences);
|
self.preferences_dialog.send_dialog_to_frontend(responses, preferences);
|
||||||
responses.add(FrontendMessage::DisplayDialog { icon: "Settings".to_string() });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize};
|
||||||
#[impl_message(Message, DialogMessage, ExportDialog)]
|
#[impl_message(Message, DialogMessage, ExportDialog)]
|
||||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum ExportDialogMessage {
|
pub enum ExportDialogMessage {
|
||||||
FileName(String),
|
|
||||||
FileType(FileType),
|
FileType(FileType),
|
||||||
ScaleFactor(f64),
|
ScaleFactor(f64),
|
||||||
TransparentBackground(bool),
|
TransparentBackground(bool),
|
||||||
|
|
|
@ -7,7 +7,6 @@ use document_legacy::LayerId;
|
||||||
/// A dialog to allow users to customize their file export.
|
/// A dialog to allow users to customize their file export.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ExportDialogMessageHandler {
|
pub struct ExportDialogMessageHandler {
|
||||||
pub file_name: String,
|
|
||||||
pub file_type: FileType,
|
pub file_type: FileType,
|
||||||
pub scale_factor: f64,
|
pub scale_factor: f64,
|
||||||
pub bounds: ExportBounds,
|
pub bounds: ExportBounds,
|
||||||
|
@ -16,17 +15,16 @@ pub struct ExportDialogMessageHandler {
|
||||||
pub has_selection: bool,
|
pub has_selection: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageHandler<ExportDialogMessage, ()> for ExportDialogMessageHandler {
|
impl MessageHandler<ExportDialogMessage, &PortfolioMessageHandler> for ExportDialogMessageHandler {
|
||||||
fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque<Message>, _data: ()) {
|
fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque<Message>, portfolio: &PortfolioMessageHandler) {
|
||||||
match message {
|
match message {
|
||||||
ExportDialogMessage::FileName(name) => self.file_name = name,
|
|
||||||
ExportDialogMessage::FileType(export_type) => self.file_type = export_type,
|
ExportDialogMessage::FileType(export_type) => self.file_type = export_type,
|
||||||
ExportDialogMessage::ScaleFactor(factor) => self.scale_factor = factor,
|
ExportDialogMessage::ScaleFactor(factor) => self.scale_factor = factor,
|
||||||
ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background,
|
ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background,
|
||||||
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area,
|
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area,
|
||||||
|
|
||||||
ExportDialogMessage::Submit => responses.add_front(DocumentMessage::ExportDocument {
|
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,
|
file_type: self.file_type,
|
||||||
scale_factor: self.scale_factor,
|
scale_factor: self.scale_factor,
|
||||||
bounds: self.bounds,
|
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;}
|
advertise_actions! {ExportDialogUpdate;}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutHolder for ExportDialogMessageHandler {
|
impl DialogLayoutHolder for ExportDialogMessageHandler {
|
||||||
fn layout(&self) -> Layout {
|
const ICON: &'static str = "File";
|
||||||
let file_name = vec![
|
const TITLE: &'static str = "Export";
|
||||||
TextLabel::new("File Name").table_align(true).widget_holder(),
|
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
fn layout_buttons(&self) -> Layout {
|
||||||
TextInput::new(&self.file_name)
|
let widgets = vec![
|
||||||
.on_update(|text_input: &TextInput| ExportDialogMessage::FileName(text_input.value.clone()).into())
|
TextButton::new("Export")
|
||||||
|
.emphasized(true)
|
||||||
|
.on_update(|_| {
|
||||||
|
DialogMessage::CloseDialogAndThen {
|
||||||
|
followups: vec![ExportDialogMessage::Submit.into()],
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
.widget_holder(),
|
.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")]
|
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(val, name)| RadioEntryData::new(name).on_update(move |_| ExportDialogMessage::FileType(val).into()))
|
.map(|(val, name)| RadioEntryData::new(name).on_update(move |_| ExportDialogMessage::FileType(val).into()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let export_type = vec![
|
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(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
RadioInput::new(entries).selected_index(self.file_type as u32).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 artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string(), false));
|
||||||
let mut export_area_options = vec![
|
let mut export_area_options = vec![
|
||||||
(ExportBounds::AllArtwork, "All Artwork".to_string(), false),
|
(ExportBounds::AllArtwork, "All Artwork".to_string(), false),
|
||||||
|
@ -74,13 +99,13 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||||
.collect()];
|
.collect()];
|
||||||
|
|
||||||
let export_area = vec![
|
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(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
|
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let transparent_background = vec![
|
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(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
CheckboxInput::new(self.transparent_background)
|
CheckboxInput::new(self.transparent_background)
|
||||||
.disabled(self.file_type == FileType::Jpg)
|
.disabled(self.file_type == FileType::Jpg)
|
||||||
|
@ -88,42 +113,11 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||||
.widget_holder(),
|
.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![
|
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: export_type },
|
||||||
LayoutGroup::Row { widgets: resolution },
|
LayoutGroup::Row { widgets: resolution },
|
||||||
LayoutGroup::Row { widgets: export_area },
|
LayoutGroup::Row { widgets: export_area },
|
||||||
LayoutGroup::Row { widgets: transparent_background },
|
LayoutGroup::Row { widgets: transparent_background },
|
||||||
LayoutGroup::Row { widgets: button_widgets },
|
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
//! Handles modal dialogs that appear as floating menus in the center of the editor window.
|
//! 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;
|
//! Then dialog can be opened by sending the `FrontendMessage::DisplayDialog` message;
|
||||||
|
|
||||||
mod dialog_message;
|
mod dialog_message;
|
||||||
|
|
|
@ -42,26 +42,47 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_layout(responses, LayoutTarget::DialogDetails);
|
self.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_actions! {NewDocumentDialogUpdate;}
|
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 {
|
impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let title = vec![TextLabel::new("New Document").bold(true).widget_holder()];
|
|
||||||
|
|
||||||
let name = vec![
|
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(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
TextInput::new(&self.name)
|
TextInput::new(&self.name)
|
||||||
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name(text_input.value.clone()).into())
|
.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(),
|
.widget_holder(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let infinite = vec![
|
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(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
CheckboxInput::new(self.infinite)
|
CheckboxInput::new(self.infinite)
|
||||||
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into())
|
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into())
|
||||||
|
@ -69,7 +90,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||||
];
|
];
|
||||||
|
|
||||||
let scale = vec![
|
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(),
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
NumberInput::new(Some(self.dimensions.x as f64))
|
NumberInput::new(Some(self.dimensions.x as f64))
|
||||||
.label("W")
|
.label("W")
|
||||||
|
@ -94,26 +115,10 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||||
.widget_holder(),
|
.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![
|
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||||
LayoutGroup::Row { widgets: title },
|
|
||||||
LayoutGroup::Row { widgets: name },
|
LayoutGroup::Row { widgets: name },
|
||||||
LayoutGroup::Row { widgets: infinite },
|
LayoutGroup::Row { widgets: infinite },
|
||||||
LayoutGroup::Row { widgets: scale },
|
LayoutGroup::Row { widgets: scale },
|
||||||
LayoutGroup::Row { widgets: button_widgets },
|
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,19 +11,18 @@ impl MessageHandler<PreferencesDialogMessage, &PreferencesMessageHandler> for Pr
|
||||||
PreferencesDialogMessage::Confirm => {}
|
PreferencesDialogMessage::Confirm => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_layout(responses, LayoutTarget::DialogDetails, preferences);
|
self.send_dialog_to_frontend(responses, preferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_actions! {PreferencesDialogUpdate;}
|
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 {
|
impl PreferencesDialogMessageHandler {
|
||||||
pub fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) {
|
const ICON: &'static str = "Settings";
|
||||||
responses.add(LayoutMessage::SendLayout {
|
const TITLE: &'static str = "Editor Preferences";
|
||||||
layout: self.layout(preferences),
|
|
||||||
layout_target,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout {
|
fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout {
|
||||||
let zoom_with_scroll = vec![
|
let zoom_with_scroll = vec![
|
||||||
|
@ -64,9 +63,32 @@ impl PreferencesDialogMessageHandler {
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let button_widgets = vec![
|
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||||
TextButton::new("Ok")
|
LayoutGroup::Row { widgets: zoom_with_scroll },
|
||||||
.min_width(96)
|
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)
|
.emphasized(true)
|
||||||
.on_update(|_| {
|
.on_update(|_| {
|
||||||
DialogMessage::CloseDialogAndThen {
|
DialogMessage::CloseDialogAndThen {
|
||||||
|
@ -75,20 +97,25 @@ impl PreferencesDialogMessageHandler {
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
TextButton::new("Reset to Defaults")
|
TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_holder(),
|
||||||
.min_width(96)
|
|
||||||
.on_update(|_| PreferencesMessage::ResetToDefaults.into())
|
|
||||||
.widget_holder(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||||
LayoutGroup::Row {
|
}
|
||||||
widgets: vec![TextLabel::new("Editor Preferences").bold(true).widget_holder()],
|
fn send_layout_buttons(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget) {
|
||||||
},
|
responses.add(LayoutMessage::SendLayout {
|
||||||
LayoutGroup::Row { widgets: zoom_with_scroll },
|
layout: self.layout_buttons(),
|
||||||
LayoutGroup::Row { widgets: imaginate_server_hostname },
|
layout_target,
|
||||||
LayoutGroup::Row { widgets: imaginate_refresh_frequency },
|
});
|
||||||
LayoutGroup::Row { widgets: button_widgets },
|
}
|
||||||
]))
|
|
||||||
|
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(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
/// A dialog for displaying information on [BuildMetadata] viewable via *Help* > *About Graphite* in the menu bar.
|
/// A dialog for displaying information on [BuildMetadata] viewable via *Help* > *About Graphite* in the menu bar.
|
||||||
pub struct AboutGraphiteDialog {
|
pub struct AboutGraphiteDialog {
|
||||||
pub localized_commit_date: String,
|
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 {
|
impl LayoutHolder for AboutGraphiteDialog {
|
||||||
fn layout(&self) -> Layout {
|
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![
|
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||||
LayoutGroup::Row {
|
LayoutGroup::Row {
|
||||||
widgets: vec![TextLabel::new("Graphite".to_string()).bold(true).widget_holder()],
|
widgets: vec![TextLabel::new("About this release").bold(true).widget_holder()],
|
||||||
},
|
|
||||||
LayoutGroup::Row {
|
|
||||||
widgets: vec![TextLabel::new(release_series()).widget_holder()],
|
|
||||||
},
|
},
|
||||||
LayoutGroup::Row {
|
LayoutGroup::Row {
|
||||||
widgets: vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_holder()],
|
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()],
|
||||||
|
},
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,30 +2,43 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
/// A dialog for confirming the closing of all documents viewable via `file -> close all` in the menu bar.
|
/// 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 {
|
impl LayoutHolder for CloseAllDocumentsDialog {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let discard = TextButton::new("Discard All")
|
let unsaved_list = "• ".to_string() + &self.unsaved_document_names.join("\n• ");
|
||||||
.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();
|
|
||||||
|
|
||||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||||
LayoutGroup::Row {
|
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 {
|
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] },
|
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,14 @@ pub struct CloseDocumentDialog {
|
||||||
pub document_id: u64,
|
pub document_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutHolder for CloseDocumentDialog {
|
impl DialogLayoutHolder for CloseDocumentDialog {
|
||||||
fn layout(&self) -> Layout {
|
const ICON: &'static str = "Warning";
|
||||||
let document_id = self.document_id;
|
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")
|
TextButton::new("Save")
|
||||||
.min_width(96)
|
|
||||||
.emphasized(true)
|
.emphasized(true)
|
||||||
.on_update(|_| {
|
.on_update(|_| {
|
||||||
DialogMessage::CloseDialogAndThen {
|
DialogMessage::CloseDialogAndThen {
|
||||||
|
@ -24,7 +25,6 @@ impl LayoutHolder for CloseDocumentDialog {
|
||||||
})
|
})
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
TextButton::new("Discard")
|
TextButton::new("Discard")
|
||||||
.min_width(96)
|
|
||||||
.on_update(move |_| {
|
.on_update(move |_| {
|
||||||
DialogMessage::CloseDialogAndThen {
|
DialogMessage::CloseDialogAndThen {
|
||||||
followups: vec![BroadcastEvent::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
followups: vec![BroadcastEvent::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
||||||
|
@ -32,17 +32,22 @@ impl LayoutHolder for CloseDocumentDialog {
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.widget_holder(),
|
.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![
|
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||||
LayoutGroup::Row {
|
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 {
|
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 },
|
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,46 @@
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
/// A dialog to notify users of an unfinished issue, optionally with an issue number.
|
/// A dialog to notify users of an unfinished issue, optionally with an issue number.
|
||||||
pub struct ComingSoonDialog {
|
pub struct ComingSoonDialog {
|
||||||
pub issue: Option<i32>,
|
pub issue: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutHolder for ComingSoonDialog {
|
impl DialogLayoutHolder for ComingSoonDialog {
|
||||||
fn layout(&self) -> Layout {
|
const ICON: &'static str = "Delay";
|
||||||
let mut details = "This feature is not implemented yet".to_string();
|
const TITLE: &'static str = "Coming Soon";
|
||||||
|
|
||||||
let mut buttons = vec![TextButton::new("OK")
|
fn layout_buttons(&self) -> Layout {
|
||||||
.emphasized(true)
|
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
|
||||||
.min_width(96)
|
|
||||||
.on_update(|_| FrontendMessage::DisplayDialogDismiss.into())
|
|
||||||
.widget_holder()];
|
|
||||||
|
|
||||||
if let Some(issue) = self.issue {
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||||
let _ = write!(details, "— but you can help add it!\nSee issue #{issue} on GitHub.");
|
}
|
||||||
buttons.push(
|
}
|
||||||
TextButton::new(format!("Issue #{issue}"))
|
|
||||||
.min_width(96)
|
impl LayoutHolder for ComingSoonDialog {
|
||||||
.on_update(move |_| {
|
fn layout(&self) -> Layout {
|
||||||
FrontendMessage::TriggerVisitLink {
|
let header = vec![TextLabel::new("You've stumbled upon a placeholder").bold(true).widget_holder()];
|
||||||
url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"),
|
let row1 = vec![TextLabel::new("This feature is not implemented yet.").widget_holder()];
|
||||||
}
|
|
||||||
.into()
|
let mut rows = vec![LayoutGroup::Row { widgets: header }, LayoutGroup::Row { widgets: row1 }];
|
||||||
})
|
|
||||||
.widget_holder(),
|
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}"))
|
||||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
.icon(Some("Website".into()))
|
||||||
LayoutGroup::Row {
|
.no_background(true)
|
||||||
widgets: vec![TextLabel::new("Coming soon").bold(true).widget_holder()],
|
.on_update(move |_| {
|
||||||
},
|
FrontendMessage::TriggerVisitLink {
|
||||||
LayoutGroup::Row {
|
url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"),
|
||||||
widgets: vec![TextLabel::new(details).multiline(true).widget_holder()],
|
}
|
||||||
},
|
.into()
|
||||||
LayoutGroup::Row { widgets: buttons },
|
})
|
||||||
]))
|
.widget_holder()];
|
||||||
|
|
||||||
|
rows.push(LayoutGroup::Row { widgets: row2 });
|
||||||
|
rows.push(LayoutGroup::Row { widgets: row3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout::WidgetLayout(WidgetLayout::new(rows))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,27 +4,38 @@ use crate::messages::prelude::*;
|
||||||
/// A dialog to let the user browse a gallery of demo artwork that can be opened.
|
/// A dialog to let the user browse a gallery of demo artwork that can be opened.
|
||||||
pub struct DemoArtworkDialog;
|
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 {
|
impl LayoutHolder for DemoArtworkDialog {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let artwork = [
|
let images = 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
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, thumbnail, _)| ImageLabel::new(thumbnail.to_string()).width(Some("256px".into())).widget_holder())
|
.map(|(_, thumbnail, _)| ImageLabel::new(thumbnail.to_string()).width(Some("256px".into())).widget_holder())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let button_widgets = artwork
|
let buttons = ARTWORK
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(label, _, url)| {
|
.map(|(label, _, url)| {
|
||||||
TextButton::new(label)
|
TextButton::new(label)
|
||||||
|
@ -39,12 +50,6 @@ impl LayoutHolder for DemoArtworkDialog {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: images }, LayoutGroup::Row { widgets: buttons }]))
|
||||||
LayoutGroup::Row {
|
|
||||||
widgets: vec![TextLabel::new("Demo Artwork".to_string()).bold(true).widget_holder()],
|
|
||||||
},
|
|
||||||
LayoutGroup::Row { widgets: image_widgets },
|
|
||||||
LayoutGroup::Row { widgets: button_widgets },
|
|
||||||
]))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,17 @@ pub struct ErrorDialog {
|
||||||
pub description: String,
|
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 {
|
impl LayoutHolder for ErrorDialog {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||||
|
@ -16,13 +27,6 @@ impl LayoutHolder for ErrorDialog {
|
||||||
LayoutGroup::Row {
|
LayoutGroup::Row {
|
||||||
widgets: vec![TextLabel::new(&self.description).multiline(true).widget_holder()],
|
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()],
|
|
||||||
},
|
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
63
editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs
Normal 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()],
|
||||||
|
},
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ mod close_document_dialog;
|
||||||
mod coming_soon_dialog;
|
mod coming_soon_dialog;
|
||||||
mod demo_artwork_dialog;
|
mod demo_artwork_dialog;
|
||||||
mod error_dialog;
|
mod error_dialog;
|
||||||
|
mod licenses_dialog;
|
||||||
|
|
||||||
pub use about_graphite_dialog::AboutGraphiteDialog;
|
pub use about_graphite_dialog::AboutGraphiteDialog;
|
||||||
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
|
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 coming_soon_dialog::ComingSoonDialog;
|
||||||
pub use demo_artwork_dialog::DemoArtworkDialog;
|
pub use demo_artwork_dialog::DemoArtworkDialog;
|
||||||
pub use error_dialog::ErrorDialog;
|
pub use error_dialog::ErrorDialog;
|
||||||
|
pub use licenses_dialog::LicensesDialog;
|
||||||
|
|
|
@ -18,14 +18,13 @@ use serde::{Deserialize, Serialize};
|
||||||
pub enum FrontendMessage {
|
pub enum FrontendMessage {
|
||||||
// Display prefix: make the frontend show something, like a dialog
|
// Display prefix: make the frontend show something, like a dialog
|
||||||
DisplayDialog {
|
DisplayDialog {
|
||||||
|
title: String,
|
||||||
icon: String,
|
icon: String,
|
||||||
},
|
},
|
||||||
DisplayDialogDismiss,
|
DisplayDialogDismiss,
|
||||||
DisplayDialogPanic {
|
DisplayDialogPanic {
|
||||||
#[serde(rename = "panicInfo")]
|
#[serde(rename = "panicInfo")]
|
||||||
panic_info: String,
|
panic_info: String,
|
||||||
header: String,
|
|
||||||
description: String,
|
|
||||||
},
|
},
|
||||||
DisplayEditableTextbox {
|
DisplayEditableTextbox {
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -122,7 +121,17 @@ pub enum FrontendMessage {
|
||||||
#[serde(rename = "documentId")]
|
#[serde(rename = "documentId")]
|
||||||
document_id: u64,
|
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")]
|
#[serde(rename = "layoutTarget")]
|
||||||
layout_target: LayoutTarget,
|
layout_target: LayoutTarget,
|
||||||
diff: Vec<WidgetDiff>,
|
diff: Vec<WidgetDiff>,
|
||||||
|
|
|
@ -289,7 +289,9 @@ impl LayoutMessageHandler {
|
||||||
|
|
||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
let message = match layout_target {
|
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::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
|
||||||
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
|
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
|
||||||
LayoutTarget::GraphViewOverlayButton => FrontendMessage::UpdateGraphViewOverlayButtonLayout { layout_target, diff },
|
LayoutTarget::GraphViewOverlayButton => FrontendMessage::UpdateGraphViewOverlayButtonLayout { layout_target, diff },
|
||||||
|
|
|
@ -15,8 +15,12 @@ use std::sync::Arc;
|
||||||
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, Serialize, Deserialize, specta::Type)]
|
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, Serialize, Deserialize, specta::Type)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum LayoutTarget {
|
pub enum LayoutTarget {
|
||||||
/// Contains the contents of the dialog, including the title and action buttons. Must be shown with the `FrontendMessage::DisplayDialog` message.
|
/// Contains the action buttons at the bottom of the dialog. Must be shown with the `FrontendMessage::DisplayDialog` message.
|
||||||
DialogDetails,
|
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.
|
/// Contains the widgets located directly above the canvas to the right, for example the zoom in and out buttons.
|
||||||
DocumentBar,
|
DocumentBar,
|
||||||
/// Contains the dropdown for design / select / guide mode found on the top left of the canvas.
|
/// 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.
|
/// 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)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, specta::Type)]
|
||||||
pub enum Layout {
|
pub enum Layout {
|
||||||
|
|
|
@ -84,6 +84,9 @@ pub struct TextButton {
|
||||||
|
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
|
|
||||||
|
#[serde(rename = "noBackground")]
|
||||||
|
pub no_background: bool,
|
||||||
|
|
||||||
pub emphasized: bool,
|
pub emphasized: bool,
|
||||||
|
|
||||||
#[serde(rename = "minWidth")]
|
#[serde(rename = "minWidth")]
|
||||||
|
|
|
@ -140,8 +140,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
||||||
document_name: target_document.name.clone(),
|
document_name: target_document.name.clone(),
|
||||||
document_id,
|
document_id,
|
||||||
};
|
};
|
||||||
dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
responses.add(FrontendMessage::DisplayDialog { icon: "File".to_string() });
|
|
||||||
|
|
||||||
// Select the document being closed
|
// Select the document being closed
|
||||||
responses.add(PortfolioMessage::SelectDocument { document_id });
|
responses.add(PortfolioMessage::SelectDocument { document_id });
|
||||||
|
@ -598,6 +597,10 @@ impl PortfolioMessageHandler {
|
||||||
self.active_document_id
|
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 {
|
pub fn generate_new_document_name(&self) -> String {
|
||||||
let mut doc_title_numbers = self
|
let mut doc_title_numbers = self
|
||||||
.ordered_document_iterator()
|
.ordered_document_iterator()
|
||||||
|
|
|
@ -80,39 +80,40 @@ impl ToolMetadata for PathTool {
|
||||||
|
|
||||||
impl LayoutHolder for PathTool {
|
impl LayoutHolder for PathTool {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
if let Some(SingleSelectedPoint { coordinates: DVec2 { x, y }, .. }) = self.tool_data.single_selected_point {
|
let coordinates = self.tool_data.single_selected_point.as_ref().map(|point| point.coordinates);
|
||||||
let x_location = NumberInput::new(Some(x))
|
let (x, y) = coordinates.map(|point| (Some(point.x), Some(point.y))).unwrap_or((None, None));
|
||||||
.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 y_location = NumberInput::new(Some(y))
|
let x_location = NumberInput::new(x)
|
||||||
.unit(" px")
|
.unit(" px")
|
||||||
.label("Y")
|
.label("X")
|
||||||
.min_width(120)
|
.min_width(120)
|
||||||
.min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))
|
.disabled(x.is_none())
|
||||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
.min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))
|
||||||
.on_update(move |number_input: &NumberInput| {
|
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||||
let new_y = number_input.value.unwrap_or(y);
|
.on_update(move |number_input: &NumberInput| {
|
||||||
PathToolMessage::SelectedPointYChanged { new_y }.into()
|
let new_x = number_input.value.unwrap_or(x.unwrap());
|
||||||
})
|
PathToolMessage::SelectedPointXChanged { new_x }.into()
|
||||||
.widget_holder();
|
})
|
||||||
|
.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 {
|
let seperator = Separator::new(SeparatorType::Related).widget_holder();
|
||||||
widgets: vec![x_location, seperator, y_location],
|
|
||||||
}]))
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||||
} else {
|
widgets: vec![x_location, seperator, y_location],
|
||||||
Layout::WidgetLayout(WidgetLayout::default())
|
}]))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,8 @@ impl LayoutHolder for SelectTool {
|
||||||
|
|
||||||
let selected_layers_count = self.tool_data.selected_layers_count;
|
let selected_layers_count = self.tool_data.selected_layers_count;
|
||||||
let deactivate_alignment = selected_layers_count < 2;
|
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;
|
let deactivate_pivot = selected_layers_count < 1;
|
||||||
|
|
||||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||||
|
@ -207,41 +209,48 @@ impl LayoutHolder for SelectTool {
|
||||||
})
|
})
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
Separator::new(SeparatorType::Related).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(),
|
Separator::new(SeparatorType::Section).widget_holder(),
|
||||||
IconButton::new("FlipHorizontal", 24)
|
IconButton::new("FlipHorizontal", 24)
|
||||||
.tooltip("Flip Horizontal")
|
.tooltip("Flip Horizontal")
|
||||||
|
.disabled(deactivate_flip)
|
||||||
.on_update(|_| SelectToolMessage::FlipHorizontal.into())
|
.on_update(|_| SelectToolMessage::FlipHorizontal.into())
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
IconButton::new("FlipVertical", 24)
|
IconButton::new("FlipVertical", 24)
|
||||||
.tooltip("Flip Vertical")
|
.tooltip("Flip Vertical")
|
||||||
|
.disabled(deactivate_flip)
|
||||||
.on_update(|_| SelectToolMessage::FlipVertical.into())
|
.on_update(|_| SelectToolMessage::FlipVertical.into())
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
Separator::new(SeparatorType::Related).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(),
|
Separator::new(SeparatorType::Section).widget_holder(),
|
||||||
IconButton::new("BooleanUnion", 24)
|
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())
|
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
IconButton::new("BooleanSubtractFront", 24)
|
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())
|
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
IconButton::new("BooleanSubtractBack", 24)
|
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())
|
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
IconButton::new("BooleanIntersect", 24)
|
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())
|
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
IconButton::new("BooleanDifference", 24)
|
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())
|
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
Separator::new(SeparatorType::Related).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(),
|
||||||
],
|
],
|
||||||
}]))
|
}]))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
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.
|
||||||
|
|
||||||
The author and rightsholder, Keavon Chambers, may be reached through the email address listed at https://graphite.rs/contact/ or https://keavon.com.
|
|
||||||
|
|
3
frontend/assets/icon-12px-solid/delay.svg
Normal 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 |
3
frontend/assets/icon-12px-solid/failure.svg
Normal 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 |
3
frontend/assets/icon-12px-solid/license-12px.svg
Normal 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 |
3
frontend/assets/icon-16px-solid/credits.svg
Normal 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 |
3
frontend/assets/icon-16px-solid/icons-grid.svg
Normal 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 |
3
frontend/assets/icon-16px-solid/license.svg
Normal 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 |
3
frontend/assets/icon-16px-solid/volunteer.svg
Normal 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 |
3
frontend/assets/icon-16px-solid/website.svg
Normal 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 |
|
@ -44,7 +44,7 @@
|
||||||
createPanicManager(editor, dialog);
|
createPanicManager(editor, dialog);
|
||||||
createPersistenceManager(editor, portfolio);
|
createPersistenceManager(editor, portfolio);
|
||||||
let dragManagerDestructor = createDragManager();
|
let dragManagerDestructor = createDragManager();
|
||||||
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, fullscreen);
|
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
|
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, onMount } from "svelte";
|
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 FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.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 WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||||
import type { DialogState } from "@graphite/state-providers/dialog";
|
import type { DialogState } from "@graphite/state-providers/dialog";
|
||||||
|
|
||||||
|
@ -20,24 +23,52 @@
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
<FloatingMenu open={true} class="dialog-modal" type="Dialog" direction="Center" bind:this={self} data-dialog-modal>
|
||||||
<LayoutRow>
|
<LayoutRow class="header-area">
|
||||||
<LayoutCol class="icon-column">
|
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
|
||||||
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
|
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
|
||||||
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
|
<TextLabel>{$dialog.title}</TextLabel>
|
||||||
</LayoutCol>
|
</LayoutRow>
|
||||||
<LayoutCol class="main-column">
|
<LayoutRow class="content">
|
||||||
{#if $dialog.widgets.layout.length > 0}
|
<LayoutCol class="column-1">
|
||||||
<WidgetLayout layout={$dialog.widgets} class="details" />
|
{#if $dialog.column1.layout.length > 0}
|
||||||
|
<WidgetLayout layout={$dialog.column1} class="details" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if ($dialog.crashDialogButtons?.length || NaN) > 0}
|
{#if $dialog.panicDetails}
|
||||||
<LayoutRow class="panic-buttons-row">
|
<div class="widget-layout details">
|
||||||
{#each $dialog.crashDialogButtons || [] as button, index (index)}
|
<div class="widget-row"><TextLabel bold={true}>The editor crashed — sorry about that</TextLabel></div>
|
||||||
<TextButton action={() => button.callback?.()} {...button.props} />
|
<div class="widget-row"><TextLabel>Please report this by filing an issue on GitHub:</TextLabel></div>
|
||||||
{/each}
|
<div class="widget-row"><TextButton label="Report Bug" icon="Warning" noBackground={true} action={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
|
||||||
</LayoutRow>
|
<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}
|
{/if}
|
||||||
</LayoutCol>
|
</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>
|
</LayoutRow>
|
||||||
</FloatingMenu>
|
</FloatingMenu>
|
||||||
|
|
||||||
|
@ -50,32 +81,45 @@
|
||||||
|
|
||||||
> .floating-menu-container > .floating-menu-content {
|
> .floating-menu-container > .floating-menu-content {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-column {
|
.header-area,
|
||||||
margin-right: 24px;
|
.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 {
|
.icon-label {
|
||||||
width: 80px;
|
width: 24px;
|
||||||
height: 80px;
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
&.file,
|
.text-label {
|
||||||
&.copy {
|
margin-left: 12px;
|
||||||
width: 60px;
|
line-height: 24px;
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin: 0 -10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-column {
|
.content {
|
||||||
margin: -4px 0;
|
margin: -4px 0;
|
||||||
|
|
||||||
|
.column-1 + .column-2 {
|
||||||
|
margin-left: 48px;
|
||||||
|
|
||||||
|
.text-button {
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.details.text-label {
|
.details.text-label {
|
||||||
-webkit-user-select: text; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
|
-webkit-user-select: text; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
@ -84,13 +128,22 @@
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.radio-input button {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used by the "Open Demo Artwork" dialog
|
||||||
.image-label {
|
.image-label {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.panic-buttons-row {
|
.footer-area {
|
||||||
height: 32px;
|
border-radius: 0 0 4px 4px;
|
||||||
align-items: center;
|
justify-content: right;
|
||||||
|
|
||||||
|
.text-button {
|
||||||
|
min-width: 96px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterUpdate, createEventDispatcher, tick } from "svelte";
|
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";
|
||||||
|
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
|
|
||||||
|
@ -103,9 +103,42 @@
|
||||||
wasOpen = isOpen;
|
wasOpen = isOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the client bounds of the elements and apply relevant styles to them
|
onMount(() => {
|
||||||
// TODO: Use DOM attribute bindings more whilst not causing recursive updates
|
// 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(() => {
|
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)
|
// 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();
|
if (!measuringOngoingGuard) positionAndStyleFloatingMenu();
|
||||||
});
|
});
|
||||||
|
|
|
@ -69,13 +69,6 @@
|
||||||
editor.instance.updateLayout(layoutTarget, widgets[index].widgetId, value);
|
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
|
// 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> {
|
function exclude<T extends object>(props: T, additional?: (keyof T)[]): Omit<T, typeof additional extends Array<infer K> ? K : never> {
|
||||||
const exclusions = ["kind", ...(additional || [])];
|
const exclusions = ["kind", ...(additional || [])];
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let label: string;
|
export let label: string;
|
||||||
export let icon: IconName | undefined = undefined;
|
export let icon: IconName | undefined = undefined;
|
||||||
export let emphasized = false;
|
export let emphasized = false;
|
||||||
|
export let noBackground = false;
|
||||||
export let minWidth = 0;
|
export let minWidth = 0;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let tooltip: string | undefined = undefined;
|
export let tooltip: string | undefined = undefined;
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
class="text-button"
|
class="text-button"
|
||||||
class:emphasized
|
class:emphasized
|
||||||
class:disabled
|
class:disabled
|
||||||
|
class:no-background={noBackground}
|
||||||
class:sharp-right-corners={sharpRightCorners}
|
class:sharp-right-corners={sharpRightCorners}
|
||||||
style:min-width={minWidth > 0 ? `${minWidth}px` : ""}
|
style:min-width={minWidth > 0 ? `${minWidth}px` : ""}
|
||||||
title={tooltip}
|
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;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.widget-column > & + .text-button,
|
||||||
|
.layout-column > & + .text-button {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-label {
|
.icon-label {
|
||||||
position: relative;
|
position: relative;
|
||||||
left: -4px;
|
left: -4px;
|
||||||
|
|
|
@ -120,7 +120,7 @@
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
|
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
|
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
|
||||||
|
@ -136,7 +136,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -289,10 +289,6 @@
|
||||||
td {
|
td {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-button:not(:hover) {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
import { type DialogState } from "@graphite/state-providers/dialog";
|
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 FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||||
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
||||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
||||||
|
@ -16,7 +17,7 @@ type EventListenerTarget = {
|
||||||
removeEventListener: typeof window.removeEventListener;
|
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;
|
const app = window.document.querySelector("[data-app-container]") as HTMLElement | undefined;
|
||||||
app?.focus();
|
app?.focus();
|
||||||
|
|
||||||
|
@ -58,8 +59,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
|
||||||
// Keyboard events
|
// Keyboard events
|
||||||
|
|
||||||
async function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): Promise<boolean> {
|
async function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): Promise<boolean> {
|
||||||
// Don't redirect when a modal is covering the workspace
|
// Don't redirect when a modal, or the overlaid graph, is covering the workspace
|
||||||
if (get(dialog).visible) return false;
|
if (get(dialog).visible || get(document).graphViewOverlayOpen) return false;
|
||||||
|
|
||||||
const key = await getLocalizedScanCode(e);
|
const key = await getLocalizedScanCode(e);
|
||||||
|
|
||||||
|
@ -239,7 +240,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onBeforeUnload(e: BeforeUnloadEvent): Promise<void> {
|
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);
|
if (activeDocument && !activeDocument.isAutoSaved) editor.instance.triggerAutoSave(activeDocument.id);
|
||||||
|
|
||||||
// Skip the message if the editor crashed, since work is already lost
|
// 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
|
// Skip the message during development, since it's annoying when testing
|
||||||
if (await editor.instance.inDevelopmentMode()) return;
|
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) {
|
if (!allDocumentsSaved) {
|
||||||
e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?";
|
e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?";
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { type Editor } from "@graphite/wasm-communication/editor";
|
||||||
import { TriggerAboutGraphiteLocalizedCommitDate } from "@graphite/wasm-communication/messages";
|
import { TriggerAboutGraphiteLocalizedCommitDate } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
export function createLocalizationManager(editor: Editor): void {
|
export function createLocalizationManager(editor: Editor): void {
|
||||||
function localizeTimestamp(utc: string): string {
|
function localizeTimestamp(utc: string): { timestamp: string, year: string } {
|
||||||
// Timestamp
|
// Timestamp
|
||||||
const date = new Date(utc);
|
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" })
|
const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
|
||||||
.formatToParts(new Date())
|
.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 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 timeString = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
||||||
const timezoneNameString = timezoneName?.value;
|
const timezoneNameString = timezoneName?.value;
|
||||||
return `${dateString} ${timeString} ${timezoneNameString}`;
|
return { timestamp: `${dateString} ${timeString} ${timezoneNameString}`, year: `${date.getFullYear()}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to process backend event
|
// Subscribe to process backend event
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (triggerAboutGraphiteLocalizedCommitDate) => {
|
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (triggerAboutGraphiteLocalizedCommitDate) => {
|
||||||
const localized = localizeTimestamp(triggerAboutGraphiteLocalizedCommitDate.commitDate);
|
const localized = localizeTimestamp(triggerAboutGraphiteLocalizedCommitDate.commitDate);
|
||||||
editor.instance.requestAboutGraphiteDialogWithLocalizedCommitDate(localized);
|
editor.instance.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import { wipeDocuments } from "@graphite/io-managers/persistence";
|
|
||||||
import { type DialogState } from "@graphite/state-providers/dialog";
|
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 { browserVersion, operatingSystem } from "@graphite/utility-functions/platform";
|
||||||
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
||||||
import { type Editor } from "@graphite/wasm-communication/editor";
|
import { type Editor } from "@graphite/wasm-communication/editor";
|
||||||
import type { TextLabel } from "@graphite/wasm-communication/messages";
|
import { DisplayDialogPanic } from "@graphite/wasm-communication/messages";
|
||||||
import { type TextButtonWidget, type WidgetLayout, Widget, DisplayDialogPanic } from "@graphite/wasm-communication/messages";
|
|
||||||
|
|
||||||
export function createPanicManager(editor: Editor, dialogState: DialogState): void {
|
export function createPanicManager(editor: Editor, dialogState: DialogState): void {
|
||||||
// Code panic dialog and console error
|
// Code panic dialog and console error
|
||||||
|
@ -19,45 +16,11 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(panicDetails);
|
console.error(panicDetails);
|
||||||
|
|
||||||
const crashDialog = prepareCrashDialog(displayDialogPanic.header, displayDialogPanic.description, panicDetails);
|
dialogState.createCrashDialog(panicDetails);
|
||||||
dialogState.createCrashDialog(...crashDialog);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareCrashDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
|
export function githubUrl(panicDetails: string): string {
|
||||||
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 {
|
|
||||||
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
|
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
|
||||||
|
|
||||||
let body = stripIndents`
|
let body = stripIndents`
|
||||||
|
|
|
@ -2,23 +2,25 @@ import {writable} from "svelte/store";
|
||||||
|
|
||||||
import { type IconName } from "@graphite/utility-functions/icons";
|
import { type IconName } from "@graphite/utility-functions/icons";
|
||||||
import { type Editor } from "@graphite/wasm-communication/editor";
|
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
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export function createDialogState(editor: Editor) {
|
export function createDialogState(editor: Editor) {
|
||||||
const { subscribe, update } = writable({
|
const { subscribe, update } = writable({
|
||||||
visible: false,
|
visible: false,
|
||||||
|
title: "",
|
||||||
icon: "" as IconName,
|
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
|
// 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 {
|
function dismissDialog(): void {
|
||||||
|
|
||||||
update((state) => {
|
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
|
// 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;
|
return state;
|
||||||
});
|
});
|
||||||
|
@ -26,12 +28,18 @@ export function createDialogState(editor: Editor) {
|
||||||
|
|
||||||
// Creates a crash dialog from JS once the editor has panicked.
|
// 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.
|
// 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) => {
|
update((state) => {
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
state.icon = icon;
|
|
||||||
state.widgets = widgets;
|
state.icon = "Failure";
|
||||||
state.crashDialogButtons = crashDialogButtons;
|
state.title = "Crash";
|
||||||
|
state.panicDetails = panicDetails;
|
||||||
|
|
||||||
|
state.column1 = defaultWidgetLayout();
|
||||||
|
state.column2 = defaultWidgetLayout();
|
||||||
|
state.buttons = defaultWidgetLayout();
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -40,15 +48,31 @@ export function createDialogState(editor: Editor) {
|
||||||
editor.subscriptions.subscribeJsMessage(DisplayDialog, (displayDialog) => {
|
editor.subscriptions.subscribeJsMessage(DisplayDialog, (displayDialog) => {
|
||||||
update((state) => {
|
update((state) => {
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
|
|
||||||
|
state.title = displayDialog.title;
|
||||||
state.icon = displayDialog.icon;
|
state.icon = displayDialog.icon;
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(UpdateDialogDetails, (updateDialogDetails) => {
|
editor.subscriptions.subscribeJsMessage(UpdateDialogButtons, (updateDialogButtons) => {
|
||||||
update((state) => {
|
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;
|
return state;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -57,7 +81,7 @@ export function createDialogState(editor: Editor) {
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
dismissDialog,
|
dismissDialog,
|
||||||
createCrashDialog: createCrashDialog,
|
createCrashDialog,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export type DialogState = ReturnType<typeof createDialogState>;
|
export type DialogState = ReturnType<typeof createDialogState>;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {tick} from "svelte";
|
|
||||||
import {writable} from "svelte/store";
|
import {writable} from "svelte/store";
|
||||||
|
|
||||||
import { type Editor } from "@graphite/wasm-communication/editor";
|
import { type Editor } from "@graphite/wasm-communication/editor";
|
||||||
|
|
|
@ -90,10 +90,15 @@ export function createPortfolioState(editor: Editor) {
|
||||||
const backgroundColor = mime.endsWith("jpeg") ? "white" : undefined;
|
const backgroundColor = mime.endsWith("jpeg") ? "white" : undefined;
|
||||||
|
|
||||||
// Rasterize the SVG to an image file
|
// 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) => {
|
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||||
updateImageData.imageData.forEach(async (element) => {
|
updateImageData.imageData.forEach(async (element) => {
|
||||||
|
|
|
@ -11,9 +11,11 @@ const GRAPHICS = {
|
||||||
import Add from "@graphite-frontend/assets/icon-12px-solid/add.svg";
|
import Add from "@graphite-frontend/assets/icon-12px-solid/add.svg";
|
||||||
import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.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 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 DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg";
|
||||||
import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.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 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 FullscreenEnter from "@graphite-frontend/assets/icon-12px-solid/fullscreen-enter.svg";
|
||||||
import FullscreenExit from "@graphite-frontend/assets/icon-12px-solid/fullscreen-exit.svg";
|
import FullscreenExit from "@graphite-frontend/assets/icon-12px-solid/fullscreen-exit.svg";
|
||||||
import Grid from "@graphite-frontend/assets/icon-12px-solid/grid.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 KeyboardShift from "@graphite-frontend/assets/icon-12px-solid/keyboard-shift.svg";
|
||||||
import KeyboardSpace from "@graphite-frontend/assets/icon-12px-solid/keyboard-space.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 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 Link from "@graphite-frontend/assets/icon-12px-solid/link.svg";
|
||||||
import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg";
|
import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg";
|
||||||
import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg";
|
import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg";
|
||||||
|
@ -47,9 +50,11 @@ const SOLID_12PX = {
|
||||||
Add: { svg: Add, size: 12 },
|
Add: { svg: Add, size: 12 },
|
||||||
Checkmark: { svg: Checkmark, size: 12 },
|
Checkmark: { svg: Checkmark, size: 12 },
|
||||||
CloseX: { svg: CloseX, size: 12 },
|
CloseX: { svg: CloseX, size: 12 },
|
||||||
|
Delay: { svg: Delay, size: 12 },
|
||||||
DropdownArrow: { svg: DropdownArrow, size: 12 },
|
DropdownArrow: { svg: DropdownArrow, size: 12 },
|
||||||
Edit12px: { svg: Edit12px, size: 12 },
|
Edit12px: { svg: Edit12px, size: 12 },
|
||||||
Empty12px: { svg: Empty12px, size: 12 },
|
Empty12px: { svg: Empty12px, size: 12 },
|
||||||
|
Failure: { svg: Failure, size: 12 },
|
||||||
FullscreenEnter: { svg: FullscreenEnter, size: 12 },
|
FullscreenEnter: { svg: FullscreenEnter, size: 12 },
|
||||||
FullscreenExit: { svg: FullscreenExit, size: 12 },
|
FullscreenExit: { svg: FullscreenExit, size: 12 },
|
||||||
Grid: { svg: Grid, size: 12 },
|
Grid: { svg: Grid, size: 12 },
|
||||||
|
@ -66,6 +71,7 @@ const SOLID_12PX = {
|
||||||
KeyboardShift: { svg: KeyboardShift, size: 12 },
|
KeyboardShift: { svg: KeyboardShift, size: 12 },
|
||||||
KeyboardSpace: { svg: KeyboardSpace, size: 12 },
|
KeyboardSpace: { svg: KeyboardSpace, size: 12 },
|
||||||
KeyboardTab: { svg: KeyboardTab, size: 12 },
|
KeyboardTab: { svg: KeyboardTab, size: 12 },
|
||||||
|
License12px: { svg: License12px, size: 12 },
|
||||||
Link: { svg: Link, size: 12 },
|
Link: { svg: Link, size: 12 },
|
||||||
Overlays: { svg: Overlays, size: 12 },
|
Overlays: { svg: Overlays, size: 12 },
|
||||||
Remove: { svg: Remove, 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 CheckboxChecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-checked.svg";
|
||||||
import CheckboxUnchecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-unchecked.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 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 CustomColor from "@graphite-frontend/assets/icon-16px-solid/custom-color.svg";
|
||||||
import Edit from "@graphite-frontend/assets/icon-16px-solid/edit.svg";
|
import Edit from "@graphite-frontend/assets/icon-16px-solid/edit.svg";
|
||||||
import Eyedropper from "@graphite-frontend/assets/icon-16px-solid/eyedropper.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 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 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 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 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 NodeArtboard from "@graphite-frontend/assets/icon-16px-solid/node-artboard.svg";
|
||||||
import NodeBlur from "@graphite-frontend/assets/icon-16px-solid/node-blur.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 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 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 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 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 NodeMagicWand from "@graphite-frontend/assets/icon-16px-solid/node-magic-wand.svg";
|
||||||
import NodeMask from "@graphite-frontend/assets/icon-16px-solid/node-mask.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 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 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 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 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 WorkingColorsSecondary from "@graphite-frontend/assets/icon-16px-solid/working-colors-secondary.svg";
|
||||||
import ZoomIn from "@graphite-frontend/assets/icon-16px-solid/zoom-in.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 },
|
CheckboxChecked: { svg: CheckboxChecked, size: 16 },
|
||||||
CheckboxUnchecked: { svg: CheckboxUnchecked, size: 16 },
|
CheckboxUnchecked: { svg: CheckboxUnchecked, size: 16 },
|
||||||
Copy: { svg: Copy, size: 16 },
|
Copy: { svg: Copy, size: 16 },
|
||||||
|
Credits: { svg: Credits, size: 16 },
|
||||||
CustomColor: { svg: CustomColor, size: 16 },
|
CustomColor: { svg: CustomColor, size: 16 },
|
||||||
Edit: { svg: Edit, size: 16 },
|
Edit: { svg: Edit, size: 16 },
|
||||||
Eyedropper: { svg: Eyedropper, size: 16 },
|
Eyedropper: { svg: Eyedropper, size: 16 },
|
||||||
|
@ -170,13 +182,15 @@ const SOLID_16PX = {
|
||||||
GraphiteLogo: { svg: GraphiteLogo, size: 16 },
|
GraphiteLogo: { svg: GraphiteLogo, size: 16 },
|
||||||
GraphViewClosed: { svg: GraphViewClosed, size: 16 },
|
GraphViewClosed: { svg: GraphViewClosed, size: 16 },
|
||||||
GraphViewOpen: { svg: GraphViewOpen, size: 16 },
|
GraphViewOpen: { svg: GraphViewOpen, size: 16 },
|
||||||
|
IconsGrid: { svg: IconsGrid, size: 16 },
|
||||||
|
Image: { svg: Image, size: 16 },
|
||||||
Layer: { svg: Layer, size: 16 },
|
Layer: { svg: Layer, size: 16 },
|
||||||
|
License: { svg: License, size: 16 },
|
||||||
NodeArtboard: { svg: NodeArtboard, size: 16 },
|
NodeArtboard: { svg: NodeArtboard, size: 16 },
|
||||||
NodeBlur: { svg: NodeBlur, size: 16 },
|
NodeBlur: { svg: NodeBlur, size: 16 },
|
||||||
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
|
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
|
||||||
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
|
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
|
||||||
NodeGradient: { svg: NodeGradient, size: 16 },
|
NodeGradient: { svg: NodeGradient, size: 16 },
|
||||||
Image: { svg: Image, size: 16 },
|
|
||||||
NodeImaginate: { svg: NodeImaginate, size: 16 },
|
NodeImaginate: { svg: NodeImaginate, size: 16 },
|
||||||
NodeMagicWand: { svg: NodeMagicWand, size: 16 },
|
NodeMagicWand: { svg: NodeMagicWand, size: 16 },
|
||||||
NodeMask: { svg: NodeMask, size: 16 },
|
NodeMask: { svg: NodeMask, size: 16 },
|
||||||
|
@ -200,6 +214,8 @@ const SOLID_16PX = {
|
||||||
ViewportDesignMode: { svg: ViewportDesignMode, size: 16 },
|
ViewportDesignMode: { svg: ViewportDesignMode, size: 16 },
|
||||||
ViewportGuideMode: { svg: ViewportGuideMode, size: 16 },
|
ViewportGuideMode: { svg: ViewportGuideMode, size: 16 },
|
||||||
ViewportSelectMode: { svg: ViewportSelectMode, size: 16 },
|
ViewportSelectMode: { svg: ViewportSelectMode, size: 16 },
|
||||||
|
Volunteer: { svg: Volunteer, size: 16 },
|
||||||
|
Website: { svg: Website, size: 16 },
|
||||||
WorkingColorsPrimary: { svg: WorkingColorsPrimary, size: 16 },
|
WorkingColorsPrimary: { svg: WorkingColorsPrimary, size: 16 },
|
||||||
WorkingColorsSecondary: { svg: WorkingColorsSecondary, size: 16 },
|
WorkingColorsSecondary: { svg: WorkingColorsSecondary, size: 16 },
|
||||||
ZoomIn: { svg: ZoomIn, size: 16 },
|
ZoomIn: { svg: ZoomIn, size: 16 },
|
||||||
|
|
|
@ -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
|
// 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> {
|
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);
|
const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);
|
||||||
|
|
||||||
// Convert the canvas to an image of the correct MIME type
|
// Convert the canvas to an image of the correct MIME type
|
||||||
|
|
|
@ -103,7 +103,7 @@ export function createEditor() {
|
||||||
instance.openDocumentFile(filename, content);
|
instance.openDocumentFile(filename, content);
|
||||||
|
|
||||||
// Remove the hash fragment from the URL
|
// Remove the hash fragment from the URL
|
||||||
window.location.hash = "";
|
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
|
||||||
} catch {}
|
} catch {}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -425,13 +425,10 @@ export class UpdateActiveDocument extends JsMessage {
|
||||||
|
|
||||||
export class DisplayDialogPanic extends JsMessage {
|
export class DisplayDialogPanic extends JsMessage {
|
||||||
readonly panicInfo!: string;
|
readonly panicInfo!: string;
|
||||||
|
|
||||||
readonly header!: string;
|
|
||||||
|
|
||||||
readonly description!: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DisplayDialog extends JsMessage {
|
export class DisplayDialog extends JsMessage {
|
||||||
|
readonly title!: string;
|
||||||
readonly icon!: IconName;
|
readonly icon!: IconName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1049,6 +1046,8 @@ export class TextButton extends WidgetProps {
|
||||||
|
|
||||||
emphasized!: boolean;
|
emphasized!: boolean;
|
||||||
|
|
||||||
|
noBackground!: boolean;
|
||||||
|
|
||||||
minWidth!: number;
|
minWidth!: number;
|
||||||
|
|
||||||
disabled!: boolean;
|
disabled!: boolean;
|
||||||
|
@ -1066,6 +1065,7 @@ export type TextButtonWidget = {
|
||||||
label: string;
|
label: string;
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
emphasized?: boolean;
|
emphasized?: boolean;
|
||||||
|
noBackground?: boolean;
|
||||||
minWidth?: number;
|
minWidth?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
tooltip?: string;
|
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
|
// 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;
|
layout.layoutTarget = updates.layoutTarget;
|
||||||
|
|
||||||
updates.diff.forEach((update) => {
|
updates.diff.forEach((update) => {
|
||||||
|
@ -1320,7 +1320,11 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WIDGET LAYOUTS
|
// 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 { }
|
export class UpdateDocumentBarLayout extends WidgetDiffUpdate { }
|
||||||
|
|
||||||
|
@ -1407,7 +1411,9 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||||
TriggerViewportResize,
|
TriggerViewportResize,
|
||||||
TriggerVisitLink,
|
TriggerVisitLink,
|
||||||
UpdateActiveDocument,
|
UpdateActiveDocument,
|
||||||
UpdateDialogDetails,
|
UpdateDialogButtons,
|
||||||
|
UpdateDialogColumn1,
|
||||||
|
UpdateDialogColumn2,
|
||||||
UpdateDocumentArtboards,
|
UpdateDocumentArtboards,
|
||||||
UpdateDocumentArtwork,
|
UpdateDocumentArtwork,
|
||||||
UpdateDocumentBarLayout,
|
UpdateDocumentBarLayout,
|
||||||
|
|
|
@ -354,8 +354,11 @@ impl JsEditorHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = requestAboutGraphiteDialogWithLocalizedCommitDate)]
|
#[wasm_bindgen(js_name = requestAboutGraphiteDialogWithLocalizedCommitDate)]
|
||||||
pub fn request_about_graphite_dialog_with_localized_commit_date(&self, localized_commit_date: String) {
|
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 };
|
let message = DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
||||||
|
localized_commit_date,
|
||||||
|
localized_commit_year,
|
||||||
|
};
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
/// 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) {
|
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);
|
error!("{}", info);
|
||||||
|
|
||||||
JS_EDITOR_HANDLES.with(|instances| {
|
JS_EDITOR_HANDLES.with(|instances| {
|
||||||
instances.borrow_mut().values_mut().for_each(|instance| {
|
instances
|
||||||
instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic {
|
.borrow_mut()
|
||||||
panic_info: info.to_string(),
|
.values_mut()
|
||||||
header: header.to_string(),
|
.for_each(|instance| instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() }))
|
||||||
description: description.to_string(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|