mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 02:48:12 +08:00
Migrate dialogs to Rust and add a New File dialog (#623)
* Migrate coming soon and about dialog to Rust * Migrate confirm close and close all * Migrate dialog error * Improve keyboard navigation throughout UI * Cleanup and fix panic dialog * Reduce css spacing to better match old dialogs * Add new document modal * Fix crash when generating default name * Populate rust about graphite data on startup * Code review changes * Move one more :focus CSS rule into App.vue * Add a dialog message and move dialogs * Split out keyboard input navigation from this branch * Improvements including simplifying panic dialog code Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
parent
7729b6219e
commit
f177f63217
45
editor/src/communication/build_metadata.rs
Normal file
45
editor/src/communication/build_metadata.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Provides metadata about the build environment.
|
||||
///
|
||||
/// This data is viewable in the editor via the [`crate::dialog::AboutGraphite`] dialog.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BuildMetadata {
|
||||
pub release: String,
|
||||
pub timestamp: String,
|
||||
pub hash: String,
|
||||
pub branch: String,
|
||||
}
|
||||
|
||||
impl Default for BuildMetadata {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
release: "unknown".to_string(),
|
||||
timestamp: "unknown".to_string(),
|
||||
hash: "unknown".to_string(),
|
||||
branch: "unknown".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildMetadata {
|
||||
pub fn release_series(&self) -> String {
|
||||
format!("Release Series: {}", self.release)
|
||||
}
|
||||
|
||||
pub fn commit_info(&self) -> String {
|
||||
format!("{}\n{}\n{}", self.commit_timestamp(), self.commit_hash(), self.commit_branch())
|
||||
}
|
||||
|
||||
pub fn commit_timestamp(&self) -> String {
|
||||
format!("Date: {}", self.timestamp)
|
||||
}
|
||||
|
||||
pub fn commit_hash(&self) -> String {
|
||||
format!("Hash: {}", self.hash)
|
||||
}
|
||||
|
||||
pub fn commit_branch(&self) -> String {
|
||||
format!("Branch: {}", self.branch)
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,20 @@ use crate::viewport_tools::tool_message_handler::ToolMessageHandler;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::BuildMetadata;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Dispatcher {
|
||||
message_queue: VecDeque<Message>,
|
||||
pub responses: Vec<FrontendMessage>,
|
||||
message_handlers: DispatcherMessageHandlers,
|
||||
build_metadata: BuildMetadata,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(Debug, Default)]
|
||||
struct DispatcherMessageHandlers {
|
||||
dialog_message_handler: DialogMessageHandler,
|
||||
global_message_handler: GlobalMessageHandler,
|
||||
input_mapper_message_handler: InputMapperMessageHandler,
|
||||
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
|
||||
@@ -31,6 +35,9 @@ struct DispatcherMessageHandlers {
|
||||
const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Rerender))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Artboard(
|
||||
ArtboardMessageDiscriminant::RenderArtboards,
|
||||
))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayer),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::DisplayDocumentLayerTreeStructure),
|
||||
@@ -63,6 +70,11 @@ impl Dispatcher {
|
||||
match message {
|
||||
#[remain::unsorted]
|
||||
NoOp => {}
|
||||
Dialog(message) => {
|
||||
self.message_handlers
|
||||
.dialog_message_handler
|
||||
.process_action(message, (&self.build_metadata, &self.message_handlers.portfolio_message_handler), &mut self.message_queue);
|
||||
}
|
||||
Frontend(message) => {
|
||||
// Image and font loading should be immediately handled
|
||||
if let FrontendMessage::UpdateImageData { .. } | FrontendMessage::TriggerFontLoad { .. } = message {
|
||||
@@ -101,6 +113,9 @@ impl Dispatcher {
|
||||
&mut self.message_queue,
|
||||
);
|
||||
}
|
||||
|
||||
#[remain::unsorted]
|
||||
PopulateBuildMetadata { new } => self.build_metadata = new,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +123,7 @@ impl Dispatcher {
|
||||
pub fn collect_actions(&self) -> ActionList {
|
||||
// TODO: Reduce the number of heap allocations
|
||||
let mut list = Vec::new();
|
||||
list.extend(self.message_handlers.dialog_message_handler.actions());
|
||||
list.extend(self.message_handlers.input_preprocessor_message_handler.actions());
|
||||
list.extend(self.message_handlers.input_mapper_message_handler.actions());
|
||||
list.extend(self.message_handlers.global_message_handler.actions());
|
||||
@@ -434,18 +450,22 @@ mod test {
|
||||
});
|
||||
|
||||
for response in responses {
|
||||
if let FrontendMessage::DisplayDialogError { title, description } = response {
|
||||
println!();
|
||||
println!("-------------------------------------------------");
|
||||
println!("Failed test due to receiving a DisplayDialogError while loading the graphite sample file!");
|
||||
println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`");
|
||||
println!("Once bumping this version number please replace the `graphite-test-document.graphite` with a valid file");
|
||||
println!("DisplayDialogError details:");
|
||||
println!("Title: {}", title);
|
||||
println!("description: {}", description);
|
||||
println!("-------------------------------------------------");
|
||||
println!();
|
||||
panic!()
|
||||
if let FrontendMessage::UpdateDialogDetails { layout_target: _, layout } = response {
|
||||
if let crate::layout::widgets::LayoutRow::Row { widgets } = &layout[0] {
|
||||
if let crate::layout::widgets::Widget::TextLabel(crate::layout::widgets::TextLabel { value, .. }) = &widgets[0].widget {
|
||||
println!();
|
||||
println!("-------------------------------------------------");
|
||||
println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file!");
|
||||
println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`");
|
||||
println!("Once bumping this version number please replace the `graphite-test-document.graphite` with a valid file.");
|
||||
println!("DisplayDialogError details:");
|
||||
println!();
|
||||
println!("Description: {}", value);
|
||||
println!("-------------------------------------------------");
|
||||
println!();
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::BuildMetadata;
|
||||
use crate::message_prelude::*;
|
||||
|
||||
use graphite_proc_macros::*;
|
||||
@@ -23,6 +24,8 @@ pub enum Message {
|
||||
#[remain::unsorted]
|
||||
NoOp,
|
||||
#[child]
|
||||
Dialog(DialogMessage),
|
||||
#[child]
|
||||
Frontend(FrontendMessage),
|
||||
#[child]
|
||||
Global(GlobalMessage),
|
||||
@@ -36,6 +39,9 @@ pub enum Message {
|
||||
Portfolio(PortfolioMessage),
|
||||
#[child]
|
||||
Tool(ToolMessage),
|
||||
|
||||
#[remain::unsorted]
|
||||
PopulateBuildMetadata { new: BuildMetadata },
|
||||
}
|
||||
|
||||
impl Message {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod build_metadata;
|
||||
pub mod dispatcher;
|
||||
pub mod message;
|
||||
pub mod message_handler;
|
||||
|
||||
pub use crate::communication::dispatcher::*;
|
||||
pub use crate::input::InputPreprocessorMessageHandler;
|
||||
pub use build_metadata::BuildMetadata;
|
||||
|
||||
use rand_chacha::rand_core::{RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
27
editor/src/dialog/dialog_message.rs
Normal file
27
editor/src/dialog/dialog_message.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use crate::message_prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::NewDocumentDialogUpdate;
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, Dialog)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum DialogMessage {
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
NewDocumentDialog(NewDocumentDialogUpdate),
|
||||
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
CloseDialogAndThen {
|
||||
followup: Box<Message>,
|
||||
},
|
||||
DisplayDialogError {
|
||||
title: String,
|
||||
description: String,
|
||||
},
|
||||
RequestAboutGraphiteDialog,
|
||||
RequestComingSoonDialog {
|
||||
issue: Option<i32>,
|
||||
},
|
||||
RequestNewDocumentDialog,
|
||||
}
|
||||
60
editor/src/dialog/dialog_message_handler.rs
Normal file
60
editor/src/dialog/dialog_message_handler.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use crate::communication::BuildMetadata;
|
||||
use crate::document::PortfolioMessageHandler;
|
||||
use crate::layout::{layout_message::LayoutTarget, widgets::PropertyHolder};
|
||||
use crate::message_prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DialogMessageHandler {
|
||||
new_document_dialog: NewDocument,
|
||||
}
|
||||
|
||||
impl MessageHandler<DialogMessage, (&BuildMetadata, &PortfolioMessageHandler)> for DialogMessageHandler {
|
||||
#[remain::check]
|
||||
fn process_action(&mut self, message: DialogMessage, (build_metadata, portfolio): (&BuildMetadata, &PortfolioMessageHandler), responses: &mut VecDeque<Message>) {
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
#[remain::unsorted]
|
||||
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_action(message, (), responses),
|
||||
|
||||
DialogMessage::CloseAllDocumentsWithConfirmation => {
|
||||
let dialog = dialogs::CloseAllDocuments;
|
||||
dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "Copy".to_string() }.into());
|
||||
}
|
||||
DialogMessage::CloseDialogAndThen { followup } => {
|
||||
responses.push_back(FrontendMessage::DisplayDialogDismiss.into());
|
||||
responses.push_back(*followup);
|
||||
}
|
||||
DialogMessage::DisplayDialogError { title, description } => {
|
||||
let dialog = dialogs::Error { title, description };
|
||||
dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }.into());
|
||||
}
|
||||
DialogMessage::RequestAboutGraphiteDialog => {
|
||||
let about_graphite = AboutGraphite {
|
||||
build_metadata: build_metadata.clone(),
|
||||
};
|
||||
about_graphite.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "GraphiteLogo".to_string() }.into());
|
||||
}
|
||||
DialogMessage::RequestComingSoonDialog { issue } => {
|
||||
let coming_soon = ComingSoon { issue };
|
||||
coming_soon.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }.into());
|
||||
}
|
||||
DialogMessage::RequestNewDocumentDialog => {
|
||||
self.new_document_dialog = NewDocument {
|
||||
name: portfolio.generate_new_document_name(),
|
||||
infinite: true,
|
||||
dimensions: glam::UVec2::new(1920, 1080),
|
||||
};
|
||||
self.new_document_dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(DialogMessageDiscriminant;RequestNewDocumentDialog,CloseAllDocumentsWithConfirmation);
|
||||
}
|
||||
50
editor/src/dialog/dialogs/about_dialog.rs
Normal file
50
editor/src/dialog/dialogs/about_dialog.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::{communication::BuildMetadata, layout::widgets::*, message_prelude::FrontendMessage};
|
||||
|
||||
/// A dialog for displaying information on [`BuildMetadata`] viewable via `help -> about graphite` in the menu bar.
|
||||
pub struct AboutGraphite {
|
||||
pub build_metadata: BuildMetadata,
|
||||
}
|
||||
|
||||
impl PropertyHolder for AboutGraphite {
|
||||
fn properties(&self) -> WidgetLayout {
|
||||
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)| {
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: label.to_string(),
|
||||
on_update: WidgetCallback::new(|_| FrontendMessage::TriggerVisitLink { url: url.to_string() }.into()),
|
||||
..Default::default()
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
WidgetLayout::new(vec![
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Graphite".to_string(),
|
||||
bold: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: self.build_metadata.release_series(),
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: self.build_metadata.commit_info(),
|
||||
multiline: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row { widgets: link_widgets },
|
||||
])
|
||||
}
|
||||
}
|
||||
47
editor/src/dialog/dialogs/close_all_documents_dialog.rs
Normal file
47
editor/src/dialog/dialogs/close_all_documents_dialog.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::layout::widgets::*;
|
||||
use crate::message_prelude::{DialogMessage, FrontendMessage, PortfolioMessage};
|
||||
|
||||
/// A dialog for confirming the closing of all documents viewable via `file -> close all` in the menu bar.
|
||||
pub struct CloseAllDocuments;
|
||||
|
||||
impl PropertyHolder for CloseAllDocuments {
|
||||
fn properties(&self) -> WidgetLayout {
|
||||
let button_widgets = vec![
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Discard All".to_string(),
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(PortfolioMessage::CloseAllDocuments.into()),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Cancel".to_string(),
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
];
|
||||
|
||||
WidgetLayout::new(vec![
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Close all documents?".to_string(),
|
||||
bold: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Unsaved work will be lost!".to_string(),
|
||||
multiline: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row { widgets: button_widgets },
|
||||
])
|
||||
}
|
||||
}
|
||||
64
editor/src/dialog/dialogs/close_document_dialog.rs
Normal file
64
editor/src/dialog/dialogs/close_document_dialog.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use crate::layout::widgets::*;
|
||||
use crate::message_prelude::{DialogMessage, DocumentMessage, FrontendMessage, PortfolioMessage};
|
||||
|
||||
/// A dialog for confirming the closing a document with unsaved changes.
|
||||
pub struct CloseDocument {
|
||||
pub document_name: String,
|
||||
pub document_id: u64,
|
||||
}
|
||||
|
||||
impl PropertyHolder for CloseDocument {
|
||||
fn properties(&self) -> WidgetLayout {
|
||||
let document_id = self.document_id;
|
||||
|
||||
let button_widgets = vec![
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Save".to_string(),
|
||||
min_width: 96,
|
||||
emphasized: true,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(DocumentMessage::SaveDocument.into()),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Discard".to_string(),
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(move |_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(PortfolioMessage::CloseDocument { document_id }.into()),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Cancel".to_string(),
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
];
|
||||
|
||||
WidgetLayout::new(vec![
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Save changes before closing?".to_string(),
|
||||
bold: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: self.document_name.clone(),
|
||||
multiline: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row { widgets: button_widgets },
|
||||
])
|
||||
}
|
||||
}
|
||||
50
editor/src/dialog/dialogs/coming_soon_dialog.rs
Normal file
50
editor/src/dialog/dialogs/coming_soon_dialog.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::{layout::widgets::*, message_prelude::FrontendMessage};
|
||||
|
||||
/// A dialog to notify users of an unfinished issue, optionally with an issue number.
|
||||
pub struct ComingSoon {
|
||||
pub issue: Option<i32>,
|
||||
}
|
||||
|
||||
impl PropertyHolder for ComingSoon {
|
||||
fn properties(&self) -> WidgetLayout {
|
||||
let mut details = "This feature is not implemented yet".to_string();
|
||||
let mut buttons = vec![WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "OK".to_string(),
|
||||
emphasized: true,
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||
..Default::default()
|
||||
}))];
|
||||
if let Some(issue) = self.issue {
|
||||
details += &format!("— but you can help add it!\nSee issue #{issue} on GitHub.");
|
||||
buttons.push(WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: format!("Issue #{issue}"),
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(move |_| {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})));
|
||||
}
|
||||
WidgetLayout::new(vec![
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Coming soon".to_string(),
|
||||
bold: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: details,
|
||||
multiline: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row { widgets: buttons },
|
||||
])
|
||||
}
|
||||
}
|
||||
37
editor/src/dialog/dialogs/error_dialog.rs
Normal file
37
editor/src/dialog/dialogs/error_dialog.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use crate::{layout::widgets::*, message_prelude::FrontendMessage};
|
||||
|
||||
/// A dialog to notify users of a non-fatal error.
|
||||
pub struct Error {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl PropertyHolder for Error {
|
||||
fn properties(&self) -> WidgetLayout {
|
||||
WidgetLayout::new(vec![
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: self.title.clone(),
|
||||
bold: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: self.description.clone(),
|
||||
multiline: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "OK".to_string(),
|
||||
emphasized: true,
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
13
editor/src/dialog/dialogs/mod.rs
Normal file
13
editor/src/dialog/dialogs/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
mod about_dialog;
|
||||
mod close_all_documents_dialog;
|
||||
mod close_document_dialog;
|
||||
mod coming_soon_dialog;
|
||||
mod error_dialog;
|
||||
mod new_document_dialog;
|
||||
|
||||
pub use about_dialog::AboutGraphite;
|
||||
pub use close_all_documents_dialog::CloseAllDocuments;
|
||||
pub use close_document_dialog::CloseDocument;
|
||||
pub use coming_soon_dialog::ComingSoon;
|
||||
pub use error_dialog::Error;
|
||||
pub use new_document_dialog::{NewDocument, NewDocumentDialogUpdate, NewDocumentDialogUpdateDiscriminant};
|
||||
177
editor/src/dialog/dialogs/new_document_dialog.rs
Normal file
177
editor/src/dialog/dialogs/new_document_dialog.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use crate::layout::layout_message::LayoutTarget;
|
||||
use crate::layout::widgets::*;
|
||||
use crate::message_prelude::*;
|
||||
|
||||
use glam::UVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A dialog to allow users to set some initial options about a new document.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct NewDocument {
|
||||
pub name: String,
|
||||
pub infinite: bool,
|
||||
pub dimensions: UVec2,
|
||||
}
|
||||
|
||||
impl PropertyHolder for NewDocument {
|
||||
fn properties(&self) -> WidgetLayout {
|
||||
let title = vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "New document".into(),
|
||||
bold: true,
|
||||
..Default::default()
|
||||
}))];
|
||||
|
||||
let name = vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Name".into(),
|
||||
table_align: true,
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||
value: self.name.clone(),
|
||||
on_update: WidgetCallback::new(|text_input: &TextInput| NewDocumentDialogUpdate::Name(text_input.value.clone()).into()),
|
||||
})),
|
||||
];
|
||||
|
||||
let infinite = vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Infinite Canvas".into(),
|
||||
table_align: true,
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
|
||||
checked: self.infinite,
|
||||
icon: "Checkmark".to_string(),
|
||||
on_update: WidgetCallback::new(|checkbox_input: &CheckboxInput| NewDocumentDialogUpdate::Infinite(checkbox_input.checked).into()),
|
||||
..Default::default()
|
||||
})),
|
||||
];
|
||||
|
||||
let scale = vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Dimensions".into(),
|
||||
table_align: true,
|
||||
..TextLabel::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: self.dimensions.x as f64,
|
||||
label: "W".into(),
|
||||
unit: " px".into(),
|
||||
disabled: self.infinite,
|
||||
is_integer: true,
|
||||
min: Some(0.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogUpdate::DimensionsX(number_input.value).into()),
|
||||
..NumberInput::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: self.dimensions.y as f64,
|
||||
label: "H".into(),
|
||||
unit: " px".into(),
|
||||
disabled: self.infinite,
|
||||
is_integer: true,
|
||||
min: Some(0.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogUpdate::DimensionsY(number_input.value).into()),
|
||||
..NumberInput::default()
|
||||
})),
|
||||
];
|
||||
|
||||
let button_widgets = vec![
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "OK".to_string(),
|
||||
min_width: 96,
|
||||
emphasized: true,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(NewDocumentDialogUpdate::Submit.into()),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Cancel".to_string(),
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
];
|
||||
|
||||
WidgetLayout::new(vec![
|
||||
LayoutRow::Row { widgets: title },
|
||||
LayoutRow::Row { widgets: name },
|
||||
LayoutRow::Row { widgets: infinite },
|
||||
LayoutRow::Row { widgets: scale },
|
||||
LayoutRow::Row { widgets: button_widgets },
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[impl_message(Message, DialogMessage, NewDocumentDialog)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum NewDocumentDialogUpdate {
|
||||
Name(String),
|
||||
Infinite(bool),
|
||||
DimensionsX(f64),
|
||||
DimensionsY(f64),
|
||||
|
||||
Submit,
|
||||
BufferArtboard,
|
||||
AddArtboard,
|
||||
FitCanvas,
|
||||
}
|
||||
|
||||
impl MessageHandler<NewDocumentDialogUpdate, ()> for NewDocument {
|
||||
fn process_action(&mut self, action: NewDocumentDialogUpdate, _data: (), responses: &mut VecDeque<Message>) {
|
||||
match action {
|
||||
NewDocumentDialogUpdate::Name(name) => self.name = name,
|
||||
NewDocumentDialogUpdate::Infinite(infinite) => self.infinite = infinite,
|
||||
NewDocumentDialogUpdate::DimensionsX(x) => self.dimensions.x = x as u32,
|
||||
NewDocumentDialogUpdate::DimensionsY(y) => self.dimensions.y = y as u32,
|
||||
|
||||
NewDocumentDialogUpdate::Submit => {
|
||||
responses.push_back(PortfolioMessage::NewDocumentWithName { name: self.name.clone() }.into());
|
||||
|
||||
responses.push_back(NewDocumentDialogUpdate::BufferArtboard.into());
|
||||
}
|
||||
NewDocumentDialogUpdate::BufferArtboard => {
|
||||
if !self.infinite {
|
||||
responses.push_back(NewDocumentDialogUpdate::AddArtboard.into());
|
||||
}
|
||||
}
|
||||
NewDocumentDialogUpdate::AddArtboard => {
|
||||
responses.push_back(
|
||||
ArtboardMessage::AddArtboard {
|
||||
id: None,
|
||||
position: (0., 0.),
|
||||
size: (self.dimensions.x as f64, self.dimensions.y as f64),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(NewDocumentDialogUpdate::FitCanvas.into());
|
||||
}
|
||||
NewDocumentDialogUpdate::FitCanvas => {
|
||||
responses.push_back(DocumentMessage::ZoomCanvasToFitAll.into());
|
||||
}
|
||||
}
|
||||
|
||||
self.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
}
|
||||
|
||||
advertise_actions! {NewDocumentDialogUpdate;}
|
||||
}
|
||||
17
editor/src/dialog/mod.rs
Normal file
17
editor/src/dialog/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
//! Handles dialogs/modals/popups that appear as boxes in the centre of the editor.
|
||||
//!
|
||||
//! Dialogs are represented as structs that implement the [`crate::layout::widgets::PropertyHolder`] trait.
|
||||
//!
|
||||
//! To open a dialog, call the function `register_properties` on the dialog struct with `responses` and the `LayoutTarget::DialogDetails`
|
||||
//! and then you can open the dialog with [`crate::message_prelude::FrontendMessage::DisplayDialog`]
|
||||
|
||||
mod dialog_message;
|
||||
mod dialog_message_handler;
|
||||
mod dialogs;
|
||||
|
||||
pub mod messages {
|
||||
pub use super::dialog_message::{DialogMessage, DialogMessageDiscriminant};
|
||||
pub use super::dialog_message_handler::DialogMessageHandler;
|
||||
}
|
||||
|
||||
pub use dialogs::*;
|
||||
@@ -509,7 +509,7 @@ impl DocumentMessageHandler {
|
||||
|
||||
pub fn load_default_font(&self, responses: &mut VecDeque<Message>) {
|
||||
if !self.graphene_document.font_cache.has_default() {
|
||||
responses.push_back(FrontendMessage::TriggerDefaultFontLoad.into())
|
||||
responses.push_back(FrontendMessage::TriggerFontLoadDefault.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,7 +536,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
||||
checked: true,
|
||||
icon: "Grid".into(),
|
||||
tooltip: "Grid".into(),
|
||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(318) }.into()),
|
||||
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()),
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Grid".into(),
|
||||
@@ -581,7 +581,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
||||
value: "pixels".into(),
|
||||
icon: "ViewModePixels".into(),
|
||||
tooltip: "View Mode: Pixels".into(),
|
||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(320) }.into()),
|
||||
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(320) }.into()),
|
||||
..RadioEntryData::default()
|
||||
},
|
||||
],
|
||||
|
||||
@@ -21,7 +21,6 @@ pub enum PortfolioMessage {
|
||||
},
|
||||
CloseActiveDocumentWithConfirmation,
|
||||
CloseAllDocuments,
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
CloseDocument {
|
||||
document_id: u64,
|
||||
},
|
||||
@@ -35,6 +34,9 @@ pub enum PortfolioMessage {
|
||||
clipboard: Clipboard,
|
||||
},
|
||||
NewDocument,
|
||||
NewDocumentWithName {
|
||||
name: String,
|
||||
},
|
||||
NextDocument,
|
||||
OpenDocument,
|
||||
OpenDocumentFile {
|
||||
@@ -59,11 +61,10 @@ pub enum PortfolioMessage {
|
||||
data: String,
|
||||
},
|
||||
PrevDocument,
|
||||
RequestAboutGraphiteDialog,
|
||||
SelectDocument {
|
||||
document_id: u64,
|
||||
},
|
||||
SetActiveDcoument {
|
||||
SetActiveDocument {
|
||||
document_id: u64,
|
||||
},
|
||||
UpdateDocumentBar,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::frontend::utility_types::FrontendDocumentDetails;
|
||||
use crate::input::InputPreprocessorMessageHandler;
|
||||
use crate::layout::layout_message::LayoutTarget;
|
||||
use crate::layout::widgets::PropertyHolder;
|
||||
use crate::message_prelude::*;
|
||||
use crate::{dialog, message_prelude::*};
|
||||
|
||||
use graphene::Operation as DocumentOperation;
|
||||
|
||||
@@ -29,14 +29,13 @@ impl PortfolioMessageHandler {
|
||||
self.documents.get_mut(&self.active_document_id).unwrap()
|
||||
}
|
||||
|
||||
fn generate_new_document_name(&self) -> String {
|
||||
pub fn generate_new_document_name(&self) -> String {
|
||||
let mut doc_title_numbers = self
|
||||
.ordered_document_iterator()
|
||||
.map(|doc| {
|
||||
.filter_map(|doc| {
|
||||
doc.name
|
||||
.rsplit_once(DEFAULT_DOCUMENT_NAME)
|
||||
.map(|(prefix, number)| (prefix.is_empty()).then(|| number.trim().parse::<isize>().ok()).flatten().unwrap_or(1))
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<isize>>();
|
||||
|
||||
@@ -168,9 +167,6 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
// Create a new blank document
|
||||
responses.push_back(NewDocument.into());
|
||||
}
|
||||
CloseAllDocumentsWithConfirmation => {
|
||||
responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into());
|
||||
}
|
||||
CloseDocument { document_id } => {
|
||||
let document_index = self.document_index(document_id);
|
||||
self.documents.remove(&document_id);
|
||||
@@ -222,7 +218,13 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_back(PortfolioMessage::CloseDocument { document_id }.into());
|
||||
} else {
|
||||
responses.push_back(FrontendMessage::DisplayConfirmationToCloseDocument { document_id }.into());
|
||||
let dialog = dialog::CloseDocument {
|
||||
document_name: target_document.name.clone(),
|
||||
document_id,
|
||||
};
|
||||
dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
|
||||
|
||||
// Select the document being closed
|
||||
responses.push_back(PortfolioMessage::SelectDocument { document_id }.into());
|
||||
}
|
||||
@@ -266,6 +268,12 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
self.load_document(new_document, document_id, false, responses);
|
||||
}
|
||||
NewDocumentWithName { name } => {
|
||||
let new_document = DocumentMessageHandler::with_name(name, ipp);
|
||||
let document_id = generate_uuid();
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
self.load_document(new_document, document_id, false, responses);
|
||||
}
|
||||
NextDocument => {
|
||||
let current_index = self.document_index(self.active_document_id);
|
||||
let next_index = (current_index + 1) % self.document_ids.len();
|
||||
@@ -303,7 +311,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
self.load_document(document, document_id, true, responses);
|
||||
}
|
||||
Err(e) => responses.push_back(
|
||||
FrontendMessage::DisplayDialogError {
|
||||
DialogMessage::DisplayDialogError {
|
||||
title: "Failed to open document".to_string(),
|
||||
description: e.to_string(),
|
||||
}
|
||||
@@ -408,16 +416,14 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
let prev_id = self.document_ids[prev_index];
|
||||
responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into());
|
||||
}
|
||||
RequestAboutGraphiteDialog => {
|
||||
responses.push_back(FrontendMessage::DisplayDialogAboutGraphite.into());
|
||||
}
|
||||
|
||||
SelectDocument { document_id } => {
|
||||
let active_document = self.active_document();
|
||||
if !active_document.is_saved() {
|
||||
responses.push_back(PortfolioMessage::AutoSaveDocument { document_id: self.active_document_id }.into());
|
||||
}
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_back(SetActiveDcoument { document_id }.into());
|
||||
responses.push_back(SetActiveDocument { document_id }.into());
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into());
|
||||
responses.push_back(RenderDocument.into());
|
||||
@@ -428,7 +434,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
|
||||
}
|
||||
SetActiveDcoument { document_id } => {
|
||||
SetActiveDocument { document_id } => {
|
||||
self.active_document_id = document_id;
|
||||
}
|
||||
UpdateDocumentBar => {
|
||||
@@ -457,7 +463,6 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
let mut common = actions!(PortfolioMessageDiscriminant;
|
||||
NewDocument,
|
||||
CloseActiveDocumentWithConfirmation,
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
CloseAllDocuments,
|
||||
NextDocument,
|
||||
PrevDocument,
|
||||
|
||||
@@ -13,32 +13,31 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
|
||||
pub enum FrontendMessage {
|
||||
// Display prefix: make the frontend show something, like a dialog
|
||||
DisplayConfirmationToCloseAllDocuments,
|
||||
DisplayConfirmationToCloseDocument { document_id: u64 },
|
||||
DisplayDialogAboutGraphite,
|
||||
DisplayDialogComingSoon { issue: Option<i32> },
|
||||
DisplayDialogError { title: String, description: String },
|
||||
DisplayDialog { icon: String },
|
||||
DisplayDialogDismiss,
|
||||
DisplayDialogPanic { panic_info: String, title: String, description: String },
|
||||
DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer },
|
||||
DisplayEditableTextbox { text: String, line_width: Option<f64>, font_size: f64, color: Color },
|
||||
DisplayRemoveEditableTextbox,
|
||||
|
||||
// Trigger prefix: cause a browser API to do something
|
||||
TriggerDefaultFontLoad,
|
||||
TriggerFileDownload { document: String, name: String },
|
||||
TriggerFileUpload,
|
||||
TriggerFontLoad { font: String },
|
||||
TriggerFontLoadDefault,
|
||||
TriggerIndexedDbRemoveDocument { document_id: u64 },
|
||||
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy { copy_text: String },
|
||||
TriggerViewportResize,
|
||||
TriggerVisitLink { url: String },
|
||||
|
||||
// Update prefix: give the frontend a new value or state for it to use
|
||||
UpdateActiveDocument { document_id: u64 },
|
||||
UpdateActiveTool { tool_name: String },
|
||||
UpdateCanvasRotation { angle_radians: f64 },
|
||||
UpdateCanvasZoom { factor: f64 },
|
||||
UpdateDialogDetails { layout_target: LayoutTarget, layout: SubLayout },
|
||||
UpdateDocumentArtboards { svg: String },
|
||||
UpdateDocumentArtwork { svg: String },
|
||||
UpdateDocumentBarLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||
|
||||
@@ -185,10 +185,10 @@ impl Default for Mapping {
|
||||
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., 1.) }, key_down=KeyPageUp},
|
||||
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }, key_down=KeyPageDown},
|
||||
// Portfolio actions
|
||||
entry! {action=PortfolioMessage::NewDocument, key_down=KeyN, modifiers=[KeyControl]},
|
||||
entry! {action=DialogMessage::RequestNewDocumentDialog, key_down=KeyN, modifiers=[KeyControl]},
|
||||
entry! {action=PortfolioMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]},
|
||||
entry! {action=PortfolioMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=PortfolioMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
|
||||
entry! {action=DialogMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
|
||||
entry! {action=PortfolioMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
|
||||
entry! {action=PortfolioMessage::Copy { clipboard: Clipboard::Device }, key_down=KeyC, modifiers=[KeyControl]},
|
||||
entry! {action=PortfolioMessage::Cut { clipboard: Clipboard::Device }, key_down=KeyX, modifiers=[KeyControl]},
|
||||
|
||||
@@ -15,6 +15,7 @@ pub enum LayoutMessage {
|
||||
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug, Hash, Eq, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum LayoutTarget {
|
||||
DialogDetails,
|
||||
DocumentBar,
|
||||
PropertiesOptionsPanel,
|
||||
PropertiesSectionsPanel,
|
||||
|
||||
@@ -15,6 +15,10 @@ impl LayoutMessageHandler {
|
||||
fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque<Message>) {
|
||||
let widget_layout = &self.layouts[layout_target as usize];
|
||||
let message = match layout_target {
|
||||
LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails {
|
||||
layout_target,
|
||||
layout: widget_layout.layout.clone(),
|
||||
},
|
||||
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout {
|
||||
layout_target,
|
||||
layout: widget_layout.layout.clone(),
|
||||
@@ -31,6 +35,7 @@ impl LayoutMessageHandler {
|
||||
layout_target,
|
||||
layout: widget_layout.layout.clone(),
|
||||
},
|
||||
|
||||
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
|
||||
};
|
||||
responses.push_back(message.into());
|
||||
@@ -38,8 +43,10 @@ impl LayoutMessageHandler {
|
||||
}
|
||||
|
||||
impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
||||
#[remain::check]
|
||||
fn process_action(&mut self, action: LayoutMessage, _data: (), responses: &mut std::collections::VecDeque<crate::message_prelude::Message>) {
|
||||
use LayoutMessage::*;
|
||||
#[remain::sorted]
|
||||
match action {
|
||||
SendLayout { layout, layout_target } => {
|
||||
self.layouts[layout_target as usize] = layout;
|
||||
@@ -49,52 +56,12 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
||||
UpdateLayout { layout_target, widget_id, value } => {
|
||||
let layout = &mut self.layouts[layout_target as usize];
|
||||
let widget_holder = layout.iter_mut().find(|widget| widget.widget_id == widget_id).expect("Received invalid widget_id from the frontend");
|
||||
#[remain::sorted]
|
||||
match &mut widget_holder.widget {
|
||||
Widget::NumberInput(number_input) => match value {
|
||||
Value::Number(num) => {
|
||||
let update_value = num.as_f64().unwrap();
|
||||
number_input.value = update_value;
|
||||
let callback_message = (number_input.on_update.callback)(number_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Value::String(str) => match str.as_str() {
|
||||
"Increment" => responses.push_back((number_input.increment_callback_increase.callback)(number_input)),
|
||||
"Decrement" => responses.push_back((number_input.increment_callback_decrease.callback)(number_input)),
|
||||
_ => {
|
||||
panic!("Invalid string found when updating `NumberInput`")
|
||||
}
|
||||
},
|
||||
_ => panic!("Invalid type found when updating `NumberInput`"),
|
||||
},
|
||||
Widget::Separator(_) => {}
|
||||
Widget::IconButton(icon_button) => {
|
||||
let callback_message = (icon_button.on_update.callback)(icon_button);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::IconLabel(_) => {}
|
||||
Widget::PopoverButton(_) => {}
|
||||
Widget::OptionalInput(optional_input) => {
|
||||
let update_value = value.as_bool().expect("OptionalInput update was not of type: bool");
|
||||
optional_input.checked = update_value;
|
||||
let callback_message = (optional_input.on_update.callback)(optional_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::RadioInput(radio_input) => {
|
||||
let update_value = value.as_u64().expect("RadioInput update was not of type: u64");
|
||||
radio_input.selected_index = update_value as u32;
|
||||
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextInput(text_input) => {
|
||||
let update_value = value.as_str().expect("TextInput update was not of type: string");
|
||||
text_input.value = update_value.into();
|
||||
let callback_message = (text_input.on_update.callback)(text_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextAreaInput(text_area_input) => {
|
||||
let update_value = value.as_str().expect("TextAreaInput update was not of type: string");
|
||||
text_area_input.value = update_value.into();
|
||||
let callback_message = (text_area_input.on_update.callback)(text_area_input);
|
||||
Widget::CheckboxInput(checkbox_input) => {
|
||||
let update_value = value.as_bool().expect("CheckboxInput update was not of type: bool");
|
||||
checkbox_input.checked = update_value;
|
||||
let callback_message = (checkbox_input.on_update.callback)(checkbox_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::ColorInput(color_input) => {
|
||||
@@ -121,6 +88,57 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
||||
let callback_message = (font_input.on_update.callback)(font_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::IconButton(icon_button) => {
|
||||
let callback_message = (icon_button.on_update.callback)(icon_button);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::IconLabel(_) => {}
|
||||
Widget::NumberInput(number_input) => match value {
|
||||
Value::Number(num) => {
|
||||
let update_value = num.as_f64().unwrap();
|
||||
number_input.value = update_value;
|
||||
let callback_message = (number_input.on_update.callback)(number_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Value::String(str) => match str.as_str() {
|
||||
"Increment" => responses.push_back((number_input.increment_callback_increase.callback)(number_input)),
|
||||
"Decrement" => responses.push_back((number_input.increment_callback_decrease.callback)(number_input)),
|
||||
_ => {
|
||||
panic!("Invalid string found when updating `NumberInput`")
|
||||
}
|
||||
},
|
||||
_ => panic!("Invalid type found when updating `NumberInput`"),
|
||||
},
|
||||
Widget::OptionalInput(optional_input) => {
|
||||
let update_value = value.as_bool().expect("OptionalInput update was not of type: bool");
|
||||
optional_input.checked = update_value;
|
||||
let callback_message = (optional_input.on_update.callback)(optional_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::PopoverButton(_) => {}
|
||||
Widget::RadioInput(radio_input) => {
|
||||
let update_value = value.as_u64().expect("RadioInput update was not of type: u64");
|
||||
radio_input.selected_index = update_value as u32;
|
||||
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::Separator(_) => {}
|
||||
Widget::TextAreaInput(text_area_input) => {
|
||||
let update_value = value.as_str().expect("TextAreaInput update was not of type: string");
|
||||
text_area_input.value = update_value.into();
|
||||
let callback_message = (text_area_input.on_update.callback)(text_area_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextButton(text_button) => {
|
||||
let callback_message = (text_button.on_update.callback)(text_button);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextInput(text_input) => {
|
||||
let update_value = value.as_str().expect("TextInput update was not of type: string");
|
||||
text_input.value = update_value.into();
|
||||
let callback_message = (text_input.on_update.callback)(text_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextLabel(_) => {}
|
||||
};
|
||||
self.send_layout(layout_target, responses);
|
||||
|
||||
@@ -150,6 +150,7 @@ impl<T> Default for WidgetCallback<T> {
|
||||
#[remain::sorted]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Widget {
|
||||
CheckboxInput(CheckboxInput),
|
||||
ColorInput(ColorInput),
|
||||
FontInput(FontInput),
|
||||
IconButton(IconButton),
|
||||
@@ -160,6 +161,7 @@ pub enum Widget {
|
||||
RadioInput(RadioInput),
|
||||
Separator(Separator),
|
||||
TextAreaInput(TextAreaInput),
|
||||
TextButton(TextButton),
|
||||
TextInput(TextInput),
|
||||
TextLabel(TextLabel),
|
||||
}
|
||||
@@ -191,6 +193,7 @@ pub struct NumberInput {
|
||||
#[serde(rename = "displayDecimalPlaces")]
|
||||
#[derivative(Default(value = "3"))]
|
||||
pub display_decimal_places: u32,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
@@ -288,6 +291,20 @@ pub struct IconButton {
|
||||
pub on_update: WidgetCallback<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))]
|
||||
pub struct TextButton {
|
||||
pub label: String,
|
||||
pub emphasized: bool,
|
||||
pub disabled: bool,
|
||||
pub min_width: u32,
|
||||
pub gap_after: bool,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TextButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct OptionalInput {
|
||||
@@ -300,6 +317,20 @@ pub struct OptionalInput {
|
||||
pub on_update: WidgetCallback<OptionalInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct CheckboxInput {
|
||||
pub checked: bool,
|
||||
pub icon: String,
|
||||
#[serde(rename = "outlineStyle")]
|
||||
pub outline_style: bool,
|
||||
#[serde(rename = "title")]
|
||||
pub tooltip: String,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<CheckboxInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct PopoverButton {
|
||||
@@ -342,4 +373,7 @@ pub struct TextLabel {
|
||||
pub value: String,
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub multiline: bool,
|
||||
#[serde(rename = "tableAlign")]
|
||||
pub table_align: bool,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod communication;
|
||||
#[macro_use]
|
||||
pub mod misc;
|
||||
pub mod consts;
|
||||
pub mod dialog;
|
||||
pub mod document;
|
||||
pub mod frontend;
|
||||
pub mod global;
|
||||
@@ -59,6 +60,7 @@ pub mod message_prelude {
|
||||
pub use crate::document::clipboards::Clipboard;
|
||||
pub use crate::LayerId;
|
||||
|
||||
pub use crate::dialog::messages::*;
|
||||
pub use crate::document::{ArtboardMessage, ArtboardMessageDiscriminant};
|
||||
pub use crate::document::{DocumentMessage, DocumentMessageDiscriminant};
|
||||
pub use crate::document::{MovementMessage, MovementMessageDiscriminant};
|
||||
|
||||
Reference in New Issue
Block a user