mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
Welcome screen, refactor to allow zero documents, and add TS typing to widgets (#702)
* unfinished implementation * Add frontend for the empty panel screen * Add an icon for Folder based on NodeFolder * fixed messages causing peicees of ui not to render on new document * Standardize nextTick syntax * WIP generisization of component subscriptions (not compiling yet) * Fix crash when loading font and there is no active document * Only advertise tool actions with a document * Fix failure to create new document * Initalise the properties panel * Fix highlight tab, canvas jump, warns and layer tree * Fix tests * Possibly fix some things? * Move WorkingColors layout definition to backend * Standardize action macro formatting * Provide typing for widgets in TS/Vue and associated cleanup * Fix viewport positioning initialization * Fix menu bar init at startup not document creation * Fix no viewport bounds bug * Change !=0 to >0 * Simplify the init system Closes #656 Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use super::broadcast_message_handler::BroadcastMessageHandler;
|
||||
use crate::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE};
|
||||
use crate::document::PortfolioMessageHandler;
|
||||
use crate::global::GlobalMessageHandler;
|
||||
use crate::input::{InputMapperMessageHandler, InputPreprocessorMessageHandler};
|
||||
@@ -7,6 +8,8 @@ use crate::message_prelude::*;
|
||||
use crate::viewport_tools::tool_message_handler::ToolMessageHandler;
|
||||
use crate::workspace::WorkspaceMessageHandler;
|
||||
|
||||
use graphene::layers::text_layer::Font;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -42,8 +45,6 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerTreeStructure),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateActiveDocument),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateOpenDocumentsList),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerSignal(BroadcastSignalDiscriminant::DocumentIsDirty)),
|
||||
];
|
||||
@@ -87,6 +88,18 @@ impl Dispatcher {
|
||||
match message {
|
||||
#[remain::unsorted]
|
||||
NoOp => {}
|
||||
#[remain::unsorted]
|
||||
Init => {
|
||||
// Display the menu bar at the top of the window
|
||||
let message = MenuBarMessage::SendLayout.into();
|
||||
queue.push_back(message);
|
||||
|
||||
// Load the default font
|
||||
let font = Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into());
|
||||
let message = FrontendMessage::TriggerFontLoad { font, is_default: true }.into();
|
||||
queue.push_back(message);
|
||||
}
|
||||
|
||||
Broadcast(message) => self.message_handlers.broadcast_message_handler.process_action(message, (), &mut queue),
|
||||
Dialog(message) => {
|
||||
self.message_handlers
|
||||
@@ -94,14 +107,16 @@ impl Dispatcher {
|
||||
.process_action(message, &self.message_handlers.portfolio_message_handler, &mut queue);
|
||||
}
|
||||
Frontend(message) => {
|
||||
// Image and font loading should be immediately handled
|
||||
if let FrontendMessage::UpdateImageData { .. } | FrontendMessage::TriggerFontLoad { .. } = message {
|
||||
// Handle these messages immediately by returning early
|
||||
if let FrontendMessage::UpdateImageData { .. } | FrontendMessage::TriggerFontLoad { .. } | FrontendMessage::TriggerRefreshBoundsOfViewports = message {
|
||||
self.responses.push(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed
|
||||
self.responses.push(message);
|
||||
// Return early to avoid running the code after the match block
|
||||
return;
|
||||
} else {
|
||||
// `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed
|
||||
self.responses.push(message);
|
||||
}
|
||||
}
|
||||
Global(message) => {
|
||||
self.message_handlers.global_message_handler.process_action(message, (), &mut queue);
|
||||
@@ -122,15 +137,19 @@ impl Dispatcher {
|
||||
.process_action(message, &self.message_handlers.input_preprocessor_message_handler, &mut queue);
|
||||
}
|
||||
Tool(message) => {
|
||||
self.message_handlers.tool_message_handler.process_action(
|
||||
message,
|
||||
(
|
||||
self.message_handlers.portfolio_message_handler.active_document(),
|
||||
&self.message_handlers.input_preprocessor_message_handler,
|
||||
self.message_handlers.portfolio_message_handler.font_cache(),
|
||||
),
|
||||
&mut queue,
|
||||
);
|
||||
if let Some(document) = self.message_handlers.portfolio_message_handler.active_document() {
|
||||
self.message_handlers.tool_message_handler.process_action(
|
||||
message,
|
||||
(
|
||||
document,
|
||||
&self.message_handlers.input_preprocessor_message_handler,
|
||||
self.message_handlers.portfolio_message_handler.font_cache(),
|
||||
),
|
||||
&mut queue,
|
||||
);
|
||||
} else {
|
||||
log::warn!("Called ToolMessage without an active document.\nGot {:?}", message);
|
||||
}
|
||||
}
|
||||
Workspace(message) => {
|
||||
self.message_handlers
|
||||
@@ -153,7 +172,9 @@ impl Dispatcher {
|
||||
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());
|
||||
list.extend(self.message_handlers.tool_message_handler.actions());
|
||||
if self.message_handlers.portfolio_message_handler.active_document().is_some() {
|
||||
list.extend(self.message_handlers.tool_message_handler.actions());
|
||||
}
|
||||
list.extend(self.message_handlers.portfolio_message_handler.actions());
|
||||
list
|
||||
}
|
||||
@@ -192,6 +213,7 @@ mod test {
|
||||
set_uuid_seed(0);
|
||||
let mut editor = Editor::new();
|
||||
|
||||
editor.new_document();
|
||||
editor.select_primary_color(Color::RED);
|
||||
editor.draw_rect(100., 200., 300., 400.);
|
||||
editor.select_primary_color(Color::BLUE);
|
||||
@@ -211,14 +233,14 @@ mod test {
|
||||
init_logger();
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone();
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
clipboard: Clipboard::Internal,
|
||||
folder_path: vec![],
|
||||
insert_index: -1,
|
||||
});
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
@@ -245,7 +267,7 @@ mod test {
|
||||
init_logger();
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone();
|
||||
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
|
||||
|
||||
editor.handle_message(DocumentMessage::SetSelectedLayers {
|
||||
@@ -258,7 +280,7 @@ mod test {
|
||||
insert_index: -1,
|
||||
});
|
||||
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
@@ -290,7 +312,7 @@ mod test {
|
||||
|
||||
editor.handle_message(DocumentMessage::CreateEmptyFolder { container_path: vec![] });
|
||||
|
||||
let document_before_added_shapes = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||
let document_before_added_shapes = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone();
|
||||
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];
|
||||
|
||||
// TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils.
|
||||
@@ -314,7 +336,7 @@ mod test {
|
||||
replacement_selected_layers: vec![vec![folder_id]],
|
||||
});
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone();
|
||||
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
editor.handle_message(DocumentMessage::DeleteSelectedLayers);
|
||||
@@ -329,7 +351,7 @@ mod test {
|
||||
insert_index: -1,
|
||||
});
|
||||
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
@@ -380,7 +402,7 @@ mod test {
|
||||
const SHAPE_INDEX: usize = 1;
|
||||
const RECT_INDEX: usize = 0;
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone();
|
||||
let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX];
|
||||
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
|
||||
|
||||
@@ -401,7 +423,7 @@ mod test {
|
||||
insert_index: -1,
|
||||
});
|
||||
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().graphene_document.clone();
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().graphene_document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
|
||||
@@ -431,7 +453,7 @@ mod test {
|
||||
fn map_to_vec(paths: Vec<&[LayerId]>) -> Vec<Vec<LayerId>> {
|
||||
paths.iter().map(|layer| layer.to_vec()).collect::<Vec<_>>()
|
||||
}
|
||||
let sorted_layers = map_to_vec(editor.dispatcher.message_handlers.portfolio_message_handler.active_document().all_layers_sorted());
|
||||
let sorted_layers = map_to_vec(editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().all_layers_sorted());
|
||||
println!("Sorted layers: {:?}", sorted_layers);
|
||||
|
||||
let verify_order = |handler: &mut DocumentMessageHandler| {
|
||||
@@ -447,15 +469,15 @@ mod test {
|
||||
});
|
||||
|
||||
editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: 1 });
|
||||
let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut());
|
||||
let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap());
|
||||
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
|
||||
|
||||
editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: -1 });
|
||||
let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut());
|
||||
let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap());
|
||||
assert_eq!(all, selected.into_iter().chain(non_selected.into_iter()).collect::<Vec<_>>());
|
||||
|
||||
editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MAX });
|
||||
let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut());
|
||||
let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap());
|
||||
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ where
|
||||
pub enum Message {
|
||||
#[remain::unsorted]
|
||||
NoOp,
|
||||
#[remain::unsorted]
|
||||
Init,
|
||||
#[child]
|
||||
Broadcast(BroadcastMessage),
|
||||
#[child]
|
||||
|
||||
@@ -18,7 +18,7 @@ pub enum DialogMessage {
|
||||
// Messages
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
CloseDialogAndThen {
|
||||
followup: Box<Message>,
|
||||
followups: Vec<Message>,
|
||||
},
|
||||
DisplayDialogError {
|
||||
title: String,
|
||||
|
||||
@@ -25,9 +25,11 @@ impl MessageHandler<DialogMessage, &PortfolioMessageHandler> for DialogMessageHa
|
||||
dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "Copy".to_string() }.into());
|
||||
}
|
||||
DialogMessage::CloseDialogAndThen { followup } => {
|
||||
DialogMessage::CloseDialogAndThen { followups } => {
|
||||
responses.push_back(FrontendMessage::DisplayDialogDismiss.into());
|
||||
responses.push_back(*followup);
|
||||
for message in followups.into_iter() {
|
||||
responses.push_back(message);
|
||||
}
|
||||
}
|
||||
DialogMessage::DisplayDialogError { title, description } => {
|
||||
let dialog = dialogs::Error { title, description };
|
||||
@@ -54,36 +56,38 @@ impl MessageHandler<DialogMessage, &PortfolioMessageHandler> for DialogMessageHa
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }.into());
|
||||
}
|
||||
DialogMessage::RequestExportDialog => {
|
||||
let artboard_handler = &portfolio.active_document().artboard_message_handler;
|
||||
let mut index = 0;
|
||||
let artboards = artboard_handler
|
||||
.artboard_ids
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|&artboard| artboard_handler.artboards_graphene_document.layer(&[artboard]).ok().map(|layer| (artboard, layer)))
|
||||
.map(|(artboard, layer)| {
|
||||
(
|
||||
artboard,
|
||||
format!(
|
||||
"Artboard: {}",
|
||||
layer.name.clone().unwrap_or_else(|| {
|
||||
index += 1;
|
||||
format!("Untitled {index}")
|
||||
})
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if let Some(document) = portfolio.active_document() {
|
||||
let artboard_handler = &document.artboard_message_handler;
|
||||
let mut index = 0;
|
||||
let artboards = artboard_handler
|
||||
.artboard_ids
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|&artboard| artboard_handler.artboards_graphene_document.layer(&[artboard]).ok().map(|layer| (artboard, layer)))
|
||||
.map(|(artboard, layer)| {
|
||||
(
|
||||
artboard,
|
||||
format!(
|
||||
"Artboard: {}",
|
||||
layer.name.clone().unwrap_or_else(|| {
|
||||
index += 1;
|
||||
format!("Untitled {index}")
|
||||
})
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.export_dialog = Export {
|
||||
file_name: portfolio.active_document().name.clone(),
|
||||
scale_factor: 1.,
|
||||
artboards,
|
||||
has_selection: portfolio.active_document().selected_layers().next().is_some(),
|
||||
..Default::default()
|
||||
};
|
||||
self.export_dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
|
||||
self.export_dialog = Export {
|
||||
file_name: document.name.clone(),
|
||||
scale_factor: 1.,
|
||||
artboards,
|
||||
has_selection: document.selected_layers().next().is_some(),
|
||||
..Default::default()
|
||||
};
|
||||
self.export_dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
|
||||
}
|
||||
}
|
||||
DialogMessage::RequestNewDocumentDialog => {
|
||||
self.new_document_dialog = NewDocument {
|
||||
@@ -97,5 +101,9 @@ impl MessageHandler<DialogMessage, &PortfolioMessageHandler> for DialogMessageHa
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(DialogMessageDiscriminant;RequestNewDocumentDialog,RequestExportDialog,CloseAllDocumentsWithConfirmation);
|
||||
advertise_actions!(DialogMessageDiscriminant;
|
||||
RequestNewDocumentDialog,
|
||||
RequestExportDialog,
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ impl PropertyHolder for CloseAllDocuments {
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(PortfolioMessage::CloseAllDocuments.into()),
|
||||
followups: vec![PortfolioMessage::CloseAllDocuments.into()],
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::layout::widgets::*;
|
||||
use crate::message_prelude::{DialogMessage, DocumentMessage, FrontendMessage, PortfolioMessage};
|
||||
use crate::message_prelude::*;
|
||||
|
||||
/// A dialog for confirming the closing a document with unsaved changes.
|
||||
pub struct CloseDocument {
|
||||
@@ -18,7 +18,7 @@ impl PropertyHolder for CloseDocument {
|
||||
emphasized: true,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(DocumentMessage::SaveDocument.into()),
|
||||
followups: vec![DocumentMessage::SaveDocument.into()],
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
@@ -29,7 +29,7 @@ impl PropertyHolder for CloseDocument {
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(move |_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(PortfolioMessage::CloseDocument { document_id }.into()),
|
||||
followups: vec![BroadcastSignal::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
|
||||
@@ -33,6 +33,7 @@ impl PropertyHolder for Export {
|
||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||
value: self.file_name.clone(),
|
||||
on_update: WidgetCallback::new(|text_input: &TextInput| ExportDialogUpdate::FileName(text_input.value.clone()).into()),
|
||||
..Default::default()
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -123,7 +124,7 @@ impl PropertyHolder for Export {
|
||||
emphasized: true,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(ExportDialogUpdate::Submit.into()),
|
||||
followups: vec![ExportDialogUpdate::Submit.into()],
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
|
||||
@@ -34,6 +34,7 @@ impl PropertyHolder for NewDocument {
|
||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||
value: self.name.clone(),
|
||||
on_update: WidgetCallback::new(|text_input: &TextInput| NewDocumentDialogUpdate::Name(text_input.value.clone()).into()),
|
||||
..Default::default()
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -98,7 +99,7 @@ impl PropertyHolder for NewDocument {
|
||||
emphasized: true,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(NewDocumentDialogUpdate::Submit.into()),
|
||||
followups: vec![NewDocumentDialogUpdate::Submit.into()],
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
@@ -131,9 +132,6 @@ pub enum NewDocumentDialogUpdate {
|
||||
DimensionsY(f64),
|
||||
|
||||
Submit,
|
||||
BufferArtboard,
|
||||
AddArtboard,
|
||||
FitCanvas,
|
||||
}
|
||||
|
||||
impl MessageHandler<NewDocumentDialogUpdate, ()> for NewDocument {
|
||||
@@ -147,27 +145,18 @@ impl MessageHandler<NewDocumentDialogUpdate, ()> for NewDocument {
|
||||
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());
|
||||
if !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0 {
|
||||
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(DocumentMessage::ZoomCanvasToFitAll.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);
|
||||
|
||||
@@ -552,8 +552,9 @@ impl DocumentMessageHandler {
|
||||
on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetSnapping { snap: optional_input.checked }.into()),
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Snapping".into(),
|
||||
header: "Snapping".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
@@ -566,8 +567,9 @@ impl DocumentMessageHandler {
|
||||
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()),
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Grid".into(),
|
||||
header: "Grid".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
@@ -580,8 +582,9 @@ impl DocumentMessageHandler {
|
||||
on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()),
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Overlays".into(),
|
||||
header: "Overlays".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
@@ -614,8 +617,9 @@ impl DocumentMessageHandler {
|
||||
],
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "View Mode".into(),
|
||||
header: "View Mode".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Section,
|
||||
|
||||
@@ -221,6 +221,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
||||
responses.push_back(SetCanvasZoom { zoom_factor: self.zoom }.into());
|
||||
}
|
||||
}
|
||||
|
||||
self.mouse_position = ipp.mouse.position;
|
||||
}
|
||||
RotateCanvasBegin => {
|
||||
|
||||
@@ -45,6 +45,8 @@ impl MessageHandler<OverlaysMessage, (bool, &FontCache, &InputPreprocessorMessag
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(OverlaysMessageDiscriminant; ClearAllOverlays)
|
||||
actions!(OverlaysMessageDiscriminant;
|
||||
ClearAllOverlays,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ pub enum PortfolioMessage {
|
||||
Cut {
|
||||
clipboard: Clipboard,
|
||||
},
|
||||
DestroyAllDocuments,
|
||||
FontLoaded {
|
||||
font_family: String,
|
||||
font_style: String,
|
||||
@@ -48,7 +49,6 @@ pub enum PortfolioMessage {
|
||||
font: Font,
|
||||
is_default: bool,
|
||||
},
|
||||
NewDocument,
|
||||
NewDocumentWithName {
|
||||
name: String,
|
||||
},
|
||||
|
||||
@@ -15,23 +15,23 @@ use graphene::Operation as DocumentOperation;
|
||||
use log::warn;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PortfolioMessageHandler {
|
||||
menu_bar_message_handler: MenuBarMessageHandler,
|
||||
documents: HashMap<u64, DocumentMessageHandler>,
|
||||
document_ids: Vec<u64>,
|
||||
active_document_id: u64,
|
||||
active_document_id: Option<u64>,
|
||||
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
|
||||
font_cache: FontCache,
|
||||
}
|
||||
|
||||
impl PortfolioMessageHandler {
|
||||
pub fn active_document(&self) -> &DocumentMessageHandler {
|
||||
self.documents.get(&self.active_document_id).unwrap()
|
||||
pub fn active_document(&self) -> Option<&DocumentMessageHandler> {
|
||||
self.active_document_id.and_then(|id| self.documents.get(&id))
|
||||
}
|
||||
|
||||
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
|
||||
self.documents.get_mut(&self.active_document_id).unwrap()
|
||||
pub fn active_document_mut(&mut self) -> Option<&mut DocumentMessageHandler> {
|
||||
self.active_document_id.and_then(|id| self.documents.get_mut(&id))
|
||||
}
|
||||
|
||||
pub fn generate_new_document_name(&self) -> String {
|
||||
@@ -56,21 +56,8 @@ impl PortfolioMessageHandler {
|
||||
}
|
||||
|
||||
// TODO Fix how this doesn't preserve tab order upon loading new document from *File > Load*
|
||||
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, replace_first_empty: bool, responses: &mut VecDeque<Message>) {
|
||||
// Special case when loading a document on an empty page
|
||||
if replace_first_empty && self.active_document().is_unmodified_default() {
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(PortfolioMessage::CloseDocument { document_id: self.active_document_id }.into());
|
||||
|
||||
let active_document_index = self
|
||||
.document_ids
|
||||
.iter()
|
||||
.position(|id| self.active_document_id == *id)
|
||||
.expect("Did not find matching active document id");
|
||||
self.document_ids.insert(active_document_index + 1, document_id);
|
||||
} else {
|
||||
self.document_ids.push(document_id);
|
||||
}
|
||||
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, responses: &mut VecDeque<Message>) {
|
||||
self.document_ids.push(document_id);
|
||||
|
||||
responses.extend(
|
||||
new_document
|
||||
@@ -86,9 +73,19 @@ impl PortfolioMessageHandler {
|
||||
|
||||
self.documents.insert(document_id, new_document);
|
||||
|
||||
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
|
||||
if self.active_document().is_some() {
|
||||
responses.push_back(PropertiesPanelMessage::Deactivate.into());
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(ToolMessage::DeactivateTools.into());
|
||||
}
|
||||
|
||||
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
|
||||
responses.push_back(PortfolioMessage::SelectDocument { document_id }.into());
|
||||
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||
responses.push_back(ToolMessage::InitTools.into());
|
||||
responses.push_back(PropertiesPanelMessage::Init.into());
|
||||
responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
|
||||
responses.push_back(DocumentMessage::DocumentStructureChanged.into())
|
||||
}
|
||||
|
||||
/// Returns an iterator over the open documents in order.
|
||||
@@ -105,25 +102,6 @@ impl PortfolioMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PortfolioMessageHandler {
|
||||
fn default() -> Self {
|
||||
let mut documents_map: HashMap<u64, DocumentMessageHandler> = HashMap::with_capacity(1);
|
||||
let starting_key = generate_uuid();
|
||||
documents_map.insert(starting_key, DocumentMessageHandler::default());
|
||||
|
||||
const EMPTY_VEC: Vec<CopyBufferEntry> = vec![];
|
||||
|
||||
Self {
|
||||
documents: documents_map,
|
||||
document_ids: vec![starting_key],
|
||||
copy_buffer: [EMPTY_VEC; INTERNAL_CLIPBOARD_COUNT as usize],
|
||||
active_document_id: starting_key,
|
||||
font_cache: Default::default(),
|
||||
menu_bar_message_handler: MenuBarMessageHandler::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for PortfolioMessageHandler {
|
||||
#[remain::check]
|
||||
fn process_action(&mut self, message: PortfolioMessage, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
@@ -134,12 +112,20 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
match message {
|
||||
// Sub-messages
|
||||
#[remain::unsorted]
|
||||
Document(message) => self.documents.get_mut(&self.active_document_id).unwrap().process_action(message, (ipp, &self.font_cache), responses),
|
||||
Document(message) => {
|
||||
if let Some(document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) {
|
||||
document.process_action(message, (ipp, &self.font_cache), responses)
|
||||
}
|
||||
}
|
||||
#[remain::unsorted]
|
||||
MenuBar(message) => self.menu_bar_message_handler.process_action(message, (), responses),
|
||||
|
||||
// Messages
|
||||
AutoSaveActiveDocument => responses.push_back(PortfolioMessage::AutoSaveDocument { document_id: self.active_document_id }.into()),
|
||||
AutoSaveActiveDocument => {
|
||||
if let Some(document_id) = self.active_document_id {
|
||||
responses.push_back(PortfolioMessage::AutoSaveDocument { document_id }.into());
|
||||
}
|
||||
}
|
||||
AutoSaveDocument { document_id } => {
|
||||
let document = self.documents.get(&document_id).unwrap();
|
||||
responses.push_back(
|
||||
@@ -156,54 +142,60 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
)
|
||||
}
|
||||
CloseActiveDocumentWithConfirmation => {
|
||||
responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation { document_id: self.active_document_id }.into());
|
||||
if let Some(document_id) = self.active_document_id {
|
||||
responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation { document_id }.into());
|
||||
}
|
||||
}
|
||||
CloseAllDocuments => {
|
||||
// Empty the list of internal document data
|
||||
self.documents.clear();
|
||||
self.document_ids.clear();
|
||||
if self.active_document_id.is_some() {
|
||||
responses.push_back(PropertiesPanelMessage::Deactivate.into());
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(ToolMessage::DeactivateTools.into());
|
||||
}
|
||||
|
||||
// Clear out all documents and make a new default document
|
||||
let new_document_id = generate_uuid();
|
||||
self.documents.insert(new_document_id, DocumentMessageHandler::default());
|
||||
self.document_ids.push(new_document_id);
|
||||
self.active_document_id = new_document_id;
|
||||
for document_id in &self.document_ids {
|
||||
responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id: *document_id }.into());
|
||||
}
|
||||
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(PortfolioMessage::DestroyAllDocuments.into());
|
||||
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
|
||||
responses.push_back(PortfolioMessage::SelectDocument { document_id: new_document_id }.into())
|
||||
}
|
||||
CloseDocument { document_id } => {
|
||||
let document_index = self.document_index(document_id);
|
||||
self.documents.remove(&document_id);
|
||||
self.document_ids.remove(document_index);
|
||||
|
||||
// Last tab was closed, so create a new blank tab
|
||||
if self.document_ids.is_empty() {
|
||||
let new_id = generate_uuid();
|
||||
self.document_ids.push(new_id);
|
||||
self.documents.insert(new_id, DocumentMessageHandler::default());
|
||||
self.active_document_id = None;
|
||||
} else if Some(document_id) == self.active_document_id {
|
||||
if document_index == self.document_ids.len() {
|
||||
// If we closed the last document take the one previous (same as last)
|
||||
responses.push_back(
|
||||
PortfolioMessage::SelectDocument {
|
||||
document_id: *self.document_ids.last().unwrap(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
// Move to the next tab
|
||||
responses.push_back(
|
||||
PortfolioMessage::SelectDocument {
|
||||
document_id: self.document_ids[document_index],
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.active_document_id = if document_id != self.active_document_id {
|
||||
// If we are not closing the active document, stay on it
|
||||
self.active_document_id
|
||||
} else if document_index >= self.document_ids.len() {
|
||||
// If we closed the last document take the one previous (same as last)
|
||||
*self.document_ids.last().unwrap()
|
||||
} else {
|
||||
// Move to the next tab
|
||||
self.document_ids[document_index]
|
||||
};
|
||||
|
||||
// Send the new list of document tab names
|
||||
responses.push_back(UpdateOpenDocumentsList.into());
|
||||
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id: self.active_document_id }.into());
|
||||
responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id }.into());
|
||||
responses.push_back(RenderDocument.into());
|
||||
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
||||
for layer in self.active_document().layer_metadata.keys() {
|
||||
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
|
||||
if let Some(document) = self.active_document() {
|
||||
for layer in document.layer_metadata.keys() {
|
||||
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
CloseDocumentWithConfirmation { document_id } => {
|
||||
@@ -225,36 +217,42 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
}
|
||||
Copy { clipboard } => {
|
||||
// We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self`
|
||||
let active_document = self.documents.get(&self.active_document_id).unwrap();
|
||||
|
||||
let copy_val = |buffer: &mut Vec<CopyBufferEntry>| {
|
||||
for layer_path in active_document.selected_layers_without_children() {
|
||||
match (active_document.graphene_document.layer(layer_path).map(|t| t.clone()), *active_document.layer_metadata(layer_path)) {
|
||||
(Ok(layer), layer_metadata) => {
|
||||
buffer.push(CopyBufferEntry { layer, layer_metadata });
|
||||
if let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get(&id)) {
|
||||
let copy_val = |buffer: &mut Vec<CopyBufferEntry>| {
|
||||
for layer_path in active_document.selected_layers_without_children() {
|
||||
match (active_document.graphene_document.layer(layer_path).map(|t| t.clone()), *active_document.layer_metadata(layer_path)) {
|
||||
(Ok(layer), layer_metadata) => {
|
||||
buffer.push(CopyBufferEntry { layer, layer_metadata });
|
||||
}
|
||||
(Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e),
|
||||
}
|
||||
(Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e),
|
||||
}
|
||||
};
|
||||
|
||||
if clipboard == Clipboard::Device {
|
||||
let mut buffer = Vec::new();
|
||||
copy_val(&mut buffer);
|
||||
let mut copy_text = String::from("graphite/layer: ");
|
||||
copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste");
|
||||
|
||||
responses.push_back(FrontendMessage::TriggerTextCopy { copy_text }.into());
|
||||
} else {
|
||||
let copy_buffer = &mut self.copy_buffer;
|
||||
copy_buffer[clipboard as usize].clear();
|
||||
copy_val(&mut copy_buffer[clipboard as usize]);
|
||||
}
|
||||
};
|
||||
|
||||
if clipboard == Clipboard::Device {
|
||||
let mut buffer = Vec::new();
|
||||
copy_val(&mut buffer);
|
||||
let mut copy_text = String::from("graphite/layer: ");
|
||||
copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste");
|
||||
|
||||
responses.push_back(FrontendMessage::TriggerTextCopy { copy_text }.into());
|
||||
} else {
|
||||
let copy_buffer = &mut self.copy_buffer;
|
||||
copy_buffer[clipboard as usize].clear();
|
||||
copy_val(&mut copy_buffer[clipboard as usize]);
|
||||
}
|
||||
}
|
||||
Cut { clipboard } => {
|
||||
responses.push_back(Copy { clipboard }.into());
|
||||
responses.push_back(DeleteSelectedLayers.into());
|
||||
}
|
||||
DestroyAllDocuments => {
|
||||
// Empty the list of internal document data
|
||||
self.documents.clear();
|
||||
self.document_ids.clear();
|
||||
self.active_document_id = None;
|
||||
}
|
||||
FontLoaded {
|
||||
font_family,
|
||||
font_style,
|
||||
@@ -263,33 +261,35 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
is_default,
|
||||
} => {
|
||||
self.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default);
|
||||
self.active_document_mut().graphene_document.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text);
|
||||
responses.push_back(DocumentMessage::RenderDocument.into());
|
||||
|
||||
if let Some(document) = self.active_document_mut() {
|
||||
document.graphene_document.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text);
|
||||
responses.push_back(DocumentMessage::RenderDocument.into());
|
||||
}
|
||||
}
|
||||
LoadFont { font, is_default } => {
|
||||
if !self.font_cache.loaded_font(&font) {
|
||||
responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default }.into());
|
||||
}
|
||||
}
|
||||
NewDocument => {
|
||||
let name = self.generate_new_document_name();
|
||||
let new_document = DocumentMessageHandler::with_name(name, ipp);
|
||||
let document_id = generate_uuid();
|
||||
responses.push_back(BroadcastSignal::ToolAbort.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(BroadcastSignal::ToolAbort.into());
|
||||
self.load_document(new_document, document_id, false, responses);
|
||||
if self.active_document().is_some() {
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
|
||||
}
|
||||
|
||||
self.load_document(new_document, document_id, responses);
|
||||
}
|
||||
NextDocument => {
|
||||
let current_index = self.document_index(self.active_document_id);
|
||||
let next_index = (current_index + 1) % self.document_ids.len();
|
||||
let next_id = self.document_ids[next_index];
|
||||
if let Some(active_document_id) = self.active_document_id {
|
||||
let current_index = self.document_index(active_document_id);
|
||||
let next_index = (current_index + 1) % self.document_ids.len();
|
||||
let next_id = self.document_ids[next_index];
|
||||
|
||||
responses.push_back(PortfolioMessage::SelectDocument { document_id: next_id }.into());
|
||||
responses.push_back(PortfolioMessage::SelectDocument { document_id: next_id }.into());
|
||||
}
|
||||
}
|
||||
OpenDocument => {
|
||||
responses.push_back(FrontendMessage::TriggerFileUpload.into());
|
||||
@@ -318,7 +318,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
match document {
|
||||
Ok(mut document) => {
|
||||
document.set_save_state(document_is_saved);
|
||||
self.load_document(document, document_id, true, responses);
|
||||
self.load_document(document, document_id, responses);
|
||||
}
|
||||
Err(e) => responses.push_back(
|
||||
DialogMessage::DisplayDialogError {
|
||||
@@ -330,22 +330,26 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
}
|
||||
}
|
||||
Paste { clipboard } => {
|
||||
let document = self.active_document();
|
||||
let shallowest_common_folder = document
|
||||
.graphene_document
|
||||
.shallowest_common_folder(document.selected_layers())
|
||||
.expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion");
|
||||
responses.push_back(DeselectAllLayers.into());
|
||||
responses.push_back(StartTransaction.into());
|
||||
responses.push_back(
|
||||
PasteIntoFolder {
|
||||
clipboard,
|
||||
folder_path: shallowest_common_folder.to_vec(),
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(CommitTransaction.into());
|
||||
let shallowest_common_folder = self.active_document().map(|document| {
|
||||
document
|
||||
.graphene_document
|
||||
.shallowest_common_folder(document.selected_layers())
|
||||
.expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion")
|
||||
});
|
||||
|
||||
if let Some(folder) = shallowest_common_folder {
|
||||
responses.push_back(DeselectAllLayers.into());
|
||||
responses.push_back(StartTransaction.into());
|
||||
responses.push_back(
|
||||
PasteIntoFolder {
|
||||
clipboard,
|
||||
folder_path: folder.to_vec(),
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(CommitTransaction.into());
|
||||
}
|
||||
}
|
||||
PasteIntoFolder {
|
||||
clipboard,
|
||||
@@ -353,26 +357,27 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
insert_index,
|
||||
} => {
|
||||
let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>| {
|
||||
log::trace!("Pasting into folder {:?} as index: {}", &path, insert_index);
|
||||
if let Some(document) = self.active_document() {
|
||||
log::trace!("Pasting into folder {:?} as index: {}", &path, insert_index);
|
||||
let destination_path = [path.to_vec(), vec![generate_uuid()]].concat();
|
||||
|
||||
let destination_path = [path.to_vec(), vec![generate_uuid()]].concat();
|
||||
|
||||
responses.push_front(
|
||||
DocumentMessage::UpdateLayerMetadata {
|
||||
layer_path: destination_path.clone(),
|
||||
layer_metadata: entry.layer_metadata,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
self.active_document().load_layer_resources(responses, &entry.layer.data, destination_path.clone());
|
||||
responses.push_front(
|
||||
DocumentOperation::InsertLayer {
|
||||
layer: entry.layer.clone(),
|
||||
destination_path,
|
||||
insert_index,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_front(
|
||||
DocumentMessage::UpdateLayerMetadata {
|
||||
layer_path: destination_path.clone(),
|
||||
layer_metadata: entry.layer_metadata,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
document.load_layer_resources(responses, &entry.layer.data, destination_path.clone());
|
||||
responses.push_front(
|
||||
DocumentOperation::InsertLayer {
|
||||
layer: entry.layer.clone(),
|
||||
destination_path,
|
||||
insert_index,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if insert_index == -1 {
|
||||
@@ -386,55 +391,69 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
}
|
||||
}
|
||||
PasteSerializedData { data } => {
|
||||
if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
|
||||
let document = self.active_document();
|
||||
let shallowest_common_folder = document
|
||||
.graphene_document
|
||||
.shallowest_common_folder(document.selected_layers())
|
||||
.expect("While pasting from serialized, the selected layers did not exist while attempting to find the appropriate folder path for insertion");
|
||||
responses.push_back(DeselectAllLayers.into());
|
||||
responses.push_back(StartTransaction.into());
|
||||
if let Some(document) = self.active_document() {
|
||||
if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
|
||||
let shallowest_common_folder = document
|
||||
.graphene_document
|
||||
.shallowest_common_folder(document.selected_layers())
|
||||
.expect("While pasting from serialized, the selected layers did not exist while attempting to find the appropriate folder path for insertion");
|
||||
responses.push_back(DeselectAllLayers.into());
|
||||
responses.push_back(StartTransaction.into());
|
||||
|
||||
for entry in data.iter().rev() {
|
||||
let destination_path = [shallowest_common_folder.to_vec(), vec![generate_uuid()]].concat();
|
||||
for entry in data.iter().rev() {
|
||||
let destination_path = [shallowest_common_folder.to_vec(), vec![generate_uuid()]].concat();
|
||||
|
||||
responses.push_front(
|
||||
DocumentMessage::UpdateLayerMetadata {
|
||||
layer_path: destination_path.clone(),
|
||||
layer_metadata: entry.layer_metadata,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
self.active_document().load_layer_resources(responses, &entry.layer.data, destination_path.clone());
|
||||
responses.push_front(
|
||||
DocumentOperation::InsertLayer {
|
||||
layer: entry.layer.clone(),
|
||||
destination_path,
|
||||
insert_index: -1,
|
||||
responses.push_front(
|
||||
DocumentMessage::UpdateLayerMetadata {
|
||||
layer_path: destination_path.clone(),
|
||||
layer_metadata: entry.layer_metadata,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
document.load_layer_resources(responses, &entry.layer.data, destination_path.clone());
|
||||
responses.push_front(
|
||||
DocumentOperation::InsertLayer {
|
||||
layer: entry.layer.clone(),
|
||||
destination_path,
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
responses.push_back(CommitTransaction.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
PrevDocument => {
|
||||
if let Some(active_document_id) = self.active_document_id {
|
||||
let len = self.document_ids.len();
|
||||
let current_index = self.document_index(active_document_id);
|
||||
let prev_index = (current_index + len - 1) % len;
|
||||
let prev_id = self.document_ids[prev_index];
|
||||
responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into());
|
||||
}
|
||||
}
|
||||
SelectDocument { document_id } => {
|
||||
if let Some(document) = self.active_document() {
|
||||
if !document.is_saved() {
|
||||
// Safe to unwrap since we know that there is an active document
|
||||
responses.push_back(
|
||||
PortfolioMessage::AutoSaveDocument {
|
||||
document_id: self.active_document_id.unwrap(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
responses.push_back(CommitTransaction.into());
|
||||
}
|
||||
}
|
||||
PrevDocument => {
|
||||
let len = self.document_ids.len();
|
||||
let current_index = self.document_index(self.active_document_id);
|
||||
let prev_index = (current_index + len - 1) % len;
|
||||
let prev_id = self.document_ids[prev_index];
|
||||
responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.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());
|
||||
if self.active_document().is_some() {
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
}
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
|
||||
// TODO: Remove this message in favor of having tools have specific data per document instance
|
||||
responses.push_back(SetActiveDocument { document_id }.into());
|
||||
|
||||
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
|
||||
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into());
|
||||
responses.push_back(RenderDocument.into());
|
||||
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
||||
@@ -444,13 +463,13 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
responses.push_back(BroadcastSignal::SelectionChanged.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||
responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
|
||||
}
|
||||
SetActiveDocument { document_id } => {
|
||||
self.active_document_id = document_id;
|
||||
}
|
||||
SetActiveDocument { document_id } => self.active_document_id = Some(document_id),
|
||||
UpdateDocumentWidgets => {
|
||||
let active_document = self.active_document();
|
||||
active_document.update_document_widgets(responses);
|
||||
if let Some(document) = self.active_document() {
|
||||
document.update_document_widgets(responses);
|
||||
}
|
||||
}
|
||||
UpdateOpenDocumentsList => {
|
||||
// Send the list of document tab names
|
||||
@@ -472,7 +491,6 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
let mut common = actions!(PortfolioMessageDiscriminant;
|
||||
NewDocument,
|
||||
CloseActiveDocumentWithConfirmation,
|
||||
CloseAllDocuments,
|
||||
NextDocument,
|
||||
@@ -481,14 +499,17 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
Paste,
|
||||
);
|
||||
|
||||
if self.active_document().layer_metadata.values().any(|data| data.selected) {
|
||||
let select = actions!(PortfolioMessageDiscriminant;
|
||||
Copy,
|
||||
Cut,
|
||||
);
|
||||
common.extend(select);
|
||||
if let Some(document) = self.active_document() {
|
||||
if document.layer_metadata.values().any(|data| data.selected) {
|
||||
let select = actions!(PortfolioMessageDiscriminant;
|
||||
Copy,
|
||||
Cut,
|
||||
);
|
||||
common.extend(select);
|
||||
}
|
||||
common.extend(document.actions());
|
||||
}
|
||||
common.extend(self.active_document().actions());
|
||||
|
||||
common
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub enum PropertiesPanelMessage {
|
||||
CheckSelectedWasDeleted { path: Vec<LayerId> },
|
||||
CheckSelectedWasUpdated { path: Vec<LayerId> },
|
||||
ClearSelection,
|
||||
Deactivate,
|
||||
Init,
|
||||
ModifyFill { fill: Fill },
|
||||
ModifyFont { font_family: String, font_style: String, size: f64 },
|
||||
|
||||
@@ -2,8 +2,8 @@ use super::utility_types::TargetDocument;
|
||||
use crate::document::properties_panel_message::TransformOp;
|
||||
use crate::layout::layout_message::LayoutTarget;
|
||||
use crate::layout::widgets::{
|
||||
ColorInput, FontInput, IconLabel, Layout, LayoutGroup, NumberInput, PopoverButton, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, TextAreaInput, TextInput, TextLabel,
|
||||
Widget, WidgetCallback, WidgetHolder, WidgetLayout,
|
||||
ColorInput, FontInput, IconLabel, IconStyle, Layout, LayoutGroup, NumberInput, PopoverButton, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, TextAreaInput, TextInput,
|
||||
TextLabel, Widget, WidgetCallback, WidgetHolder, WidgetLayout,
|
||||
};
|
||||
use crate::message_prelude::*;
|
||||
|
||||
@@ -156,6 +156,13 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
||||
);
|
||||
self.active_selection = None;
|
||||
}
|
||||
Deactivate => responses.push_back(
|
||||
BroadcastMessage::UnsubscribeSignal {
|
||||
on: BroadcastSignal::SelectionChanged,
|
||||
message: Box::new(PropertiesPanelMessage::UpdateSelectedDocumentProperties.into()),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
Init => responses.push_back(
|
||||
BroadcastMessage::SubscribeSignal {
|
||||
on: BroadcastSignal::SelectionChanged,
|
||||
@@ -262,7 +269,7 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeArtboard".into(),
|
||||
gap_after: true,
|
||||
icon_style: IconStyle::Node,
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
@@ -279,14 +286,16 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||
value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()),
|
||||
on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Options Bar".into(),
|
||||
header: "Options Bar".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
}];
|
||||
@@ -414,7 +423,8 @@ fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Me
|
||||
PropertiesPanelMessage::ModifyFill { fill: Fill::None }.into()
|
||||
}
|
||||
}),
|
||||
can_set_transparent: false,
|
||||
no_transparency: true,
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
@@ -444,19 +454,19 @@ fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Mes
|
||||
match &layer.data {
|
||||
LayerDataType::Folder(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeFolder".into(),
|
||||
gap_after: true,
|
||||
icon_style: IconStyle::Node,
|
||||
})),
|
||||
LayerDataType::Shape(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeShape".into(),
|
||||
gap_after: true,
|
||||
icon_style: IconStyle::Node,
|
||||
})),
|
||||
LayerDataType::Text(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeText".into(),
|
||||
gap_after: true,
|
||||
icon_style: IconStyle::Node,
|
||||
})),
|
||||
LayerDataType::Image(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeImage".into(),
|
||||
gap_after: true,
|
||||
icon_style: IconStyle::Node,
|
||||
})),
|
||||
},
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
@@ -474,14 +484,16 @@ fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Mes
|
||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||
value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()),
|
||||
on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Options Bar".into(),
|
||||
header: "Options Bar".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
}];
|
||||
@@ -704,6 +716,7 @@ fn node_section_font(layer: &TextLayer) -> LayoutGroup {
|
||||
WidgetHolder::new(Widget::TextAreaInput(TextAreaInput {
|
||||
value: layer.text.clone(),
|
||||
on_update: WidgetCallback::new(|text_area: &TextAreaInput| PropertiesPanelMessage::ModifyText { new_text: text_area.value.clone() }.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
@@ -729,6 +742,7 @@ fn node_section_font(layer: &TextLayer) -> LayoutGroup {
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
@@ -754,6 +768,7 @@ fn node_section_font(layer: &TextLayer) -> LayoutGroup {
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
@@ -997,6 +1012,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutGroup {
|
||||
.with_dash_lengths(&text_input.value)
|
||||
.map_or(PropertiesPanelMessage::ResendActiveProperties.into(), |stroke| PropertiesPanelMessage::ModifyStroke { stroke }.into())
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ pub enum FrontendMessage {
|
||||
// Display prefix: make the frontend show something, like a dialog
|
||||
DisplayDialog { icon: String },
|
||||
DisplayDialogDismiss,
|
||||
DisplayDialogPanic { panic_info: String, title: String, description: String },
|
||||
DisplayDialogPanic { panic_info: String, header: String, description: String },
|
||||
DisplayEditableTextbox { text: String, line_width: Option<f64>, font_size: f64, color: Color },
|
||||
DisplayRemoveEditableTextbox,
|
||||
|
||||
@@ -30,6 +30,7 @@ pub enum FrontendMessage {
|
||||
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
|
||||
TriggerPaste,
|
||||
TriggerRasterDownload { document: String, name: String, mime: String, size: (f64, f64) },
|
||||
TriggerRefreshBoundsOfViewports,
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy { copy_text: String },
|
||||
TriggerViewportResize,
|
||||
@@ -58,5 +59,5 @@ pub enum FrontendMessage {
|
||||
UpdatePropertyPanelSectionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||
UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||
UpdateToolShelfLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||
UpdateWorkingColors { primary: Color, secondary: Color },
|
||||
UpdateWorkingColorsLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||
}
|
||||
|
||||
@@ -27,5 +27,9 @@ impl MessageHandler<GlobalMessage, ()> for GlobalMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(GlobalMessageDiscriminant; LogInfo, LogDebug, LogTrace);
|
||||
advertise_actions!(GlobalMessageDiscriminant;
|
||||
LogInfo,
|
||||
LogDebug,
|
||||
LogTrace,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -266,14 +266,12 @@ impl Default for Mapping {
|
||||
|
||||
impl Mapping {
|
||||
pub fn match_message(&self, message: InputMapperMessage, keys: &KeyStates, actions: ActionList) -> Option<Message> {
|
||||
use InputMapperMessage::*;
|
||||
|
||||
let list = match message {
|
||||
KeyDown(key) => &self.key_down[key as usize],
|
||||
KeyUp(key) => &self.key_up[key as usize],
|
||||
DoubleClick => &self.double_click,
|
||||
MouseScroll => &self.mouse_scroll,
|
||||
PointerMove => &self.pointer_move,
|
||||
InputMapperMessage::KeyDown(key) => &self.key_down[key as usize],
|
||||
InputMapperMessage::KeyUp(key) => &self.key_up[key as usize],
|
||||
InputMapperMessage::DoubleClick => &self.double_click,
|
||||
InputMapperMessage::MouseScroll => &self.mouse_scroll,
|
||||
InputMapperMessage::PointerMove => &self.pointer_move,
|
||||
};
|
||||
list.match_mapping(keys, actions)
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ impl InputPreprocessorMessageHandler {
|
||||
}
|
||||
|
||||
pub fn document_bounds(&self) -> [DVec2; 2] {
|
||||
// ipp bounds are relative to the entire screen
|
||||
// IPP bounds are relative to the entire application
|
||||
[(0., 0.).into(), self.viewport_bounds.bottom_right - self.viewport_bounds.top_left]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ pub enum LayoutTarget {
|
||||
PropertiesSections,
|
||||
ToolOptions,
|
||||
ToolShelf,
|
||||
WorkingColors,
|
||||
|
||||
// KEEP THIS ENUM LAST
|
||||
// This is a marker that is used to define an array that is used to hold widgets
|
||||
|
||||
@@ -55,6 +55,10 @@ impl LayoutMessageHandler {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout().layout,
|
||||
},
|
||||
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout().layout,
|
||||
},
|
||||
|
||||
#[remain::unsorted]
|
||||
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
|
||||
@@ -168,6 +172,7 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::Separator(_) => {}
|
||||
Widget::SwatchPairInput(_) => {}
|
||||
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();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::layout_message::LayoutTarget;
|
||||
use crate::input::keyboard::Key;
|
||||
use crate::message_prelude::*;
|
||||
use crate::Color;
|
||||
|
||||
use derivative::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -195,18 +196,18 @@ pub type SubLayout = Vec<LayoutGroup>;
|
||||
#[remain::sorted]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum LayoutGroup {
|
||||
#[serde(rename = "column")]
|
||||
Column {
|
||||
#[serde(rename = "columnWidgets")]
|
||||
widgets: Vec<WidgetHolder>,
|
||||
},
|
||||
#[serde(rename = "row")]
|
||||
Row {
|
||||
#[serde(rename = "rowWidgets")]
|
||||
widgets: Vec<WidgetHolder>,
|
||||
},
|
||||
Section {
|
||||
name: String,
|
||||
layout: SubLayout,
|
||||
},
|
||||
#[serde(rename = "section")]
|
||||
Section { name: String, layout: SubLayout },
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -281,6 +282,7 @@ impl<'a> Iterator for WidgetIterMut<'a> {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct WidgetHolder {
|
||||
#[serde(rename = "widgetId")]
|
||||
pub widget_id: u64,
|
||||
pub widget: Widget,
|
||||
}
|
||||
@@ -323,97 +325,265 @@ pub enum Widget {
|
||||
PopoverButton(PopoverButton),
|
||||
RadioInput(RadioInput),
|
||||
Separator(Separator),
|
||||
SwatchPairInput(SwatchPairInput),
|
||||
TextAreaInput(TextAreaInput),
|
||||
TextButton(TextButton),
|
||||
TextInput(TextInput),
|
||||
TextLabel(TextLabel),
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct NumberInput {
|
||||
pub value: Option<f64>,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<NumberInput>,
|
||||
pub min: Option<f64>,
|
||||
pub max: Option<f64>,
|
||||
#[serde(rename = "isInteger")]
|
||||
pub is_integer: bool,
|
||||
#[serde(rename = "incrementBehavior")]
|
||||
pub increment_behavior: NumberInputIncrementBehavior,
|
||||
#[serde(rename = "incrementFactor")]
|
||||
#[derivative(Default(value = "1."))]
|
||||
pub increment_factor: f64,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub increment_callback_increase: WidgetCallback<NumberInput>,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub increment_callback_decrease: WidgetCallback<NumberInput>,
|
||||
pub label: String,
|
||||
pub unit: String,
|
||||
#[serde(rename = "displayDecimalPlaces")]
|
||||
#[derivative(Default(value = "3"))]
|
||||
pub display_decimal_places: u32,
|
||||
pub disabled: bool,
|
||||
}
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct CheckboxInput {
|
||||
pub checked: bool,
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct TextInput {
|
||||
pub value: String,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TextInput>,
|
||||
}
|
||||
pub icon: String,
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct TextAreaInput {
|
||||
pub value: String,
|
||||
pub tooltip: String,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TextAreaInput>,
|
||||
pub on_update: WidgetCallback<CheckboxInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct ColorInput {
|
||||
pub value: Option<String>,
|
||||
|
||||
pub label: Option<String>,
|
||||
|
||||
#[serde(rename = "noTransparency")]
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub no_transparency: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<ColorInput>,
|
||||
#[serde(rename = "canSetTransparent")]
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct DropdownInput {
|
||||
pub entries: DropdownInputEntries,
|
||||
|
||||
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
|
||||
#[serde(rename = "selectedIndex")]
|
||||
pub selected_index: Option<u32>,
|
||||
|
||||
#[serde(rename = "drawIcon")]
|
||||
pub draw_icon: bool,
|
||||
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub can_set_transparent: bool,
|
||||
pub interactive: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
//
|
||||
// Callbacks
|
||||
// `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput`
|
||||
}
|
||||
|
||||
pub type DropdownInputEntries = Vec<Vec<DropdownEntryData>>;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct DropdownEntryData {
|
||||
pub value: String,
|
||||
|
||||
pub label: String,
|
||||
|
||||
pub icon: String,
|
||||
|
||||
pub shortcut: Vec<String>,
|
||||
|
||||
#[serde(rename = "shortcutRequiresLock")]
|
||||
pub shortcut_requires_lock: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
pub children: DropdownInputEntries,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct FontInput {
|
||||
#[serde(rename = "isStyle")]
|
||||
pub is_style_picker: bool,
|
||||
#[serde(rename = "fontFamily")]
|
||||
pub font_family: String,
|
||||
|
||||
#[serde(rename = "fontStyle")]
|
||||
pub font_style: String,
|
||||
|
||||
#[serde(rename = "isStyle")]
|
||||
pub is_style_picker: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<FontInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct IconButton {
|
||||
pub icon: String,
|
||||
|
||||
pub size: u32, // TODO: Convert to an `IconSize` enum
|
||||
|
||||
pub active: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)]
|
||||
pub struct IconLabel {
|
||||
pub icon: String,
|
||||
|
||||
#[serde(rename = "iconStyle")]
|
||||
pub icon_style: IconStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)]
|
||||
pub enum IconStyle {
|
||||
#[default]
|
||||
Normal,
|
||||
Node,
|
||||
}
|
||||
|
||||
/// This widget allows for the flexible use of the layout system.
|
||||
/// In a custom layout, one can define a widget that is just used to trigger code on the backend.
|
||||
/// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend.
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct Invisible {
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct NumberInput {
|
||||
pub label: String,
|
||||
|
||||
pub value: Option<f64>,
|
||||
|
||||
pub min: Option<f64>,
|
||||
|
||||
pub max: Option<f64>,
|
||||
|
||||
#[serde(rename = "isInteger")]
|
||||
pub is_integer: bool,
|
||||
|
||||
#[serde(rename = "displayDecimalPlaces")]
|
||||
#[derivative(Default(value = "3"))]
|
||||
pub display_decimal_places: u32,
|
||||
|
||||
pub unit: String,
|
||||
|
||||
#[serde(rename = "unitIsHiddenWhenEditing")]
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub unit_is_hidden_when_editing: bool,
|
||||
|
||||
#[serde(rename = "incrementBehavior")]
|
||||
pub increment_behavior: NumberInputIncrementBehavior,
|
||||
|
||||
#[serde(rename = "incrementFactor")]
|
||||
#[derivative(Default(value = "1."))]
|
||||
pub increment_factor: f64,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<NumberInput>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub increment_callback_increase: WidgetCallback<NumberInput>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub increment_callback_decrease: WidgetCallback<NumberInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
|
||||
pub enum NumberInputIncrementBehavior {
|
||||
#[default]
|
||||
Add,
|
||||
Multiply,
|
||||
Callback,
|
||||
}
|
||||
|
||||
impl Default for NumberInputIncrementBehavior {
|
||||
fn default() -> Self {
|
||||
Self::Add
|
||||
}
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct OptionalInput {
|
||||
pub checked: bool,
|
||||
|
||||
pub icon: String,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<OptionalInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct PopoverButton {
|
||||
pub icon: Option<String>,
|
||||
|
||||
// Body
|
||||
pub header: String,
|
||||
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct RadioInput {
|
||||
pub entries: Vec<RadioEntryData>,
|
||||
|
||||
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
|
||||
#[serde(rename = "selectedIndex")]
|
||||
pub selected_index: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct RadioEntryData {
|
||||
pub value: String,
|
||||
|
||||
pub label: String,
|
||||
|
||||
pub icon: String,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -438,19 +608,27 @@ pub enum SeparatorType {
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct IconButton {
|
||||
pub icon: String,
|
||||
#[serde(rename = "title")]
|
||||
pub tooltip: String,
|
||||
pub size: u32,
|
||||
pub active: bool,
|
||||
#[serde(rename = "gapAfter")]
|
||||
pub gap_after: bool,
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct SwatchPairInput {
|
||||
pub primary: Color,
|
||||
|
||||
pub secondary: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct TextAreaInput {
|
||||
pub value: String,
|
||||
|
||||
pub label: Option<String>,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<IconButton>,
|
||||
pub on_update: WidgetCallback<TextAreaInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
@@ -458,125 +636,46 @@ pub struct IconButton {
|
||||
#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))]
|
||||
pub struct TextButton {
|
||||
pub label: String,
|
||||
|
||||
pub emphasized: bool,
|
||||
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
pub gap_after: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TextButton>,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct OptionalInput {
|
||||
pub checked: bool,
|
||||
pub icon: String,
|
||||
#[serde(rename = "title")]
|
||||
pub tooltip: String,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
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 = "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 {
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct DropdownInput {
|
||||
pub entries: Vec<Vec<DropdownEntryData>>,
|
||||
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace with usize when we switch to a native UI)
|
||||
#[serde(rename = "selectedIndex")]
|
||||
pub selected_index: Option<u32>,
|
||||
#[serde(rename = "drawIcon")]
|
||||
pub draw_icon: bool,
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub interactive: bool,
|
||||
// `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput`
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct DropdownEntryData {
|
||||
pub struct TextInput {
|
||||
pub value: String,
|
||||
pub label: String,
|
||||
pub icon: String,
|
||||
pub shortcut: Vec<String>,
|
||||
#[serde(rename = "shortcutRequiresLock")]
|
||||
pub shortcut_requires_lock: bool,
|
||||
pub disabled: bool,
|
||||
pub children: Vec<Vec<DropdownEntryData>>,
|
||||
|
||||
pub label: Option<String>,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct RadioInput {
|
||||
pub entries: Vec<RadioEntryData>,
|
||||
|
||||
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number
|
||||
// TODO(mfish33): Replace with usize when using native UI
|
||||
#[serde(rename = "selectedIndex")]
|
||||
pub selected_index: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct RadioEntryData {
|
||||
pub value: String,
|
||||
pub label: String,
|
||||
pub icon: String,
|
||||
pub tooltip: String,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq)]
|
||||
pub struct IconLabel {
|
||||
pub icon: String,
|
||||
#[serde(rename = "gapAfter")]
|
||||
pub gap_after: bool,
|
||||
pub on_update: WidgetCallback<TextInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq, Default)]
|
||||
pub struct TextLabel {
|
||||
pub value: String,
|
||||
pub bold: bool,
|
||||
|
||||
pub italic: bool,
|
||||
pub multiline: bool,
|
||||
|
||||
#[serde(rename = "tableAlign")]
|
||||
pub table_align: bool,
|
||||
}
|
||||
|
||||
// This widget allows for the flexible use of the layout system
|
||||
// In a custom layout one can define a widget that is just used to trigger code on the backend
|
||||
// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct Invisible {
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<()>,
|
||||
pub multiline: bool,
|
||||
|
||||
// Body
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
///
|
||||
/// and
|
||||
/// ```ignore
|
||||
/// actions!(DocumentMessage; Undo, Redo);
|
||||
/// actions!(DocumentMessage;
|
||||
/// Undo,
|
||||
/// Redo,
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// expands to:
|
||||
|
||||
@@ -8,6 +8,8 @@ use graphene::color::Color;
|
||||
|
||||
/// A set of utility functions to make the writing of editor test more declarative
|
||||
pub trait EditorTestUtils {
|
||||
fn new_document(&mut self);
|
||||
|
||||
fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64);
|
||||
fn draw_shape(&mut self, x1: f64, y1: f64, x2: f64, y2: f64);
|
||||
fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64);
|
||||
@@ -24,6 +26,10 @@ pub trait EditorTestUtils {
|
||||
}
|
||||
|
||||
impl EditorTestUtils for Editor {
|
||||
fn new_document(&mut self) {
|
||||
self.handle_message(Message::Portfolio(PortfolioMessage::NewDocumentWithName { name: String::from("Test document") }));
|
||||
}
|
||||
|
||||
fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
self.drag_tool(ToolType::Rectangle, x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ pub struct SignalToMessageMap {
|
||||
|
||||
pub trait ToolTransition {
|
||||
fn signal_to_message_map(&self) -> SignalToMessageMap;
|
||||
|
||||
fn activate(&self, responses: &mut VecDeque<Message>) {
|
||||
let mut subscribe_message = |broadcast_to_tool_mapping: Option<ToolMessage>, signal: BroadcastSignal| {
|
||||
if let Some(mapping) = broadcast_to_tool_mapping {
|
||||
@@ -78,6 +79,7 @@ pub trait ToolTransition {
|
||||
unsubscribe_message(signal_to_tool_map.selection_changed, BroadcastSignal::SelectionChanged);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToolMetadata {
|
||||
fn icon_name(&self) -> String;
|
||||
fn tooltip(&self) -> String;
|
||||
|
||||
@@ -75,10 +75,10 @@ pub enum ToolMessage {
|
||||
// Detail(DetailToolMessage),
|
||||
|
||||
// Messages
|
||||
#[remain::unsorted]
|
||||
ActivateTool {
|
||||
tool_type: ToolType,
|
||||
},
|
||||
DeactivateTools,
|
||||
InitTools,
|
||||
ResetColors,
|
||||
SelectPrimaryColor {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use super::tool::{message_to_tool_type, DocumentToolData, ToolFsmState};
|
||||
use super::tool::{message_to_tool_type, ToolFsmState};
|
||||
use crate::document::DocumentMessageHandler;
|
||||
use crate::input::InputPreprocessorMessageHandler;
|
||||
use crate::layout::layout_message::LayoutTarget;
|
||||
use crate::layout::widgets::PropertyHolder;
|
||||
use crate::layout::widgets::{IconButton, Layout, LayoutGroup, SwatchPairInput, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::message_prelude::*;
|
||||
use crate::viewport_tools::tool::DocumentToolData;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
@@ -73,12 +75,16 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
|
||||
// Notify the frontend about the new active tool to be displayed
|
||||
tool_data.register_properties(responses, LayoutTarget::ToolShelf);
|
||||
}
|
||||
DeactivateTools => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
tool_data.tools.get(&tool_data.active_tool_type).unwrap().deactivate(responses);
|
||||
}
|
||||
InitTools => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
let document_data = &self.tool_state.document_tool_data;
|
||||
let active_tool = &tool_data.active_tool_type;
|
||||
|
||||
// subscribe tool to broadcast messages
|
||||
// Subscribe tool to broadcast messages
|
||||
tool_data.tools.get(active_tool).unwrap().activate(responses);
|
||||
|
||||
// Register initial properties
|
||||
@@ -87,6 +93,10 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
|
||||
// Notify the frontend about the initial active tool
|
||||
tool_data.register_properties(responses, LayoutTarget::ToolShelf);
|
||||
|
||||
// Notify the frontend about the initial working colors
|
||||
update_working_colors(document_data, responses);
|
||||
responses.push_back(FrontendMessage::TriggerRefreshBoundsOfViewports.into());
|
||||
|
||||
// Set initial hints and cursor
|
||||
tool_data
|
||||
.active_tool_mut()
|
||||
@@ -156,7 +166,12 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
let mut list = actions!(ToolMessageDiscriminant; SelectRandomPrimaryColor, ResetColors, SwapColors, ActivateTool);
|
||||
let mut list = actions!(ToolMessageDiscriminant;
|
||||
ActivateTool,
|
||||
SelectRandomPrimaryColor,
|
||||
ResetColors,
|
||||
SwapColors,
|
||||
);
|
||||
list.extend(self.tool_state.tool_data.active_tool().actions());
|
||||
|
||||
list
|
||||
@@ -164,10 +179,37 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
|
||||
}
|
||||
|
||||
fn update_working_colors(document_data: &DocumentToolData, responses: &mut VecDeque<Message>) {
|
||||
let layout = WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::SwatchPairInput(SwatchPairInput {
|
||||
primary: document_data.primary_color,
|
||||
secondary: document_data.secondary_color,
|
||||
}))],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
size: 16,
|
||||
icon: "Swap".into(),
|
||||
tooltip: "Swap (Shift+X)".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut
|
||||
on_update: WidgetCallback::new(|_| ToolMessage::SwapColors.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
size: 16,
|
||||
icon: "ResetColors".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut
|
||||
tooltip: "Reset (Ctrl+Shift+X)".into(),
|
||||
on_update: WidgetCallback::new(|_| ToolMessage::ResetColors.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
responses.push_back(
|
||||
FrontendMessage::UpdateWorkingColors {
|
||||
primary: document_data.primary_color,
|
||||
secondary: document_data.secondary_color,
|
||||
LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(layout),
|
||||
layout_target: LayoutTarget::WorkingColors,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
@@ -73,7 +73,13 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for ArtboardTool
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(ArtboardToolMessageDiscriminant; PointerDown, PointerUp, PointerMove, DeleteSelected, Abort);
|
||||
advertise_actions!(ArtboardToolMessageDiscriminant;
|
||||
PointerDown,
|
||||
PointerUp,
|
||||
PointerMove,
|
||||
DeleteSelected,
|
||||
Abort,
|
||||
);
|
||||
}
|
||||
|
||||
impl PropertyHolder for ArtboardTool {}
|
||||
|
||||
@@ -75,8 +75,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for EllipseTool
|
||||
use EllipseToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(EllipseToolMessageDiscriminant; DragStart),
|
||||
Drawing => actions!(EllipseToolMessageDiscriminant; DragStop, Abort, Resize),
|
||||
Ready => actions!(EllipseToolMessageDiscriminant;
|
||||
DragStart,
|
||||
),
|
||||
Drawing => actions!(EllipseToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,10 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for EyedropperTo
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(EyedropperToolMessageDiscriminant; LeftMouseDown, RightMouseDown);
|
||||
advertise_actions!(EyedropperToolMessageDiscriminant;
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
);
|
||||
}
|
||||
|
||||
impl ToolTransition for EyedropperTool {
|
||||
|
||||
@@ -67,7 +67,10 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for FillTool {
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(FillToolMessageDiscriminant; LeftMouseDown, RightMouseDown);
|
||||
advertise_actions!(FillToolMessageDiscriminant;
|
||||
LeftMouseDown,
|
||||
RightMouseDown,
|
||||
);
|
||||
}
|
||||
|
||||
impl ToolTransition for FillTool {
|
||||
|
||||
@@ -115,8 +115,16 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for FreehandTool
|
||||
use FreehandToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(FreehandToolMessageDiscriminant; DragStart, DragStop, Abort),
|
||||
Drawing => actions!(FreehandToolMessageDiscriminant; DragStop, PointerMove, Abort),
|
||||
Ready => actions!(FreehandToolMessageDiscriminant;
|
||||
DragStart,
|
||||
DragStop,
|
||||
Abort,
|
||||
),
|
||||
Drawing => actions!(FreehandToolMessageDiscriminant;
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Abort,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,12 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for GradientTool
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(GradientToolMessageDiscriminant; PointerDown, PointerUp, PointerMove, Abort);
|
||||
advertise_actions!(GradientToolMessageDiscriminant;
|
||||
PointerDown,
|
||||
PointerUp,
|
||||
PointerMove,
|
||||
Abort,
|
||||
);
|
||||
}
|
||||
|
||||
impl PropertyHolder for GradientTool {
|
||||
|
||||
@@ -116,8 +116,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for LineTool {
|
||||
use LineToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(LineToolMessageDiscriminant; DragStart),
|
||||
Drawing => actions!(LineToolMessageDiscriminant; DragStop, Redraw, Abort),
|
||||
Ready => actions!(LineToolMessageDiscriminant;
|
||||
DragStart,
|
||||
),
|
||||
Drawing => actions!(LineToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Redraw,
|
||||
Abort,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,16 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for NavigateTool
|
||||
use NavigateToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(NavigateToolMessageDiscriminant; TranslateCanvasBegin, RotateCanvasBegin, ZoomCanvasBegin),
|
||||
_ => actions!(NavigateToolMessageDiscriminant; ClickZoom, PointerMove, TransformCanvasEnd),
|
||||
Ready => actions!(NavigateToolMessageDiscriminant;
|
||||
TranslateCanvasBegin,
|
||||
RotateCanvasBegin,
|
||||
ZoomCanvasBegin,
|
||||
),
|
||||
_ => actions!(NavigateToolMessageDiscriminant;
|
||||
ClickZoom,
|
||||
PointerMove,
|
||||
TransformCanvasEnd,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,15 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PathTool {
|
||||
use PathToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(PathToolMessageDiscriminant; DragStart, Delete),
|
||||
Dragging => actions!(PathToolMessageDiscriminant; DragStop, PointerMove, Delete),
|
||||
Ready => actions!(PathToolMessageDiscriminant;
|
||||
DragStart,
|
||||
Delete,
|
||||
),
|
||||
Dragging => actions!(PathToolMessageDiscriminant;
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Delete,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,8 +131,20 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PenTool {
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
PenToolFsmState::Ready => actions!(PenToolMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
|
||||
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; DragStart, DragStop, PointerMove, Confirm, Abort),
|
||||
PenToolFsmState::Ready => actions!(PenToolMessageDiscriminant;
|
||||
Undo,
|
||||
DragStart,
|
||||
DragStop,
|
||||
Confirm,
|
||||
Abort,
|
||||
),
|
||||
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Confirm,
|
||||
Abort,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for RectangleToo
|
||||
use RectangleToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(RectangleToolMessageDiscriminant; DragStart),
|
||||
Drawing => actions!(RectangleToolMessageDiscriminant; DragStop, Abort, Resize),
|
||||
Ready => actions!(RectangleToolMessageDiscriminant;
|
||||
DragStart,
|
||||
),
|
||||
Drawing => actions!(RectangleToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +161,9 @@ impl PropertyHolder for SelectTool {
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Align".into(),
|
||||
header: "Align".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
@@ -187,8 +188,9 @@ impl PropertyHolder for SelectTool {
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Flip".into(),
|
||||
header: "Flip".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
@@ -234,8 +236,9 @@ impl PropertyHolder for SelectTool {
|
||||
separator_type: SeparatorType::Related,
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Boolean".into(),
|
||||
header: "Boolean".into(),
|
||||
text: "The contents of this popover menu are coming soon".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
}]))
|
||||
@@ -266,8 +269,18 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for SelectTool {
|
||||
use SelectToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(SelectToolMessageDiscriminant; DragStart, PointerMove, Abort, EditLayer),
|
||||
_ => actions!(SelectToolMessageDiscriminant; DragStop, PointerMove, Abort, EditLayer),
|
||||
Ready => actions!(SelectToolMessageDiscriminant;
|
||||
DragStart,
|
||||
PointerMove,
|
||||
Abort,
|
||||
EditLayer,
|
||||
),
|
||||
_ => actions!(SelectToolMessageDiscriminant;
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Abort,
|
||||
EditLayer,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +114,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for ShapeTool {
|
||||
use ShapeToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(ShapeToolMessageDiscriminant; DragStart),
|
||||
Drawing => actions!(ShapeToolMessageDiscriminant; DragStop, Abort, Resize),
|
||||
Ready => actions!(ShapeToolMessageDiscriminant;
|
||||
DragStart,
|
||||
),
|
||||
Drawing => actions!(ShapeToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,19 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for SplineTool {
|
||||
use SplineToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(SplineToolMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
|
||||
Drawing => actions!(SplineToolMessageDiscriminant; DragStop, PointerMove, Confirm, Abort),
|
||||
Ready => actions!(SplineToolMessageDiscriminant;
|
||||
Undo,
|
||||
DragStart,
|
||||
DragStop,
|
||||
Confirm,
|
||||
Abort,
|
||||
),
|
||||
Drawing => actions!(SplineToolMessageDiscriminant;
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Confirm,
|
||||
Abort,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ impl PropertyHolder for TextTool {
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
@@ -112,6 +113,7 @@ impl PropertyHolder for TextTool {
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
@@ -169,8 +171,14 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for TextTool {
|
||||
use TextToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(TextMessageDiscriminant; Interact),
|
||||
Editing => actions!(TextMessageDiscriminant; Interact, Abort, CommitText),
|
||||
Ready => actions!(TextMessageDiscriminant;
|
||||
Interact,
|
||||
),
|
||||
Editing => actions!(TextMessageDiscriminant;
|
||||
Interact,
|
||||
Abort,
|
||||
CommitText,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
frontend/assets/graphics/graphite-logotype-solid.svg
Normal file
11
frontend/assets/graphics/graphite-logotype-solid.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="937" height="240" viewBox="0 0 937 240" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<path fill="#ffffff" d="M934.29,139.3c-3.08,2.94-6.82,5.09-10.91,6.27c-3.49,1.06-7.1,1.63-10.74,1.71c-6.08,0.08-11.98-2.06-16.6-6.02c-4.78-4.01-7.49-10.63-8.14-19.86l48.01-6.02c0-8.68-2.58-15.71-7.73-21.08c-5.16-5.37-12.72-8.06-22.7-8.06c-7.19-0.04-14.29,1.57-20.75,4.72c-6.37,3.07-11.75,7.86-15.54,13.83c-3.91,6.08-5.86,13.46-5.86,22.14c0,8.03,1.76,14.98,5.29,20.83c3.41,5.76,8.38,10.44,14.32,13.51c6.21,3.19,13.11,4.81,20.1,4.72c9.01,0,16.14-2.2,21.41-6.59c5.51-4.74,9.78-10.74,12.45-17.5L934.29,139.3z M891.64,99.01c2.28-3.85,5.26-5.78,8.95-5.78c3.79,0,6.48,1.84,8.06,5.53c1.68,4.2,2.59,8.66,2.69,13.18l-23.6,2.93C888.06,108.15,889.37,102.86,891.64,99.01" />
|
||||
<path fill="#ffffff" d="M844.61,151.33c-7.06,0-10.58-4.34-10.58-13.02v-34.5c0-4.34,2.17-6.51,6.51-6.51h14.65v-8.62h-21.16c0-4.12,0.05-8.19,0.16-12.21c0.11-4.01,0.59-11.63,0.91-15.76l-25.49,11.81v16.16h-9.77v8.62h9.77v44.27c0,7.16,2.01,13.02,6.02,17.58c4.01,4.56,9.87,6.83,17.58,6.84c4.07,0.13,8.11-0.71,11.8-2.44c3.03-1.49,5.72-3.6,7.89-6.18c1.98-2.37,3.62-5,4.88-7.81l-2.6-2.6C852.42,149.81,848.59,151.4,844.61,151.33" />
|
||||
<path fill="#ffffff" d="M783.25,154.67c-0.64-2.97-0.91-6-0.81-9.03v-38.9c0-5.21,0.08-9.52,0.24-12.94s0.3-5.94,0.41-7.57l-0.98-0.98l-35.48,16.44l1.63,3.74c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.31,2.85-0.32c0.97-0.07,1.92,0.22,2.69,0.81c0.59,0.54,0.89,1.63,0.9,3.26v37.43c0.08,3.03-0.14,6.05-0.65,9.03c-0.44,2.01-1.2,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.39,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.39-6.02-1.14C784.64,157.85,783.56,156.38,783.25,154.67 M771.04,77.28c3.74,0.07,7.35-1.44,9.93-4.15c2.64-2.59,4.11-6.15,4.07-9.85c0.03-3.72-1.44-7.3-4.07-9.93c-2.56-2.75-6.17-4.29-9.93-4.23c-3.81-0.09-7.48,1.45-10.09,4.23c-2.64,2.63-4.1,6.21-4.07,9.93c0.02,7.75,6.32,14.02,14.07,14C770.98,77.29,771.01,77.29,771.04,77.28" />
|
||||
<path fill="#ffffff" d="M732.15,154.68c-0.64-2.97-0.91-6-0.81-9.03v-39.22c0-7.05-1.57-12.18-4.72-15.38c-3.15-3.2-8.08-4.8-14.81-4.8c-4.06,0.01-8.07,0.84-11.8,2.44c-3.08,1.21-6.03,2.75-8.79,4.57c-3.07,2.01-5.99,4.25-8.71,6.72V61.55c0-5.21,0.08-9.52,0.24-12.94c0.16-3.42,0.3-5.94,0.41-7.57L682.11,40l-35.45,16.42l1.66,3.82c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.32,2.85-0.33c0.96-0.07,1.92,0.22,2.68,0.81c0.6,0.55,0.9,1.63,0.9,3.26v82.63c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99c-0.64-2.97-0.91-6-0.82-9.03v-37.92c2.72-1.87,5.71-3.29,8.87-4.23c2.26-0.61,4.58-0.94,6.92-0.98c3.79,0,6.18,1,7.16,3.01c1.06,2.43,1.56,5.08,1.46,7.73v32.39c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99" />
|
||||
<path fill="#ffffff" d="M624.97,90.71c-4.3-2.92-9.37-4.48-14.57-4.48c-5.74-0.16-11.38,1.43-16.19,4.56c-4.26,2.76-7.67,6.65-9.85,11.23h-0.32c0-3.26,0.12-6.35,0.39-9.49c0.14-2.07,0.38-4.14,0.73-6.18l-0.98-0.98l-33.84,15.68l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.81c0.6,0.54,0.9,1.63,0.9,3.25v73.9c0.08,3.02-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.72-2.86,1.11-4.39,1.14V200h43.12v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.96-2.18-3.33-3.99c-0.64-2.97-0.91-6-0.81-9.03v-16.76c1.52,0.22,3.17,0.38,4.96,0.49s3.77,0.16,5.94,0.16c5.18-0.03,10.33-0.8,15.3-2.28c5.21-1.52,10.1-4.01,14.4-7.32c4.5-3.5,8.15-7.98,10.66-13.1c2.71-5.37,4.07-11.96,4.07-19.78c0-7.81-1.36-14.49-4.07-20.02C633.4,98.33,629.66,93.92,624.97,90.71 M608.94,150.61c-3.26,5.04-7.27,7.57-12.04,7.57c-5.21,0-9.33-2.39-12.37-7.16v-43.3c1.7-1.75,3.75-3.11,6.02-3.99c2.03-0.79,4.18-1.2,6.35-1.22c4.77,0,8.79,2.31,12.04,6.92c3.26,4.61,4.88,11.64,4.88,21.08C613.82,138.86,612.19,145.57,608.94,150.61" />
|
||||
<path fill="#ffffff" d="M541.31,150.61c-1.17,0.45-2.41,0.7-3.66,0.73c-1.95,0-3.25-0.68-3.91-2.03c-0.74-1.83-1.07-3.81-0.98-5.78v-35.48c0-12.25-7.16-19.5-19.95-21.8c-8.97-1.62-19.39-1.04-28.28,0.57c-5.06,0.92-10.37,2.79-13.57,5.49v23.95h3.71c0.91-5.48,3.36-10.58,7.07-14.72c3.2-3.81,7.96-5.97,12.94-5.86c3.8,0,6.75,1.11,8.87,3.34c2.12,2.23,3.17,5.89,3.17,10.99v8.63c-13.78,3.69-23.95,7.76-30.52,12.21s-9.85,10.25-9.85,17.42c-0.06,4.5,1.47,8.88,4.31,12.36c2.87,3.58,7.29,5.37,13.27,5.37c4.5-0.01,8.92-1.16,12.86-3.34c4.18-2.27,7.62-5.69,9.93-9.85h0.33c0.95,3.66,3.1,6.9,6.1,9.2c2.87,2.12,6.97,3.17,12.29,3.17c4.71,0.08,9.34-1.19,13.35-3.66c4.15-2.73,7.43-6.6,9.44-11.15l-2.6-2.6C544.39,148.99,542.93,149.96,541.31,150.61 M506.73,146.3c-1.27,1.36-2.72,2.54-4.31,3.5c-1.74,1.05-3.75,1.58-5.78,1.54c-2.11,0.12-4.16-0.75-5.53-2.36c-1.32-1.63-2.02-3.68-1.95-5.78c0.09-1.95,0.5-3.88,1.22-5.7c1.09-2.66,2.82-5.01,5.05-6.84c2.55-2.28,6.32-4.12,11.31-5.53L506.73,146.3z" />
|
||||
<path fill="#ffffff" d="M440.68,91.63c-4.8,1.93-9.07,4.75-11.91,9.87h-0.33c-0.02-2.98,0.11-5.96,0.41-8.92c0.13-2.13,0.37-4.25,0.73-6.35l-0.98-0.98l-33.85,15.79l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.82c0.59,0.54,0.89,1.63,0.9,3.25v37.44c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h43.13v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.97-2.18-3.34-3.99c-0.64-2.97-0.91-6-0.82-9.03v-36.29c2.1-1.79,4.53-3.15,7.16-3.99c2.49-0.72,5.06-1.08,7.65-1.06c2.42,0.01,4.78,0.68,6.84,1.95c2.17,1.3,3.71,5.12,4.48,10h4.1V92.3C455.3,89.03,446.61,89.25,440.68,91.63" />
|
||||
<path fill="#ffffff" d="M344.13,115.53c2.68,0.05,5.32,0.57,7.81,1.55c1.73,0.81,2.9,2.6,3.5,5.37c0.72,4.38,1.02,8.82,0.9,13.26c0,3.8-0.04,6.29-0.2,9.22c-0.16,2.93-0.39,4.51-1.58,6.47c-1.63,2.71-4.43,4-7.41,4.59c-2.7,0.57-5.46,0.87-8.22,0.9c-6.29,0-12.7-1.98-16.81-5.14c-5.27-4.05-9.38-11.35-12.04-19.92c-2.4-8.27-3.58-16.84-3.51-25.45c0-14.54,4.01-24.17,9.38-31.43c5.46-7.37,14.61-11.25,25-11.89c4.13-0.21,8.27,0.21,12.28,1.25c3.63,1.12,7.4,2.65,10.43,6.07c3.03,3.42,4.67,7.11,6.85,13.4h3.74v-24.9c-4.86-1.84-9.87-3.25-14.97-4.23c-5.73-1.18-11.56-1.78-17.41-1.79c-8.11-0.06-16.17,1.23-23.85,3.82c-7.23,2.44-13.91,6.25-19.69,11.23c-5.77,5.04-10.36,11.29-13.43,18.31c-3.38,7.91-5.05,16.46-4.88,25.07c0,10.96,2.39,20.57,7.16,28.81c4.6,8.07,11.36,14.7,19.53,19.12c8.5,4.57,18.02,6.89,27.67,6.76c7.53,0.11,15.02-0.97,22.22-3.18c5.71-1.74,11.2-4.14,16.36-7.16c3.26-1.87,6.32-4.08,9.11-6.59c-0.63-2.67-1.01-5.4-1.14-8.14c-0.11-2.61-0.16-5.37-0.16-8.3v-9.44c0-2.82,0.3-4.77,0.9-5.86c0.66-1.12,1.87-1.81,3.17-1.79v-3.74h-40.7V115.53z" />
|
||||
<path fill="#ffffff" d="M231.18,218.98l-0.07-0.69c-0.86-9.39-11.15-121.38-11.18-121.86c-0.23-2.84-1.07-5.6-2.45-8.09c-0.03-0.09-0.07-0.17-0.11-0.25l-0.06-0.15l-0.03,0.03l-0.02-0.01l0.04-0.02L205.5,67.5L172.31,10c-3.58-6.19-10.18-10-17.33-10H64.99c-7.14,0-13.74,3.81-17.32,10l-45,77.93c-3.57,6.19-3.57,13.81,0,20l45,77.93c3.57,6.19,10.17,10,17.32,10h89.99c3.86-0.03,7.63-1.19,10.85-3.32l38.59,27.68c-6.97-2.18-14.18-3.47-21.47-3.83c-18.11-0.87-71.2-0.28-131.42,4.63c-24.71,2.01-36.39,7.88-35.03,9.03c3.49,2.98,7.62,4.16,28.2,4.08c18.32-0.06,71.65,1.91,87.76,2.9c11.41,0.71,23.41,2.88,32.04,2.97c9.2-0.12,18.37-0.82,27.48-2.1c13.74-1.89,31.96-5.7,36.15-10.77C230.34,225.03,231.47,222.02,231.18,218.98z M62.49,24.32c1.67-2.55,4.45-4.16,7.5-4.33h79.99c3.04,0.17,5.81,1.77,7.49,4.31l33.26,57.61c-4.99,5.2-9.32,11-12.89,17.26l-24.77,2.75L138.3,122c-7.21-0.04-14.4,0.82-21.4,2.54L60.77,27.31L62.49,24.32z M69.99,175.86c-3.05-0.17-5.83-1.78-7.5-4.33l-40-69.27c-1.37-2.72-1.37-5.94,0-8.66l26.73-46.28l59.6,103.24l0.04-0.02c0.69,1.24,1.64,2.31,2.79,3.15l30.93,22.18L69.99,175.86z M186.75,182.93l-57.9-41.53c6.39-1.39,12.91-2.09,19.45-2.09l14.77-20.07l24.77-2.75c3.26-5.66,7.13-10.95,11.52-15.79l7.03,70.9C198.07,170.71,190.13,175.29,186.75,182.93z M81.64,154.71c1.49,2.33,0.8,5.42-1.52,6.91c-2.33,1.49-5.42,0.8-6.91-1.52c-0.08-0.12-0.15-0.25-0.22-0.38l-35-60.61c-1.49-2.33-0.8-5.42,1.52-6.91c2.33-1.49,5.42-0.8,6.91,1.52c0.08,0.12,0.15,0.25,0.22,0.38L81.64,154.71z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
4
frontend/assets/icon-16px-solid/folder.svg
Normal file
4
frontend/assets/icon-16px-solid/folder.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14,13H2c-0.55,0-1-0.45-1-1V6h5.42l1-1H15v7C15,12.55,14.55,13,14,13z" />
|
||||
<path d="M6,2H2C1.45,2,1,2.45,1,3v2h5l1-1h8c0-0.55-0.45-1-1-1H7L6,2z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
@@ -213,6 +213,7 @@ img {
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { createBlobManager } from "@/io-managers/blob";
|
||||
import { createClipboardManager } from "@/io-managers/clipboard";
|
||||
import { createHyperlinkManager } from "@/io-managers/hyperlinks";
|
||||
import { createInputManager } from "@/io-managers/input";
|
||||
@@ -222,6 +223,7 @@ import { createPersistenceManager } from "@/io-managers/persistence";
|
||||
import { createDialogState, DialogState } from "@/state-providers/dialog";
|
||||
import { createFontsState, FontsState } from "@/state-providers/fonts";
|
||||
import { createFullscreenState, FullscreenState } from "@/state-providers/fullscreen";
|
||||
import { createPanelsState, PanelsState } from "@/state-providers/panels";
|
||||
import { createPortfolioState, PortfolioState } from "@/state-providers/portfolio";
|
||||
import { createWorkspaceState, WorkspaceState } from "@/state-providers/workspace";
|
||||
import { createEditor, Editor } from "@/wasm-communication/editor";
|
||||
@@ -229,6 +231,7 @@ import { createEditor, Editor } from "@/wasm-communication/editor";
|
||||
import MainWindow from "@/components/window/MainWindow.vue";
|
||||
|
||||
const managerDestructors: {
|
||||
createBlobManager?: () => void;
|
||||
createClipboardManager?: () => void;
|
||||
createHyperlinkManager?: () => void;
|
||||
createInputManager?: () => void;
|
||||
@@ -248,6 +251,7 @@ declare module "@vue/runtime-core" {
|
||||
dialog: DialogState;
|
||||
fonts: FontsState;
|
||||
fullscreen: FullscreenState;
|
||||
panels: PanelsState;
|
||||
portfolio: PortfolioState;
|
||||
workspace: WorkspaceState;
|
||||
}
|
||||
@@ -266,7 +270,8 @@ export default defineComponent({
|
||||
// State provider systems
|
||||
dialog: createDialogState(editor),
|
||||
fonts: createFontsState(editor),
|
||||
fullscreen: createFullscreenState(),
|
||||
fullscreen: createFullscreenState(editor),
|
||||
panels: createPanelsState(editor),
|
||||
portfolio: createPortfolioState(editor),
|
||||
workspace: createWorkspaceState(editor),
|
||||
};
|
||||
@@ -274,6 +279,7 @@ export default defineComponent({
|
||||
async mounted() {
|
||||
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
|
||||
Object.assign(managerDestructors, {
|
||||
createBlobManager: createBlobManager(this.editor),
|
||||
createClipboardManager: createClipboardManager(this.editor),
|
||||
createHyperlinkManager: createHyperlinkManager(this.editor),
|
||||
createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen),
|
||||
@@ -283,7 +289,7 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
|
||||
this.editor.instance.init_app();
|
||||
this.editor.instance.init_after_frontend_ready();
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Call the destructor for each manager
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
import { defineComponent, nextTick, PropType } from "vue";
|
||||
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
|
||||
@@ -319,7 +319,7 @@ export default defineComponent({
|
||||
// To be called by the parent component. Measures the actual width of the floating menu content element and returns it in a promise.
|
||||
async measureAndEmitNaturalWidth(): Promise<void> {
|
||||
// Wait for the changed content which fired the `updated()` Vue event to be put into the DOM
|
||||
await this.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
// Wait until all fonts have been loaded and rendered so measurements of content involving text are accurate
|
||||
// API is experimental but supported in all browsers - https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready
|
||||
@@ -329,7 +329,8 @@ export default defineComponent({
|
||||
// Make the component show itself with 0 min-width so it can be measured, and wait until the values have been updated to the DOM
|
||||
this.measuringOngoing = true;
|
||||
this.measuringOngoingGuard = true;
|
||||
await this.$nextTick();
|
||||
|
||||
await nextTick();
|
||||
|
||||
// Only measure if the menu is visible, perhaps because a parent component with a `v-if` condition is false
|
||||
let naturalWidth;
|
||||
@@ -341,7 +342,7 @@ export default defineComponent({
|
||||
|
||||
// Turn off measuring mode for the component, which triggers another call to the `updated()` Vue event, so we can turn off the protection after that has happened
|
||||
this.measuringOngoing = false;
|
||||
await this.$nextTick();
|
||||
await nextTick();
|
||||
this.measuringOngoingGuard = false;
|
||||
|
||||
// Emit the measured natural width to the parent
|
||||
@@ -424,7 +425,7 @@ export default defineComponent({
|
||||
},
|
||||
watch: {
|
||||
// Called only when `open` is changed from outside this component (with v-model)
|
||||
open(newState: boolean, oldState: boolean) {
|
||||
async open(newState: boolean, oldState: boolean) {
|
||||
// Switching from closed to open
|
||||
if (newState && !oldState) {
|
||||
// Close floating menu if pointer strays far enough away
|
||||
@@ -435,15 +436,17 @@ export default defineComponent({
|
||||
window.addEventListener("pointerdown", this.pointerDownHandler);
|
||||
// Cancel the subsequent click event to prevent the floating menu from reopening if the floating menu's button is the click event target
|
||||
window.addEventListener("pointerup", this.pointerUpHandler);
|
||||
// Floating menu min-width resize observer
|
||||
this.$nextTick(() => {
|
||||
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
|
||||
if (!floatingMenuContainer) return;
|
||||
|
||||
// Start a new observation of the now-open floating menu
|
||||
this.containerResizeObserver.disconnect();
|
||||
this.containerResizeObserver.observe(floatingMenuContainer);
|
||||
});
|
||||
// Floating menu min-width resize observer
|
||||
|
||||
await nextTick();
|
||||
|
||||
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
|
||||
if (!floatingMenuContainer) return;
|
||||
|
||||
// Start a new observation of the now-open floating menu
|
||||
this.containerResizeObserver.disconnect();
|
||||
this.containerResizeObserver.observe(floatingMenuContainer);
|
||||
}
|
||||
|
||||
// Switching from open to closed
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { IconName } from "@/utility-functions/icons";
|
||||
import { MenuListEntry, SectionsOfMenuListEntries, MenuListEntryData } from "@/wasm-communication/messages";
|
||||
|
||||
import FloatingMenu, { MenuDirection } from "@/components/floating-menus/FloatingMenu.vue";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
@@ -172,23 +172,6 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import Separator from "@/components/widgets/labels/Separator.vue";
|
||||
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
|
||||
|
||||
export type MenuListEntries<Value = string> = MenuListEntry<Value>[];
|
||||
export type SectionsOfMenuListEntries<Value = string> = MenuListEntries<Value>[];
|
||||
|
||||
interface MenuListEntryData<Value = string> {
|
||||
value?: Value;
|
||||
label?: string;
|
||||
icon?: IconName;
|
||||
font?: URL;
|
||||
shortcut?: string[];
|
||||
shortcutRequiresLock?: boolean;
|
||||
disabled?: boolean;
|
||||
action?: () => void;
|
||||
children?: SectionsOfMenuListEntries;
|
||||
}
|
||||
|
||||
export type MenuListEntry<Value = string> = MenuListEntryData<Value> & { ref?: typeof FloatingMenu | typeof MenuList };
|
||||
|
||||
const KEYBOARD_LOCK_USE_FULLSCREEN = "This hotkey is reserved by the browser, but becomes available in fullscreen mode";
|
||||
const KEYBOARD_LOCK_SWITCH_BROWSER = "This hotkey is reserved by the browser, but becomes available in Chrome, Edge, and Opera which support the Keyboard.lock() API";
|
||||
|
||||
|
||||
@@ -17,12 +17,7 @@
|
||||
<LayoutCol class="spacer"></LayoutCol>
|
||||
|
||||
<LayoutCol class="working-colors">
|
||||
<SwatchPairInput />
|
||||
<LayoutRow class="swap-and-reset">
|
||||
<!-- TODO: Remember to make these tooltip input hints customized to macOS also -->
|
||||
<IconButton :action="swapWorkingColors" :icon="'Swap'" title="Swap (Shift+X)" :size="16" />
|
||||
<IconButton :action="resetWorkingColors" :icon="'ResetColors'" title="Reset (Ctrl+Shift+X)" :size="16" />
|
||||
</LayoutRow>
|
||||
<WidgetLayout :layout="workingColorsLayout" />
|
||||
</LayoutCol>
|
||||
</LayoutCol>
|
||||
<LayoutCol class="viewport">
|
||||
@@ -135,8 +130,16 @@
|
||||
.working-colors {
|
||||
flex: 0 0 auto;
|
||||
|
||||
.swap-and-reset {
|
||||
flex: 0 0 auto;
|
||||
.widget-row {
|
||||
min-height: 0;
|
||||
|
||||
.swatch-pair {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
--widget-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,55 +225,26 @@ import { defineComponent, nextTick } from "vue";
|
||||
|
||||
import { textInputCleanup } from "@/utility-functions/keyboard-entry";
|
||||
import {
|
||||
UpdateDocumentArtwork,
|
||||
UpdateDocumentOverlays,
|
||||
UpdateDocumentScrollbars,
|
||||
UpdateDocumentRulers,
|
||||
UpdateDocumentArtboards,
|
||||
UpdateMouseCursor,
|
||||
UpdateDocumentModeLayout,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
UpdateWorkingColorsLayout,
|
||||
defaultWidgetLayout,
|
||||
UpdateDocumentBarLayout,
|
||||
UpdateImageData,
|
||||
TriggerTextCommit,
|
||||
TriggerViewportResize,
|
||||
DisplayRemoveEditableTextbox,
|
||||
DisplayEditableTextbox,
|
||||
MouseCursorIcon,
|
||||
XY,
|
||||
} from "@/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
|
||||
import CanvasRuler from "@/components/widgets/metrics/CanvasRuler.vue";
|
||||
import PersistentScrollbar from "@/components/widgets/metrics/PersistentScrollbar.vue";
|
||||
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["editor"],
|
||||
inject: ["editor", "panels"],
|
||||
methods: {
|
||||
viewportResize() {
|
||||
// Resize the canvas
|
||||
|
||||
const canvas = this.$refs.canvas as HTMLElement;
|
||||
|
||||
// Get the width and height rounded up to the nearest even number because resizing is centered and dividing an odd number by 2 for centering causes antialiasing
|
||||
let width = Math.ceil(parseFloat(getComputedStyle(canvas).width));
|
||||
if (width % 2 === 1) width += 1;
|
||||
let height = Math.ceil(parseFloat(getComputedStyle(canvas).height));
|
||||
if (height % 2 === 1) height += 1;
|
||||
|
||||
this.canvasSvgWidth = `${width}px`;
|
||||
this.canvasSvgHeight = `${height}px`;
|
||||
|
||||
// Resize the rulers
|
||||
const rulerHorizontal = this.$refs.rulerHorizontal as typeof CanvasRuler;
|
||||
const rulerVertical = this.$refs.rulerVertical as typeof CanvasRuler;
|
||||
rulerHorizontal?.resize();
|
||||
rulerVertical?.resize();
|
||||
},
|
||||
pasteFile(e: DragEvent) {
|
||||
const { dataTransfer } = e;
|
||||
if (!dataTransfer) return;
|
||||
@@ -304,12 +278,6 @@ export default defineComponent({
|
||||
const move = delta < 0 ? 1 : -1;
|
||||
this.editor.instance.translate_canvas_by_fraction(0, move);
|
||||
},
|
||||
swapWorkingColors() {
|
||||
this.editor.instance.swap_colors();
|
||||
},
|
||||
resetWorkingColors() {
|
||||
this.editor.instance.reset_colors();
|
||||
},
|
||||
canvasPointerDown(e: PointerEvent) {
|
||||
const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable;
|
||||
if (!onEditbox) {
|
||||
@@ -317,76 +285,65 @@ export default defineComponent({
|
||||
canvas.setPointerCapture(e.pointerId);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, (UpdateDocumentArtwork) => {
|
||||
this.artworkSvg = UpdateDocumentArtwork.svg;
|
||||
// Update rendered SVGs
|
||||
async updateDocumentArtwork(svg: string) {
|
||||
this.artworkSvg = svg;
|
||||
|
||||
nextTick((): void => {
|
||||
if (this.textInput) {
|
||||
const canvas = this.$refs.canvas as HTMLElement;
|
||||
const foreignObject = canvas.getElementsByTagName("foreignObject")[0] as SVGForeignObjectElement;
|
||||
if (foreignObject.children.length > 0) return;
|
||||
await nextTick();
|
||||
|
||||
const addedInput = foreignObject.appendChild(this.textInput);
|
||||
|
||||
nextTick((): void => {
|
||||
// Necessary to select contenteditable: https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element/6150060#6150060
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(addedInput);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
addedInput.focus();
|
||||
addedInput.click();
|
||||
});
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("modifyinputfield", {
|
||||
detail: addedInput,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentOverlays, (updateDocumentOverlays) => {
|
||||
this.overlaysSvg = updateDocumentOverlays.svg;
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentArtboards, (updateDocumentArtboards) => {
|
||||
this.artboardSvg = updateDocumentArtboards.svg;
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentScrollbars, (updateDocumentScrollbars) => {
|
||||
this.scrollbarPos = updateDocumentScrollbars.position;
|
||||
this.scrollbarSize = updateDocumentScrollbars.size;
|
||||
this.scrollbarMultiplier = updateDocumentScrollbars.multiplier;
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentRulers, (updateDocumentRulers) => {
|
||||
this.rulerOrigin = updateDocumentRulers.origin;
|
||||
this.rulerSpacing = updateDocumentRulers.spacing;
|
||||
this.rulerInterval = updateDocumentRulers.interval;
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateMouseCursor, (updateMouseCursor) => {
|
||||
this.canvasCursor = updateMouseCursor.cursor;
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(TriggerTextCommit, () => {
|
||||
if (this.textInput) {
|
||||
const textCleaned = textInputCleanup(this.textInput.innerText);
|
||||
this.editor.instance.on_change_text(textCleaned);
|
||||
}
|
||||
});
|
||||
const canvas = this.$refs.canvas as HTMLElement;
|
||||
const foreignObject = canvas.getElementsByTagName("foreignObject")[0] as SVGForeignObjectElement;
|
||||
if (foreignObject.children.length > 0) return;
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(DisplayEditableTextbox, (displayEditableTextbox) => {
|
||||
const addedInput = foreignObject.appendChild(this.textInput);
|
||||
window.dispatchEvent(new CustomEvent("modifyinputfield", { detail: addedInput }));
|
||||
|
||||
await nextTick();
|
||||
|
||||
// Necessary to select contenteditable: https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element/6150060#6150060
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(addedInput);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
addedInput.focus();
|
||||
addedInput.click();
|
||||
}
|
||||
},
|
||||
updateDocumentOverlays(svg: string) {
|
||||
this.overlaysSvg = svg;
|
||||
},
|
||||
updateDocumentArtboards(svg: string) {
|
||||
this.artboardSvg = svg;
|
||||
},
|
||||
// Update scrollbars and rulers
|
||||
updateDocumentScrollbars(position: XY, size: XY, multiplier: XY) {
|
||||
this.scrollbarPos = position;
|
||||
this.scrollbarSize = size;
|
||||
this.scrollbarMultiplier = multiplier;
|
||||
},
|
||||
updateDocumentRulers(origin: XY, spacing: number, interval: number) {
|
||||
this.rulerOrigin = origin;
|
||||
this.rulerSpacing = spacing;
|
||||
this.rulerInterval = interval;
|
||||
},
|
||||
// Update mouse cursor icon
|
||||
updateMouseCursor(cursor: MouseCursorIcon) {
|
||||
this.canvasCursor = cursor;
|
||||
},
|
||||
// Text entry
|
||||
triggerTextCommit() {
|
||||
if (!this.textInput) return;
|
||||
const textCleaned = textInputCleanup(this.textInput.innerText);
|
||||
this.editor.instance.on_change_text(textCleaned);
|
||||
},
|
||||
displayEditableTextbox(displayEditableTextbox: DisplayEditableTextbox) {
|
||||
this.textInput = document.createElement("DIV") as HTMLDivElement;
|
||||
|
||||
if (displayEditableTextbox.text === "") this.textInput.textContent = "";
|
||||
@@ -399,49 +356,52 @@ export default defineComponent({
|
||||
this.textInput.style.color = displayEditableTextbox.color.toRgbaCSS();
|
||||
|
||||
this.textInput.oninput = (): void => {
|
||||
if (this.textInput) this.editor.instance.update_bounds(textInputCleanup(this.textInput.innerText));
|
||||
if (!this.textInput) return;
|
||||
this.editor.instance.update_bounds(textInputCleanup(this.textInput.innerText));
|
||||
};
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(DisplayRemoveEditableTextbox, () => {
|
||||
},
|
||||
displayRemoveEditableTextbox() {
|
||||
this.textInput = undefined;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("modifyinputfield", {
|
||||
detail: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentModeLayout, (updateDocumentModeLayout) => {
|
||||
window.dispatchEvent(new CustomEvent("modifyinputfield", { detail: undefined }));
|
||||
},
|
||||
// Update layouts
|
||||
updateDocumentModeLayout(updateDocumentModeLayout: UpdateDocumentModeLayout) {
|
||||
this.documentModeLayout = updateDocumentModeLayout;
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateToolOptionsLayout, (updateToolOptionsLayout) => {
|
||||
},
|
||||
updateToolOptionsLayout(updateToolOptionsLayout: UpdateToolOptionsLayout) {
|
||||
this.toolOptionsLayout = updateToolOptionsLayout;
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentBarLayout, (updateDocumentBarLayout) => {
|
||||
},
|
||||
updateDocumentBarLayout(updateDocumentBarLayout: UpdateDocumentBarLayout) {
|
||||
this.documentBarLayout = updateDocumentBarLayout;
|
||||
});
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateToolShelfLayout, (updateToolShelfLayout) => {
|
||||
},
|
||||
updateToolShelfLayout(updateToolShelfLayout: UpdateToolShelfLayout) {
|
||||
this.toolShelfLayout = updateToolShelfLayout;
|
||||
});
|
||||
},
|
||||
updateWorkingColorsLayout(updateWorkingColorsLayout: UpdateWorkingColorsLayout) {
|
||||
this.workingColorsLayout = updateWorkingColorsLayout;
|
||||
},
|
||||
// Resize elements to render the new viewport size
|
||||
viewportResize() {
|
||||
// Resize the canvas
|
||||
// Width and height are rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
|
||||
const canvas = this.$refs.canvas as HTMLElement;
|
||||
const width = Math.ceil(parseFloat(getComputedStyle(canvas).width));
|
||||
const height = Math.ceil(parseFloat(getComputedStyle(canvas).height));
|
||||
this.canvasSvgWidth = `${width % 2 === 1 ? width + 1 : width}px`;
|
||||
this.canvasSvgHeight = `${height % 2 === 1 ? height + 1 : height}px`;
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(TriggerViewportResize, this.viewportResize);
|
||||
// Resize the rulers
|
||||
const rulerHorizontal = this.$refs.rulerHorizontal as typeof CanvasRuler;
|
||||
const rulerVertical = this.$refs.rulerVertical as typeof CanvasRuler;
|
||||
rulerHorizontal?.resize();
|
||||
rulerVertical?.resize();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.panels.registerPanel("Document", this);
|
||||
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||
updateImageData.image_data.forEach(async (element) => {
|
||||
// Using updateImageData.image_data.buffer returns undefined for some reason?
|
||||
const blob = new Blob([new Uint8Array(element.image_data.values()).buffer], { type: element.mime });
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const image = await createImageBitmap(blob);
|
||||
|
||||
this.editor.instance.set_image_blob_url(element.path, url, image.width, image.height);
|
||||
});
|
||||
});
|
||||
// Once this component is mounted, we want to resend the document bounds to the backend via the resize event handler which does that
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -449,39 +409,38 @@ export default defineComponent({
|
||||
textInput: undefined as undefined | HTMLDivElement,
|
||||
|
||||
// CSS properties
|
||||
canvasSvgWidth: "100%",
|
||||
canvasSvgHeight: "100%",
|
||||
canvasCursor: "default",
|
||||
canvasSvgWidth: "100%" as string,
|
||||
canvasSvgHeight: "100%" as string,
|
||||
canvasCursor: "default" as MouseCursorIcon,
|
||||
|
||||
// Scrollbars
|
||||
scrollbarPos: { x: 0.5, y: 0.5 },
|
||||
scrollbarSize: { x: 0.5, y: 0.5 },
|
||||
scrollbarMultiplier: { x: 0, y: 0 },
|
||||
scrollbarPos: { x: 0.5, y: 0.5 } as XY,
|
||||
scrollbarSize: { x: 0.5, y: 0.5 } as XY,
|
||||
scrollbarMultiplier: { x: 0, y: 0 } as XY,
|
||||
|
||||
// Rulers
|
||||
rulerOrigin: { x: 0, y: 0 },
|
||||
rulerSpacing: 100,
|
||||
rulerInterval: 100,
|
||||
rulerOrigin: { x: 0, y: 0 } as XY,
|
||||
rulerSpacing: 100 as number,
|
||||
rulerInterval: 100 as number,
|
||||
|
||||
// Rendered SVG viewport data
|
||||
artworkSvg: "",
|
||||
artboardSvg: "",
|
||||
overlaysSvg: "",
|
||||
artworkSvg: "" as string,
|
||||
artboardSvg: "" as string,
|
||||
overlaysSvg: "" as string,
|
||||
|
||||
// Layouts
|
||||
documentModeLayout: defaultWidgetLayout(),
|
||||
toolOptionsLayout: defaultWidgetLayout(),
|
||||
documentBarLayout: defaultWidgetLayout(),
|
||||
toolShelfLayout: defaultWidgetLayout(),
|
||||
workingColorsLayout: defaultWidgetLayout(),
|
||||
};
|
||||
},
|
||||
components: {
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
SwatchPairInput,
|
||||
PersistentScrollbar,
|
||||
CanvasRuler,
|
||||
IconButton,
|
||||
WidgetLayout,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`.trim() || null"
|
||||
>
|
||||
<LayoutRow class="layer-type-icon">
|
||||
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" :style="'node'" title="Folder" />
|
||||
<IconLabel v-else-if="listing.entry.layer_type === 'Image'" :icon="'NodeImage'" :style="'node'" title="Image" />
|
||||
<IconLabel v-else-if="listing.entry.layer_type === 'Shape'" :icon="'NodeShape'" :style="'node'" title="Shape" />
|
||||
<IconLabel v-else-if="listing.entry.layer_type === 'Text'" :icon="'NodeText'" :style="'node'" title="Path" />
|
||||
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" :iconStyle="'Node'" title="Folder" />
|
||||
<IconLabel v-else-if="listing.entry.layer_type === 'Image'" :icon="'NodeImage'" :iconStyle="'Node'" title="Image" />
|
||||
<IconLabel v-else-if="listing.entry.layer_type === 'Shape'" :icon="'NodeShape'" :iconStyle="'Node'" title="Shape" />
|
||||
<IconLabel v-else-if="listing.entry.layer_type === 'Text'" :icon="'NodeText'" :iconStyle="'Node'" title="Path" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
|
||||
<input
|
||||
@@ -261,7 +261,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
import { defaultWidgetLayout, UpdateDocumentLayerTreeStructure, UpdateDocumentLayerDetails, UpdateLayerTreeOptionsLayout, LayerPanelEntry } from "@/wasm-communication/messages";
|
||||
|
||||
@@ -313,16 +313,16 @@ export default defineComponent({
|
||||
handleExpandArrowClick(path: BigUint64Array) {
|
||||
this.editor.instance.toggle_layer_expansion(path);
|
||||
},
|
||||
onEditLayerName(listing: LayerListingInfo) {
|
||||
async onEditLayerName(listing: LayerListingInfo) {
|
||||
if (listing.editingName) return;
|
||||
|
||||
this.draggable = false;
|
||||
|
||||
listing.editingName = true;
|
||||
const tree: HTMLElement = (this.$refs.layerTreeList as typeof LayoutCol).$el;
|
||||
this.$nextTick(() => {
|
||||
(tree.querySelector("[data-text-input]:not([disabled])") as HTMLInputElement).select();
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
(tree.querySelector("[data-text-input]:not([disabled])") as HTMLInputElement).select();
|
||||
},
|
||||
onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | null) {
|
||||
// Eliminate duplicate events
|
||||
@@ -334,13 +334,13 @@ export default defineComponent({
|
||||
listing.editingName = false;
|
||||
this.editor.instance.set_layer_name(listing.entry.path, name);
|
||||
},
|
||||
onEditLayerNameDeselect(listing: LayerListingInfo) {
|
||||
async onEditLayerNameDeselect(listing: LayerListingInfo) {
|
||||
this.draggable = true;
|
||||
|
||||
listing.editingName = false;
|
||||
this.$nextTick(() => {
|
||||
window.getSelection()?.removeAllRanges();
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
window.getSelection()?.removeAllRanges();
|
||||
},
|
||||
async selectLayer(clickedLayer: LayerPanelEntry, ctrl: boolean, shift: boolean) {
|
||||
this.editor.instance.select_layer(clickedLayer.path, ctrl, shift);
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeImage'" :style="'node'" />
|
||||
<IconLabel :icon="'NodeImage'" :iconStyle="'Node'" />
|
||||
<TextLabel>Image</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeImage'" :style="'node'" />
|
||||
<IconLabel :icon="'NodeImage'" :iconStyle="'Node'" />
|
||||
<TextLabel>Mask</TextLabel>
|
||||
</div>
|
||||
<div class="arguments">
|
||||
@@ -69,7 +69,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeTransform'" :style="'node'" />
|
||||
<IconLabel :icon="'NodeTransform'" :iconStyle="'Node'" />
|
||||
<TextLabel>Transform</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeMotionBlur'" :style="'node'" />
|
||||
<IconLabel :icon="'NodeMotionBlur'" :iconStyle="'Node'" />
|
||||
<TextLabel>Motion Blur</TextLabel>
|
||||
</div>
|
||||
<div class="arguments">
|
||||
@@ -110,7 +110,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeShape'" :style="'node'" />
|
||||
<IconLabel :icon="'NodeShape'" :iconStyle="'Node'" />
|
||||
<TextLabel>Shape</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeBrushwork'" :style="'node'" />
|
||||
<IconLabel :icon="'NodeBrushwork'" :iconStyle="'Node'" />
|
||||
<TextLabel>Brushwork</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,7 +138,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeBlur'" :style="'node'" />
|
||||
<IconLabel :icon="'NodeBlur'" :iconStyle="'Node'" />
|
||||
<TextLabel>Blur</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +152,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeGradient'" :style="'node'" />
|
||||
<IconLabel :icon="'NodeGradient'" :iconStyle="'Node'" />
|
||||
<TextLabel>Gradient</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,35 +4,41 @@
|
||||
<template>
|
||||
<div :class="`widget-${direction}`">
|
||||
<template v-for="(component, index) in widgets" :key="index">
|
||||
<CheckboxInput v-if="component.kind === 'CheckboxInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" v-model:open="open" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<DropdownInput v-if="component.kind === 'DropdownInput'" v-bind="component.props" v-model:open="open" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
||||
<FontInput
|
||||
v-if="component.kind === 'FontInput'"
|
||||
<CheckboxInput v-if="component.props.kind === 'CheckboxInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widgetId, value)" />
|
||||
<ColorInput v-if="component.props.kind === 'ColorInput'" v-bind="component.props" v-model:open="open" @update:value="(value: string) => updateLayout(component.widgetId, value)" />
|
||||
<DropdownInput
|
||||
v-if="component.props.kind === 'DropdownInput'"
|
||||
v-bind="component.props"
|
||||
v-model:open="open"
|
||||
@changeFont="(value: { name: string, style: string, file: string }) => updateLayout(component.widget_id, value)"
|
||||
@update:selectedIndex="(value: number) => updateLayout(component.widgetId, value)"
|
||||
/>
|
||||
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||
<IconLabel v-if="component.kind === 'IconLabel'" v-bind="component.props" />
|
||||
<NumberInput
|
||||
v-if="component.kind === 'NumberInput'"
|
||||
<FontInput
|
||||
v-if="component.props.kind === 'FontInput'"
|
||||
v-bind="component.props"
|
||||
@update:value="(value: number) => updateLayout(component.widget_id, value)"
|
||||
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
|
||||
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
||||
v-model:open="open"
|
||||
@changeFont="(value: { name: string, style: string, file: string }) => updateLayout(component.widgetId, value)"
|
||||
/>
|
||||
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||
<PopoverButton v-if="component.kind === 'PopoverButton'">
|
||||
<h3>{{ component.props.title }}</h3>
|
||||
<IconButton v-if="component.props.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widgetId, null)" />
|
||||
<IconLabel v-if="component.props.kind === 'IconLabel'" v-bind="component.props" />
|
||||
<NumberInput
|
||||
v-if="component.props.kind === 'NumberInput'"
|
||||
v-bind="component.props"
|
||||
@update:value="(value: number) => updateLayout(component.widgetId, value)"
|
||||
:incrementCallbackIncrease="() => updateLayout(component.widgetId, 'Increment')"
|
||||
:incrementCallbackDecrease="() => updateLayout(component.widgetId, 'Decrement')"
|
||||
/>
|
||||
<OptionalInput v-if="component.props.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widgetId, value)" />
|
||||
<PopoverButton v-if="component.props.kind === 'PopoverButton'" v-bind="component.props">
|
||||
<h3>{{ component.props.header }}</h3>
|
||||
<p>{{ component.props.text }}</p>
|
||||
</PopoverButton>
|
||||
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
||||
<Separator v-if="component.kind === 'Separator'" v-bind="component.props" />
|
||||
<TextAreaInput v-if="component.kind === 'TextAreaInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<TextButton v-if="component.kind === 'TextButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<TextLabel v-if="component.kind === 'TextLabel'" v-bind="withoutValue(component.props)">{{ component.props.value }}</TextLabel>
|
||||
<RadioInput v-if="component.props.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widgetId, value)" />
|
||||
<Separator v-if="component.props.kind === 'Separator'" v-bind="component.props" />
|
||||
<SwatchPairInput v-if="component.props.kind === 'SwatchPairInput'" v-bind="component.props" />
|
||||
<TextAreaInput v-if="component.props.kind === 'TextAreaInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widgetId, value)" />
|
||||
<TextButton v-if="component.props.kind === 'TextButton'" v-bind="component.props" :action="() => updateLayout(component.widgetId, null)" />
|
||||
<TextInput v-if="component.props.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widgetId, value)" />
|
||||
<TextLabel v-if="component.props.kind === 'TextLabel'" v-bind="withoutValue(component.props)">{{ component.props.value }}</TextLabel>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -84,6 +90,7 @@ import FontInput from "@/components/widgets/inputs/FontInput.vue";
|
||||
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
||||
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
|
||||
import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
|
||||
import TextAreaInput from "@/components/widgets/inputs/TextAreaInput.vue";
|
||||
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
@@ -123,21 +130,22 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Separator,
|
||||
PopoverButton,
|
||||
TextButton,
|
||||
CheckboxInput,
|
||||
NumberInput,
|
||||
TextInput,
|
||||
IconButton,
|
||||
OptionalInput,
|
||||
RadioInput,
|
||||
DropdownInput,
|
||||
TextLabel,
|
||||
IconLabel,
|
||||
ColorInput,
|
||||
DropdownInput,
|
||||
FontInput,
|
||||
IconButton,
|
||||
IconLabel,
|
||||
NumberInput,
|
||||
OptionalInput,
|
||||
PopoverButton,
|
||||
RadioInput,
|
||||
Separator,
|
||||
SwatchPairInput,
|
||||
TextAreaInput,
|
||||
TextButton,
|
||||
TextInput,
|
||||
TextLabel,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<button :class="['icon-button', `size-${size}`, active && 'active']" @click="(e: MouseEvent) => action(e)">
|
||||
<button :class="['icon-button', `size-${size}`, active && 'active']" @click="(e: MouseEvent) => action(e)" :title="tooltip">
|
||||
<IconLabel :icon="icon" />
|
||||
</button>
|
||||
</template>
|
||||
@@ -69,11 +69,13 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
||||
icon: { type: String as PropType<IconName>, required: true },
|
||||
size: { type: Number as PropType<IconSize>, required: true },
|
||||
active: { type: Boolean as PropType<boolean>, default: false },
|
||||
gapAfter: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
|
||||
// Callbacks
|
||||
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
||||
},
|
||||
components: { IconLabel },
|
||||
});
|
||||
|
||||
@@ -62,8 +62,10 @@ export default defineComponent({
|
||||
LayoutRow,
|
||||
},
|
||||
props: {
|
||||
action: { type: Function as PropType<() => void>, required: false },
|
||||
icon: { type: String as PropType<IconName>, default: "DropdownArrow" },
|
||||
|
||||
// Callbacks
|
||||
action: { type: Function as PropType<() => void>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// TODO: Try and get rid of the need for this file
|
||||
|
||||
export interface TextButtonWidget {
|
||||
kind: "TextButton";
|
||||
tooltip?: string;
|
||||
message?: string | object;
|
||||
callback?: () => void;
|
||||
props: {
|
||||
// `action` is used via `IconButtonWidget.callback`
|
||||
kind: "TextButton";
|
||||
label: string;
|
||||
emphasized?: boolean;
|
||||
disabled?: boolean;
|
||||
minWidth?: number;
|
||||
gapAfter?: boolean;
|
||||
disabled?: boolean;
|
||||
|
||||
// Callbacks
|
||||
// `action` is used via `IconButtonWidget.callback`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,12 +62,13 @@ import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
|
||||
label: { type: String as PropType<string>, required: true },
|
||||
emphasized: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
gapAfter: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Callbacks
|
||||
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
|
||||
},
|
||||
components: { TextLabel },
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<LayoutRow class="checkbox-input">
|
||||
<input type="checkbox" :id="`checkbox-input-${id}`" :checked="checked" @change="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" />
|
||||
<label :for="`checkbox-input-${id}`" :tabindex="disableTabIndex ? -1 : 0" @keydown.enter="(e) => ((e.target as HTMLElement).previousSibling as HTMLInputElement).click()">
|
||||
<label :for="`checkbox-input-${id}`" tabindex="0" @keydown.enter="(e) => ((e.target as HTMLElement).previousSibling as HTMLInputElement).click()" :title="tooltip">
|
||||
<LayoutRow class="checkbox-box">
|
||||
<IconLabel :icon="icon" />
|
||||
</LayoutRow>
|
||||
@@ -66,6 +66,11 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:checked"],
|
||||
props: {
|
||||
checked: { type: Boolean as PropType<boolean>, default: false },
|
||||
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: `${Math.random()}`.substring(2),
|
||||
@@ -76,11 +81,6 @@ export default defineComponent({
|
||||
return this.checked;
|
||||
},
|
||||
},
|
||||
props: {
|
||||
checked: { type: Boolean as PropType<boolean>, default: false },
|
||||
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
||||
disableTabIndex: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
LayoutRow,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutRow class="color-input">
|
||||
<OptionalInput v-if="canSetTransparent" :icon="'CloseX'" :checked="!!value" @update:checked="(val) => updateEnabled(val)"></OptionalInput>
|
||||
<LayoutRow class="color-input" :title="tooltip">
|
||||
<OptionalInput v-if="!noTransparency" :icon="'CloseX'" :checked="Boolean(value)" @update:checked="(val) => updateEnabled(val)"></OptionalInput>
|
||||
<TextInput :value="displayValue" :label="label" :disabled="disabled || !value" @commitText="(value: string) => textInputUpdated(value)" :center="true" />
|
||||
<Separator :type="'Related'" />
|
||||
<LayoutRow class="swatch">
|
||||
@@ -82,11 +82,15 @@ import Separator from "@/components/widgets/labels/Separator.vue";
|
||||
export default defineComponent({
|
||||
emits: ["update:value", "update:open"],
|
||||
props: {
|
||||
value: { type: String as PropType<string | undefined>, required: true },
|
||||
open: { type: Boolean as PropType<boolean>, required: true },
|
||||
value: { type: String as PropType<string | undefined>, required: false },
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
canSetTransparent: { type: Boolean as PropType<boolean>, required: false, default: true },
|
||||
noTransparency: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
|
||||
// Bound through `v-model`
|
||||
// TODO: See if this should be made to follow the pattern of DropdownInput.vue so this could be removed
|
||||
open: { type: Boolean as PropType<boolean>, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -99,7 +99,9 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, toRaw } from "vue";
|
||||
|
||||
import MenuList, { MenuListEntry, SectionsOfMenuListEntries } from "@/components/floating-menus/MenuList.vue";
|
||||
import { MenuListEntry, SectionsOfMenuListEntries } from "@/wasm-communication/messages";
|
||||
|
||||
import MenuList from "@/components/floating-menus/MenuList.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
|
||||
|
||||
@@ -71,9 +71,10 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, PropType } from "vue";
|
||||
|
||||
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
|
||||
import MenuList, { MenuListEntry } from "@/components/floating-menus/MenuList.vue";
|
||||
import { MenuListEntry } from "@/wasm-communication/messages";
|
||||
|
||||
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
|
||||
import MenuList from "@/components/floating-menus/MenuList.vue";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
@@ -84,8 +85,8 @@ export default defineComponent({
|
||||
props: {
|
||||
fontFamily: { type: String as PropType<string>, required: true },
|
||||
fontStyle: { type: String as PropType<string>, required: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
isStyle: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -111,6 +112,7 @@ export default defineComponent({
|
||||
},
|
||||
async setOpen() {
|
||||
this.open = true;
|
||||
|
||||
// Scroll to the active entry (the scroller div does not yet exist so we must wait for vue to render)
|
||||
await nextTick();
|
||||
if (this.activeEntry) {
|
||||
|
||||
@@ -72,9 +72,9 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { MenuEntry, UpdateMenuBarLayout } from "@/wasm-communication/messages";
|
||||
import { MenuEntry, UpdateMenuBarLayout, MenuListEntry } from "@/wasm-communication/messages";
|
||||
|
||||
import MenuList, { MenuListEntry } from "@/components/floating-menus/MenuList.vue";
|
||||
import MenuList from "@/components/floating-menus/MenuList.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
|
||||
const LOCK_REQUIRING_SHORTCUTS = [
|
||||
@@ -100,7 +100,7 @@ export default defineComponent({
|
||||
group.map((entry) => ({
|
||||
...entry,
|
||||
children: entry.children ? menuEntryToFrontendMenuEntry(entry.children) : undefined,
|
||||
action: (): void => this.editor.instance.update_layout(updateMenuBarLayout.layout_target, entry.action.widget_id, undefined),
|
||||
action: (): void => this.editor.instance.update_layout(updateMenuBarLayout.layout_target, entry.action.widgetId, undefined),
|
||||
shortcutRequiresLock: entry.shortcut ? shortcutRequiresLock(entry.shortcut) : undefined,
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -87,27 +87,30 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { IncrementBehavior } from "@/wasm-communication/messages";
|
||||
|
||||
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
||||
|
||||
type IncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
|
||||
type IncrementDirection = "Decrease" | "Increase";
|
||||
export type IncrementDirection = "Decrease" | "Increase";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:value"],
|
||||
props: {
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
value: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
|
||||
min: { type: Number as PropType<number>, required: false },
|
||||
max: { type: Number as PropType<number>, required: false },
|
||||
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
|
||||
incrementFactor: { type: Number as PropType<number>, default: 1 },
|
||||
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
|
||||
incrementCallbackDecrease: { type: Function as PropType<() => void>, required: false },
|
||||
isInteger: { type: Boolean as PropType<boolean>, default: false },
|
||||
displayDecimalPlaces: { type: Number as PropType<number>, default: 3 },
|
||||
unit: { type: String as PropType<string>, default: "" },
|
||||
unitIsHiddenWhenEditing: { type: Boolean as PropType<boolean>, default: true },
|
||||
displayDecimalPlaces: { type: Number as PropType<number>, default: 3 },
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
|
||||
incrementFactor: { type: Number as PropType<number>, default: 1 },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Callbacks
|
||||
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
|
||||
incrementCallbackDecrease: { type: Function as PropType<() => void>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutRow class="optional-input">
|
||||
<CheckboxInput :checked="checked" @input="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" :icon="icon" />
|
||||
<CheckboxInput :checked="checked" @input="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" :icon="icon" :tooltip="tooltip" />
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
@@ -47,6 +47,7 @@ export default defineComponent({
|
||||
props: {
|
||||
checked: { type: Boolean as PropType<boolean>, required: true },
|
||||
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
components: {
|
||||
CheckboxInput,
|
||||
|
||||
@@ -64,22 +64,12 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { IconName } from "@/utility-functions/icons";
|
||||
import { RadioEntries, RadioEntryData } from "@/wasm-communication/messages";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
export interface RadioEntryData {
|
||||
value?: string;
|
||||
label?: string;
|
||||
icon?: IconName;
|
||||
tooltip?: string;
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
export type RadioEntries = RadioEntryData[];
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:selectedIndex"],
|
||||
props: {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<LayoutCol class="swatch-pair">
|
||||
<LayoutRow class="secondary swatch">
|
||||
<button @click="() => clickSecondarySwatch()" ref="secondaryButton" data-hover-menu-spawner></button>
|
||||
<button @click="() => clickSecondarySwatch()" :style="`--swatch-color: ${secondary.toRgbaCSS()}`" data-hover-menu-spawner></button>
|
||||
<FloatingMenu :type="'Popover'" :direction="'Right'" v-model:open="secondaryOpen">
|
||||
<ColorPicker @update:color="(color: RGBA) => secondaryColorChanged(color)" :color="secondaryColor" />
|
||||
<ColorPicker @update:color="(color: RGBA) => secondaryColorChanged(color)" :color="secondary.toRgba()" />
|
||||
</FloatingMenu>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="primary swatch">
|
||||
<button @click="() => clickPrimarySwatch()" ref="primaryButton" data-hover-menu-spawner></button>
|
||||
<button @click="() => clickPrimarySwatch()" :style="`--swatch-color: ${primary.toRgbaCSS()}`" data-hover-menu-spawner></button>
|
||||
<FloatingMenu :type="'Popover'" :direction="'Right'" v-model:open="primaryOpen">
|
||||
<ColorPicker @update:color="(color: RGBA) => primaryColorChanged(color)" :color="primaryColor" />
|
||||
<ColorPicker @update:color="(color: RGBA) => primaryColorChanged(color)" :color="primary.toRgba()" />
|
||||
</FloatingMenu>
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
@@ -66,10 +66,10 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { rgbaToDecimalRgba } from "@/utility-functions/color";
|
||||
import { type RGBA, UpdateWorkingColors } from "@/wasm-communication/messages";
|
||||
import { type RGBA, Color } from "@/wasm-communication/messages";
|
||||
|
||||
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
|
||||
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
|
||||
@@ -84,12 +84,14 @@ export default defineComponent({
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
},
|
||||
props: {
|
||||
primary: { type: Object as PropType<Color>, required: true },
|
||||
secondary: { type: Object as PropType<Color>, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
primaryOpen: false,
|
||||
secondaryOpen: false,
|
||||
primaryColor: { r: 0, g: 0, b: 0, a: 1 } as RGBA,
|
||||
secondaryColor: { r: 255, g: 255, b: 255, a: 1 } as RGBA,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -102,44 +104,13 @@ export default defineComponent({
|
||||
this.secondaryOpen = true;
|
||||
},
|
||||
primaryColorChanged(color: RGBA) {
|
||||
this.primaryColor = color;
|
||||
this.updatePrimaryColor();
|
||||
const newColor = rgbaToDecimalRgba(color);
|
||||
this.editor.instance.update_primary_color(newColor.r, newColor.g, newColor.b, newColor.a);
|
||||
},
|
||||
secondaryColorChanged(color: RGBA) {
|
||||
this.secondaryColor = color;
|
||||
this.updateSecondaryColor();
|
||||
const newColor = rgbaToDecimalRgba(color);
|
||||
this.editor.instance.update_secondary_color(newColor.r, newColor.g, newColor.b, newColor.a);
|
||||
},
|
||||
async updatePrimaryColor() {
|
||||
let color = this.primaryColor;
|
||||
const button = this.$refs.primaryButton as HTMLButtonElement;
|
||||
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
|
||||
|
||||
color = rgbaToDecimalRgba(this.primaryColor);
|
||||
this.editor.instance.update_primary_color(color.r, color.g, color.b, color.a);
|
||||
},
|
||||
async updateSecondaryColor() {
|
||||
let color = this.secondaryColor;
|
||||
const button = this.$refs.secondaryButton as HTMLButtonElement;
|
||||
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
|
||||
|
||||
color = rgbaToDecimalRgba(this.secondaryColor);
|
||||
this.editor.instance.update_secondary_color(color.r, color.g, color.b, color.a);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateWorkingColors, (updateWorkingColors) => {
|
||||
this.primaryColor = updateWorkingColors.primary.toRgba();
|
||||
this.secondaryColor = updateWorkingColors.secondary.toRgba();
|
||||
|
||||
const primaryButton = this.$refs.primaryButton as HTMLButtonElement;
|
||||
primaryButton.style.setProperty("--swatch-color", updateWorkingColors.primary.toRgbaCSS());
|
||||
|
||||
const secondaryButton = this.$refs.secondaryButton as HTMLButtonElement;
|
||||
secondaryButton.style.setProperty("--swatch-color", updateWorkingColors.secondary.toRgbaCSS());
|
||||
});
|
||||
|
||||
this.updatePrimaryColor();
|
||||
this.updateSecondaryColor();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<LayoutRow :class="['icon-label', iconSize, iconStyle]">
|
||||
<LayoutRow :class="['icon-label', iconSizeClass, iconStyleClass]">
|
||||
<component :is="icon" />
|
||||
</LayoutRow>
|
||||
</template>
|
||||
@@ -42,16 +42,15 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
export default defineComponent({
|
||||
props: {
|
||||
icon: { type: String as PropType<IconName>, required: true },
|
||||
gapAfter: { type: Boolean as PropType<boolean>, default: false },
|
||||
style: { type: String as PropType<IconStyle>, default: "" },
|
||||
iconStyle: { type: String as PropType<IconStyle | undefined>, required: false },
|
||||
},
|
||||
computed: {
|
||||
iconSize(): string {
|
||||
iconSizeClass(): string {
|
||||
return `size-${icons[this.icon].size}`;
|
||||
},
|
||||
iconStyle(): string {
|
||||
if (!this.style) return "";
|
||||
return `${this.style}-style`;
|
||||
iconStyleClass(): string {
|
||||
if (!this.iconStyle || this.iconStyle === "Normal") return "";
|
||||
return `${this.iconStyle.toLowerCase()}-style`;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
export type SeparatorDirection = "Horizontal" | "Vertical";
|
||||
export type SeparatorType = "Related" | "Unrelated" | "Section" | "List";
|
||||
import { SeparatorDirection, SeparatorType } from "@/wasm-communication/messages";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
||||
@@ -21,7 +21,36 @@
|
||||
</PopoverButton>
|
||||
</LayoutRow>
|
||||
<LayoutCol class="panel-body">
|
||||
<component :is="panelType" />
|
||||
<component :is="panelType" v-if="panelType" />
|
||||
<LayoutCol class="empty-panel" v-else>
|
||||
<LayoutCol class="content">
|
||||
<LayoutRow class="logotype">
|
||||
<IconLabel :icon="'GraphiteLogotypeSolid'" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="actions">
|
||||
<LayoutCol>
|
||||
<IconButton :action="() => newDocument()" :icon="'File'" :size="24" />
|
||||
<IconButton :action="() => openDocument()" :icon="'Folder'" :size="24" />
|
||||
</LayoutCol>
|
||||
<LayoutCol>
|
||||
<Separator :type="'Related'" />
|
||||
<Separator :type="'Related'" />
|
||||
</LayoutCol>
|
||||
<LayoutCol>
|
||||
<TextLabel>New Document:</TextLabel>
|
||||
<TextLabel>Open Document:</TextLabel>
|
||||
</LayoutCol>
|
||||
<LayoutCol>
|
||||
<Separator :type="'Unrelated'" />
|
||||
<Separator :type="'Unrelated'" />
|
||||
</LayoutCol>
|
||||
<LayoutCol>
|
||||
<UserInputLabel :inputKeys="[['KeyControl', 'KeyN']]" />
|
||||
<UserInputLabel :inputKeys="[['KeyControl', 'KeyO']]" />
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</LayoutCol>
|
||||
</LayoutCol>
|
||||
</LayoutCol>
|
||||
</template>
|
||||
@@ -141,6 +170,45 @@
|
||||
flex: 1 1 100%;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
.empty-panel {
|
||||
background: var(--color-2-mildblack);
|
||||
margin: 4px;
|
||||
border-radius: 2px;
|
||||
justify-content: center;
|
||||
|
||||
.content {
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
|
||||
.logotype {
|
||||
margin-bottom: 40px;
|
||||
|
||||
svg {
|
||||
width: auto;
|
||||
height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
> div {
|
||||
gap: 8px;
|
||||
|
||||
> * {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.text-label {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.user-input-label {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -156,6 +224,10 @@ import NodeGraph from "@/components/panels/NodeGraph.vue";
|
||||
import Properties from "@/components/panels/Properties.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import Separator from "@/components/widgets/labels/Separator.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
|
||||
|
||||
const panelComponents = {
|
||||
Document,
|
||||
@@ -168,18 +240,31 @@ const panelComponents = {
|
||||
type PanelTypes = keyof typeof panelComponents;
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["editor"],
|
||||
props: {
|
||||
tabMinWidths: { type: Boolean as PropType<boolean>, default: false },
|
||||
tabCloseButtons: { type: Boolean as PropType<boolean>, default: false },
|
||||
tabLabels: { type: Array as PropType<string[]>, required: true },
|
||||
tabActiveIndex: { type: Number as PropType<number>, required: true },
|
||||
panelType: { type: String as PropType<PanelTypes>, required: true },
|
||||
panelType: { type: String as PropType<PanelTypes>, required: false },
|
||||
clickAction: { type: Function as PropType<(index: number) => void>, required: false },
|
||||
closeAction: { type: Function as PropType<(index: number) => void>, required: false },
|
||||
},
|
||||
methods: {
|
||||
newDocument() {
|
||||
this.editor.instance.new_document_dialog();
|
||||
},
|
||||
openDocument() {
|
||||
this.editor.instance.open_file_upload();
|
||||
},
|
||||
},
|
||||
components: {
|
||||
LayoutCol,
|
||||
LayoutRow,
|
||||
IconLabel,
|
||||
TextLabel,
|
||||
UserInputLabel,
|
||||
Separator,
|
||||
...panelComponents,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<LayoutCol class="workspace-grid-subdivision">
|
||||
<LayoutRow class="workspace-grid-subdivision">
|
||||
<Panel
|
||||
:panelType="'Document'"
|
||||
:panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined"
|
||||
:tabCloseButtons="true"
|
||||
:tabMinWidths="true"
|
||||
:tabLabels="portfolio.state.documents.map((doc) => doc.displayName)"
|
||||
@@ -64,7 +64,7 @@
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
import DialogModal from "@/components/floating-menus/DialogModal.vue";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
@@ -117,7 +117,7 @@ export default defineComponent({
|
||||
nextSibling.style.flexGrow = (nextSiblingSize + mouseDelta).toString();
|
||||
previousSibling.style.flexGrow = (previousSiblingSize - mouseDelta).toString();
|
||||
|
||||
window.dispatchEvent(new CustomEvent("resize", { detail: {} }));
|
||||
window.dispatchEvent(new CustomEvent("resize"));
|
||||
}
|
||||
|
||||
function cleanup(event: PointerEvent): void {
|
||||
@@ -134,12 +134,12 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeDocumentIndex(newIndex: number) {
|
||||
this.$nextTick(() => {
|
||||
const documentsPanel = this.$refs.documentsPanel as typeof Panel;
|
||||
const newActiveTab = documentsPanel.$el.querySelectorAll("[data-tab-bar] [data-tab]")[newIndex];
|
||||
newActiveTab.scrollIntoView();
|
||||
});
|
||||
async activeDocumentIndex(newIndex: number) {
|
||||
await nextTick();
|
||||
|
||||
const documentsPanel = this.$refs.documentsPanel as typeof Panel;
|
||||
const newActiveTab = documentsPanel.$el.querySelectorAll("[data-tab-bar] [data-tab]")[newIndex];
|
||||
newActiveTab.scrollIntoView();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
18
frontend/src/io-managers/blob.ts
Normal file
18
frontend/src/io-managers/blob.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Editor } from "@/wasm-communication/editor";
|
||||
import { UpdateImageData } from "@/wasm-communication/messages";
|
||||
|
||||
export function createBlobManager(editor: Editor): void {
|
||||
// Subscribe to process backend event
|
||||
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||
updateImageData.image_data.forEach(async (element) => {
|
||||
// Using updateImageData.image_data.buffer returns undefined for some reason?
|
||||
const blob = new Blob([new Uint8Array(element.image_data.values()).buffer], { type: element.mime });
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const image = await createImageBitmap(blob);
|
||||
|
||||
editor.instance.set_image_blob_url(element.path, url, image.width, image.height);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { DialogState } from "@/state-providers/dialog";
|
||||
import { IconName } from "@/utility-functions/icons";
|
||||
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||
import { Editor } from "@/wasm-communication/editor";
|
||||
import { DisplayDialogPanic, WidgetLayout } from "@/wasm-communication/messages";
|
||||
import { DisplayDialogPanic, Widget, WidgetLayout } from "@/wasm-communication/messages";
|
||||
|
||||
export function createPanicManager(editor: Editor, dialogState: DialogState): void {
|
||||
// Code panic dialog and console error
|
||||
@@ -17,53 +17,32 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(panicDetails);
|
||||
|
||||
const panicDialog = preparePanicDialog(displayDialogPanic.title, displayDialogPanic.description, panicDetails);
|
||||
const panicDialog = preparePanicDialog(displayDialogPanic.header, displayDialogPanic.description, panicDetails);
|
||||
dialogState.createPanicDialog(...panicDialog);
|
||||
});
|
||||
}
|
||||
|
||||
function preparePanicDialog(title: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
|
||||
function preparePanicDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
|
||||
const widgets: WidgetLayout = {
|
||||
layout: [
|
||||
{
|
||||
rowWidgets: [
|
||||
{
|
||||
kind: "TextLabel",
|
||||
props: { value: title, bold: true },
|
||||
// eslint-disable-next-line camelcase
|
||||
widget_id: 0n,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
rowWidgets: [
|
||||
{
|
||||
kind: "TextLabel",
|
||||
props: { value: details, multiline: true },
|
||||
// eslint-disable-next-line camelcase
|
||||
widget_id: 0n,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ rowWidgets: [new Widget({ kind: "TextLabel", value: header, bold: true, italic: false, tableAlign: false, multiline: false }, 0n)] },
|
||||
{ rowWidgets: [new Widget({ kind: "TextLabel", value: details, bold: false, italic: false, tableAlign: false, multiline: true }, 1n)] },
|
||||
],
|
||||
// eslint-disable-next-line camelcase
|
||||
layout_target: null,
|
||||
};
|
||||
|
||||
const reloadButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => window.location.reload(),
|
||||
props: { label: "Reload", emphasized: true, minWidth: 96 },
|
||||
props: { kind: "TextButton", label: "Reload", emphasized: true, minWidth: 96 },
|
||||
};
|
||||
const copyErrorLogButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => navigator.clipboard.writeText(panicDetails),
|
||||
props: { label: "Copy Error Log", emphasized: false, minWidth: 96 },
|
||||
props: { kind: "TextButton", label: "Copy Error Log", emphasized: false, minWidth: 96 },
|
||||
};
|
||||
const reportOnGithubButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => window.open(githubUrl(panicDetails), "_blank"),
|
||||
props: { label: "Report Bug", emphasized: false, minWidth: 96 },
|
||||
props: { kind: "TextButton", label: "Report Bug", emphasized: false, minWidth: 96 },
|
||||
};
|
||||
const jsCallbackBasedButtons = [reloadButton, copyErrorLogButton, reportOnGithubButton];
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import App from "@/App.vue";
|
||||
}
|
||||
</style>
|
||||
<h2>This browser is too old</h2>
|
||||
<p>Please upgrade to a modern web browser such as the latest Firefox, Chrome, Edge, or Safari version 15 or later.</p>
|
||||
<p>Please upgrade to a modern web browser such as the latest Firefox, Chrome, Edge, or Safari version 15 or newer.</p>
|
||||
<p>(The <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array#browser_compatibility" target="_blank"><code>BigInt64Array</code></a>
|
||||
JavaScript API must be supported by the browser for Graphite to function.)</p>
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { reactive, readonly } from "vue";
|
||||
|
||||
import { Editor } from "@/wasm-communication/editor";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createFullscreenState() {
|
||||
export function createFullscreenState(_: Editor) {
|
||||
const state = reactive({
|
||||
windowFullscreen: false,
|
||||
keyboardLocked: false,
|
||||
|
||||
130
frontend/src/state-providers/panels.ts
Normal file
130
frontend/src/state-providers/panels.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { nextTick, reactive, readonly } from "vue";
|
||||
|
||||
import { Editor } from "@/wasm-communication/editor";
|
||||
import {
|
||||
DisplayEditableTextbox,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerRefreshBoundsOfViewports,
|
||||
TriggerTextCommit,
|
||||
TriggerViewportResize,
|
||||
UpdateDocumentArtboards,
|
||||
UpdateDocumentArtwork,
|
||||
UpdateDocumentBarLayout,
|
||||
UpdateDocumentModeLayout,
|
||||
UpdateDocumentOverlays,
|
||||
UpdateDocumentRulers,
|
||||
UpdateDocumentScrollbars,
|
||||
UpdateMouseCursor,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
UpdateWorkingColorsLayout,
|
||||
} from "@/wasm-communication/messages";
|
||||
|
||||
import DocumentComponent from "@/components/panels/Document.vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createPanelsState(editor: Editor) {
|
||||
const state = reactive({
|
||||
documentPanel: DocumentComponent,
|
||||
});
|
||||
|
||||
// We use `any` instead of `typeof DocumentComponent` as a workaround for the fact that calling this function with the `this` argument from within `Document.vue` isn't a compatible type
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function registerPanel(type: string, panelComponent: any): void {
|
||||
state.documentPanel = panelComponent;
|
||||
}
|
||||
|
||||
function subscribeDocumentPanel(): void {
|
||||
// Update rendered SVGs
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, async (updateDocumentArtwork) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateDocumentArtwork(updateDocumentArtwork.svg);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentOverlays, async (updateDocumentOverlays) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateDocumentOverlays(updateDocumentOverlays.svg);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtboards, async (updateDocumentArtboards) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateDocumentArtboards(updateDocumentArtboards.svg);
|
||||
});
|
||||
|
||||
// Update scrollbars and rulers
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentScrollbars, async (updateDocumentScrollbars) => {
|
||||
await nextTick();
|
||||
const { position, size, multiplier } = updateDocumentScrollbars;
|
||||
state.documentPanel.updateDocumentScrollbars(position, size, multiplier);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentRulers, async (updateDocumentRulers) => {
|
||||
await nextTick();
|
||||
const { origin, spacing, interval } = updateDocumentRulers;
|
||||
state.documentPanel.updateDocumentRulers(origin, spacing, interval);
|
||||
});
|
||||
|
||||
// Update mouse cursor icon
|
||||
editor.subscriptions.subscribeJsMessage(UpdateMouseCursor, async (updateMouseCursor) => {
|
||||
await nextTick();
|
||||
const { cursor } = updateMouseCursor;
|
||||
state.documentPanel.updateMouseCursor(cursor);
|
||||
});
|
||||
|
||||
// Text entry
|
||||
editor.subscriptions.subscribeJsMessage(TriggerTextCommit, async () => {
|
||||
await nextTick();
|
||||
state.documentPanel.triggerTextCommit();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextbox, async (displayEditableTextbox) => {
|
||||
await nextTick();
|
||||
state.documentPanel.displayEditableTextbox(displayEditableTextbox);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayRemoveEditableTextbox, async () => {
|
||||
await nextTick();
|
||||
state.documentPanel.displayRemoveEditableTextbox();
|
||||
});
|
||||
|
||||
// Update layouts
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentModeLayout, async (updateDocumentModeLayout) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateDocumentModeLayout(updateDocumentModeLayout);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateToolOptionsLayout, async (updateToolOptionsLayout) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateToolOptionsLayout(updateToolOptionsLayout);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentBarLayout, async (updateDocumentBarLayout) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateDocumentBarLayout(updateDocumentBarLayout);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateToolShelfLayout, async (updateToolShelfLayout) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateToolShelfLayout(updateToolShelfLayout);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateWorkingColorsLayout, async (updateWorkingColorsLayout) => {
|
||||
await nextTick();
|
||||
state.documentPanel.updateWorkingColorsLayout(updateWorkingColorsLayout);
|
||||
});
|
||||
|
||||
// Resize elements to render the new viewport size
|
||||
editor.subscriptions.subscribeJsMessage(TriggerViewportResize, async () => {
|
||||
await nextTick();
|
||||
state.documentPanel.viewportResize();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerRefreshBoundsOfViewports, async () => {
|
||||
// Wait to display the unpopulated document panel (missing: tools, options bar content, scrollbar positioning, and canvas)
|
||||
await nextTick();
|
||||
// Wait to display the populated document panel
|
||||
await nextTick();
|
||||
|
||||
// Request a resize event so the viewport gets measured now that the canvas is populated and positioned correctly
|
||||
window.dispatchEvent(new CustomEvent("resize"));
|
||||
});
|
||||
}
|
||||
|
||||
subscribeDocumentPanel();
|
||||
|
||||
return {
|
||||
state: readonly(state) as typeof state,
|
||||
registerPanel,
|
||||
};
|
||||
}
|
||||
export type PanelsState = ReturnType<typeof createPanelsState>;
|
||||
@@ -1,5 +1,12 @@
|
||||
/* eslint-disable import/first */
|
||||
|
||||
// Graphics
|
||||
import GraphiteLogotypeSolid from "@/../assets/graphics/graphite-logotype-solid.svg";
|
||||
|
||||
const GRAPHICS = {
|
||||
GraphiteLogotypeSolid: { component: GraphiteLogotypeSolid, size: null },
|
||||
} as const;
|
||||
|
||||
// 12px Solid
|
||||
import Checkmark from "@/../assets/icon-12px-solid/checkmark.svg";
|
||||
import CloseX from "@/../assets/icon-12px-solid/close-x.svg";
|
||||
@@ -83,6 +90,7 @@ import EyeVisible from "@/../assets/icon-16px-solid/eye-visible.svg";
|
||||
import File from "@/../assets/icon-16px-solid/file.svg";
|
||||
import FlipHorizontal from "@/../assets/icon-16px-solid/flip-horizontal.svg";
|
||||
import FlipVertical from "@/../assets/icon-16px-solid/flip-vertical.svg";
|
||||
import Folder from "@/../assets/icon-16px-solid/folder.svg";
|
||||
import GraphiteLogo from "@/../assets/icon-16px-solid/graphite-logo.svg";
|
||||
import NodeArtboard from "@/../assets/icon-16px-solid/node-artboard.svg";
|
||||
import NodeBlur from "@/../assets/icon-16px-solid/node-blur.svg";
|
||||
@@ -130,6 +138,7 @@ const SOLID_16PX = {
|
||||
File: { component: File, size: 16 },
|
||||
FlipHorizontal: { component: FlipHorizontal, size: 16 },
|
||||
FlipVertical: { component: FlipVertical, size: 16 },
|
||||
Folder: { component: Folder, size: 16 },
|
||||
GraphiteLogo: { component: GraphiteLogo, size: 16 },
|
||||
NodeArtboard: { component: NodeArtboard, size: 16 },
|
||||
NodeBlur: { component: NodeBlur, size: 16 },
|
||||
@@ -232,6 +241,7 @@ const TWO_TONE_24PX = {
|
||||
|
||||
// All icons
|
||||
const ICON_LIST = {
|
||||
...GRAPHICS,
|
||||
...SOLID_12PX,
|
||||
...SOLID_16PX,
|
||||
...TWO_TONE_16PX,
|
||||
@@ -243,8 +253,8 @@ export const icons: IconDefinitionType<typeof ICON_LIST> = ICON_LIST;
|
||||
export const iconComponents = Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component]));
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
export type IconSize = 12 | 16 | 24 | 32;
|
||||
export type IconStyle = "node" | "";
|
||||
export type IconSize = null | 12 | 16 | 24 | 32;
|
||||
export type IconStyle = "Normal" | "Node";
|
||||
|
||||
// The following helper type declarations allow us to avoid manually maintaining the `IconName` type declaration as a string union paralleling the keys of the
|
||||
// icon definitions. It lets TypeScript do that for us. Our goal is to define the big key-value pair of icons by constraining its values, but inferring its keys.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { Transform, Type, plainToClass } from "class-transformer";
|
||||
|
||||
import { IconName } from "@/utility-functions/icons";
|
||||
import { IconName, IconSize, IconStyle } from "@/utility-functions/icons";
|
||||
import type { WasmEditorInstance, WasmRawInstance } from "@/wasm-communication/editor";
|
||||
|
||||
export class JsMessage {
|
||||
@@ -12,7 +12,7 @@ export class JsMessage {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Add additional classes to replicate Rust's `FrontendMessage`s and data structures below.
|
||||
// Add additional classes below to replicate Rust's `FrontendMessage`s and data structures.
|
||||
//
|
||||
// Remember to add each message to the `messageConstructors` export at the bottom of the file.
|
||||
//
|
||||
@@ -20,6 +20,15 @@ export class JsMessage {
|
||||
// for details about how to transform the JSON from wasm-bindgen into classes.
|
||||
// ============================================================================
|
||||
|
||||
export class UpdateNodeGraphVisibility extends JsMessage {
|
||||
readonly visible!: boolean;
|
||||
}
|
||||
|
||||
export class UpdateOpenDocumentsList extends JsMessage {
|
||||
@Type(() => FrontendDocumentDetails)
|
||||
readonly open_documents!: FrontendDocumentDetails[];
|
||||
}
|
||||
|
||||
// Allows the auto save system to use a string for the id rather than a BigInt.
|
||||
// IndexedDb does not allow for BigInts as primary keys.
|
||||
// TypeScript does not allow subclasses to change the type of class variables in subclasses.
|
||||
@@ -40,13 +49,24 @@ export class FrontendDocumentDetails extends DocumentDetails {
|
||||
readonly id!: bigint;
|
||||
}
|
||||
|
||||
export class UpdateNodeGraphVisibility extends JsMessage {
|
||||
readonly visible!: boolean;
|
||||
export class TriggerIndexedDbWriteDocument extends JsMessage {
|
||||
document!: string;
|
||||
|
||||
@Type(() => IndexedDbDocumentDetails)
|
||||
details!: IndexedDbDocumentDetails;
|
||||
|
||||
version!: string;
|
||||
}
|
||||
|
||||
export class UpdateOpenDocumentsList extends JsMessage {
|
||||
@Type(() => FrontendDocumentDetails)
|
||||
readonly open_documents!: FrontendDocumentDetails[];
|
||||
export class IndexedDbDocumentDetails extends DocumentDetails {
|
||||
@Transform(({ value }: { value: bigint }) => value.toString())
|
||||
id!: string;
|
||||
}
|
||||
|
||||
export class TriggerIndexedDbRemoveDocument extends JsMessage {
|
||||
// Use a string since IndexedDB can not use BigInts for keys
|
||||
@Transform(({ value }: { value: bigint }) => value.toString())
|
||||
document_id!: string;
|
||||
}
|
||||
|
||||
export class UpdateInputHints extends JsMessage {
|
||||
@@ -86,7 +106,7 @@ export type HSVA = {
|
||||
a: number;
|
||||
};
|
||||
|
||||
const To255Scale = Transform(({ value }) => value * 255);
|
||||
const To255Scale = Transform(({ value }: { value: number }) => value * 255);
|
||||
export class Color {
|
||||
@To255Scale
|
||||
readonly red!: number;
|
||||
@@ -109,14 +129,6 @@ export class Color {
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateWorkingColors extends JsMessage {
|
||||
@Type(() => Color)
|
||||
readonly primary!: Color;
|
||||
|
||||
@Type(() => Color)
|
||||
readonly secondary!: Color;
|
||||
}
|
||||
|
||||
export class UpdateActiveDocument extends JsMessage {
|
||||
readonly document_id!: bigint;
|
||||
}
|
||||
@@ -124,7 +136,7 @@ export class UpdateActiveDocument extends JsMessage {
|
||||
export class DisplayDialogPanic extends JsMessage {
|
||||
readonly panic_info!: string;
|
||||
|
||||
readonly title!: string;
|
||||
readonly header!: string;
|
||||
|
||||
readonly description!: string;
|
||||
}
|
||||
@@ -145,48 +157,46 @@ export class UpdateDocumentArtboards extends JsMessage {
|
||||
readonly svg!: string;
|
||||
}
|
||||
|
||||
const TupleToVec2 = Transform(({ value }) => ({ x: value[0], y: value[1] }));
|
||||
const TupleToVec2 = Transform(({ value }: { value: [number, number] }) => ({ x: value[0], y: value[1] }));
|
||||
|
||||
export type XY = { x: number; y: number };
|
||||
|
||||
export class UpdateDocumentScrollbars extends JsMessage {
|
||||
@TupleToVec2
|
||||
readonly position!: { x: number; y: number };
|
||||
readonly position!: XY;
|
||||
|
||||
@TupleToVec2
|
||||
readonly size!: { x: number; y: number };
|
||||
readonly size!: XY;
|
||||
|
||||
@TupleToVec2
|
||||
readonly multiplier!: { x: number; y: number };
|
||||
readonly multiplier!: XY;
|
||||
}
|
||||
|
||||
export class UpdateDocumentRulers extends JsMessage {
|
||||
@TupleToVec2
|
||||
readonly origin!: { x: number; y: number };
|
||||
readonly origin!: XY;
|
||||
|
||||
readonly spacing!: number;
|
||||
|
||||
readonly interval!: number;
|
||||
}
|
||||
|
||||
export type MouseCursorIcon = "default" | "zoom-in" | "zoom-out" | "grabbing" | "crosshair" | "text" | "ns-resize" | "ew-resize" | "nesw-resize" | "nwse-resize";
|
||||
|
||||
const ToCssCursorProperty = Transform(({ value }) => {
|
||||
const cssNames: Record<string, MouseCursorIcon> = {
|
||||
ZoomIn: "zoom-in",
|
||||
ZoomOut: "zoom-out",
|
||||
Grabbing: "grabbing",
|
||||
Crosshair: "crosshair",
|
||||
Text: "text",
|
||||
NSResize: "ns-resize",
|
||||
EWResize: "ew-resize",
|
||||
NESWResize: "nesw-resize",
|
||||
NWSEResize: "nwse-resize",
|
||||
};
|
||||
|
||||
return cssNames[value] || "default";
|
||||
});
|
||||
const mouseCursorIconCSSNames = {
|
||||
ZoomIn: "zoom-in",
|
||||
ZoomOut: "zoom-out",
|
||||
Grabbing: "grabbing",
|
||||
Crosshair: "crosshair",
|
||||
Text: "text",
|
||||
NSResize: "ns-resize",
|
||||
EWResize: "ew-resize",
|
||||
NESWResize: "nesw-resize",
|
||||
NWSEResize: "nwse-resize",
|
||||
} as const;
|
||||
export type MouseCursor = keyof typeof mouseCursorIconCSSNames;
|
||||
export type MouseCursorIcon = typeof mouseCursorIconCSSNames[MouseCursor];
|
||||
|
||||
export class UpdateMouseCursor extends JsMessage {
|
||||
@ToCssCursorProperty
|
||||
@Transform(({ value }: { value: MouseCursor }) => mouseCursorIconCSSNames[value] || "default")
|
||||
readonly cursor!: MouseCursorIcon;
|
||||
}
|
||||
|
||||
@@ -208,9 +218,11 @@ export class TriggerRasterDownload extends JsMessage {
|
||||
readonly mime!: string;
|
||||
|
||||
@TupleToVec2
|
||||
readonly size!: { x: number; y: number };
|
||||
readonly size!: XY;
|
||||
}
|
||||
|
||||
export class TriggerRefreshBoundsOfViewports extends JsMessage {}
|
||||
|
||||
export class DocumentChanged extends JsMessage {}
|
||||
|
||||
export class UpdateDocumentLayerTreeStructure extends JsMessage {
|
||||
@@ -290,6 +302,7 @@ export class DisplayEditableTextbox extends JsMessage {
|
||||
}
|
||||
|
||||
export class UpdateImageData extends JsMessage {
|
||||
@Type(() => ImageData)
|
||||
readonly image_data!: ImageData[];
|
||||
}
|
||||
|
||||
@@ -307,7 +320,7 @@ export class LayerPanelEntry {
|
||||
|
||||
layer_type!: LayerType;
|
||||
|
||||
@Transform(({ value }) => new BigUint64Array(value))
|
||||
@Transform(({ value }: { value: bigint[] }) => new BigUint64Array(value))
|
||||
path!: BigUint64Array;
|
||||
|
||||
@Type(() => LayerMetadata)
|
||||
@@ -332,28 +345,8 @@ export class ImageData {
|
||||
readonly image_data!: Uint8Array;
|
||||
}
|
||||
|
||||
export class IndexedDbDocumentDetails extends DocumentDetails {
|
||||
@Transform(({ value }: { value: bigint }) => value.toString())
|
||||
id!: string;
|
||||
}
|
||||
|
||||
export class DisplayDialogDismiss extends JsMessage {}
|
||||
|
||||
export class TriggerIndexedDbWriteDocument extends JsMessage {
|
||||
document!: string;
|
||||
|
||||
@Type(() => IndexedDbDocumentDetails)
|
||||
details!: IndexedDbDocumentDetails;
|
||||
|
||||
version!: string;
|
||||
}
|
||||
|
||||
export class TriggerIndexedDbRemoveDocument extends JsMessage {
|
||||
// Use a string since IndexedDB can not use BigInts for keys
|
||||
@Transform(({ value }: { value: bigint }) => value.toString())
|
||||
document_id!: string;
|
||||
}
|
||||
|
||||
export class Font {
|
||||
font_family!: string;
|
||||
|
||||
@@ -371,15 +364,273 @@ export class TriggerVisitLink extends JsMessage {
|
||||
url!: string;
|
||||
}
|
||||
|
||||
export class TriggerTextCommit extends JsMessage {}
|
||||
|
||||
export class TriggerTextCopy extends JsMessage {
|
||||
readonly copy_text!: string;
|
||||
}
|
||||
|
||||
export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
|
||||
readonly commit_date!: string;
|
||||
}
|
||||
|
||||
export class TriggerViewportResize extends JsMessage {}
|
||||
|
||||
// WIDGET PROPS
|
||||
|
||||
export abstract class WidgetProps {
|
||||
kind!: string;
|
||||
}
|
||||
|
||||
export class CheckboxInput extends WidgetProps {
|
||||
checked!: boolean;
|
||||
|
||||
icon!: IconName;
|
||||
|
||||
tooltip!: string;
|
||||
}
|
||||
|
||||
export class ColorInput extends WidgetProps {
|
||||
value!: string | undefined;
|
||||
|
||||
label!: string | undefined;
|
||||
|
||||
noTransparency!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
tooltip!: string;
|
||||
}
|
||||
|
||||
export interface MenuListEntryData<Value = string> {
|
||||
value?: Value;
|
||||
label?: string;
|
||||
icon?: IconName;
|
||||
font?: URL;
|
||||
shortcut?: string[];
|
||||
shortcutRequiresLock?: boolean;
|
||||
disabled?: boolean;
|
||||
action?: () => void;
|
||||
children?: SectionsOfMenuListEntries;
|
||||
}
|
||||
export type MenuListEntry<Value = string> = MenuListEntryData<Value> & { ref?: typeof FloatingMenu | typeof MenuList };
|
||||
export type MenuListEntries<Value = string> = MenuListEntry<Value>[];
|
||||
export type SectionsOfMenuListEntries<Value = string> = MenuListEntries<Value>[];
|
||||
|
||||
export class DropdownInput extends WidgetProps {
|
||||
entries!: SectionsOfMenuListEntries;
|
||||
|
||||
selectedIndex!: number | undefined;
|
||||
|
||||
drawIcon!: boolean;
|
||||
|
||||
interactive!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
}
|
||||
|
||||
export class FontInput extends WidgetProps {
|
||||
fontFamily!: string;
|
||||
|
||||
fontStyle!: string;
|
||||
|
||||
isStyle!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
}
|
||||
|
||||
export class IconButton extends WidgetProps {
|
||||
icon!: IconName;
|
||||
|
||||
size!: IconSize;
|
||||
|
||||
active!: boolean;
|
||||
|
||||
tooltip!: string;
|
||||
}
|
||||
|
||||
export class IconLabel extends WidgetProps {
|
||||
icon!: IconName;
|
||||
|
||||
iconStyle!: IconStyle | undefined;
|
||||
}
|
||||
|
||||
export type IncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
|
||||
|
||||
export class NumberInput extends WidgetProps {
|
||||
label!: string | undefined;
|
||||
|
||||
value!: number | undefined;
|
||||
|
||||
min!: number | undefined;
|
||||
|
||||
max!: number | undefined;
|
||||
|
||||
isInteger!: boolean;
|
||||
|
||||
displayDecimalPlaces!: number;
|
||||
|
||||
unit!: string;
|
||||
|
||||
unitIsHiddenWhenEditing!: boolean;
|
||||
|
||||
incrementBehavior!: IncrementBehavior;
|
||||
|
||||
incrementFactor!: number;
|
||||
|
||||
disabled!: boolean;
|
||||
}
|
||||
|
||||
export class OptionalInput extends WidgetProps {
|
||||
checked!: boolean;
|
||||
|
||||
icon!: IconName;
|
||||
|
||||
tooltip!: string;
|
||||
}
|
||||
|
||||
export class PopoverButton extends WidgetProps {
|
||||
icon!: string | undefined;
|
||||
|
||||
// Body
|
||||
header!: string;
|
||||
|
||||
text!: string;
|
||||
}
|
||||
|
||||
export interface RadioEntryData {
|
||||
value?: string;
|
||||
label?: string;
|
||||
icon?: IconName;
|
||||
tooltip?: string;
|
||||
|
||||
// Callbacks
|
||||
action?: () => void;
|
||||
}
|
||||
export type RadioEntries = RadioEntryData[];
|
||||
|
||||
export class RadioInput extends WidgetProps {
|
||||
entries!: RadioEntries;
|
||||
|
||||
selectedIndex!: number;
|
||||
}
|
||||
|
||||
export type SeparatorDirection = "Horizontal" | "Vertical";
|
||||
export type SeparatorType = "Related" | "Unrelated" | "Section" | "List";
|
||||
|
||||
export class Separator extends WidgetProps {
|
||||
direction!: SeparatorDirection;
|
||||
|
||||
type!: SeparatorType;
|
||||
}
|
||||
|
||||
export class SwatchPairInput extends WidgetProps {
|
||||
@Type(() => Color)
|
||||
primary!: Color;
|
||||
|
||||
@Type(() => Color)
|
||||
secondary!: Color;
|
||||
}
|
||||
|
||||
export class TextAreaInput extends WidgetProps {
|
||||
value!: string;
|
||||
|
||||
label!: string | undefined;
|
||||
|
||||
disabled!: boolean;
|
||||
}
|
||||
|
||||
export class TextButton extends WidgetProps {
|
||||
label!: string;
|
||||
|
||||
emphasized!: boolean;
|
||||
|
||||
minWidth!: number;
|
||||
|
||||
disabled!: boolean;
|
||||
}
|
||||
|
||||
export class TextInput extends WidgetProps {
|
||||
value!: string;
|
||||
|
||||
label!: string | undefined;
|
||||
|
||||
disabled!: boolean;
|
||||
}
|
||||
|
||||
export class TextLabel extends WidgetProps {
|
||||
// Body
|
||||
value!: string;
|
||||
|
||||
// Props
|
||||
bold!: boolean;
|
||||
|
||||
italic!: boolean;
|
||||
|
||||
tableAlign!: boolean;
|
||||
|
||||
multiline!: boolean;
|
||||
}
|
||||
|
||||
// WIDGET
|
||||
|
||||
const widgetSubTypes = [
|
||||
{ value: CheckboxInput, name: "CheckboxInput" },
|
||||
{ value: ColorInput, name: "ColorInput" },
|
||||
{ value: DropdownInput, name: "DropdownInput" },
|
||||
{ value: FontInput, name: "FontInput" },
|
||||
{ value: IconButton, name: "IconButton" },
|
||||
{ value: IconLabel, name: "IconLabel" },
|
||||
{ value: NumberInput, name: "NumberInput" },
|
||||
{ value: OptionalInput, name: "OptionalInput" },
|
||||
{ value: PopoverButton, name: "PopoverButton" },
|
||||
{ value: RadioInput, name: "RadioInput" },
|
||||
{ value: Separator, name: "Separator" },
|
||||
{ value: SwatchPairInput, name: "SwatchPairInput" },
|
||||
{ value: TextAreaInput, name: "TextAreaInput" },
|
||||
{ value: TextButton, name: "TextButton" },
|
||||
{ value: TextInput, name: "TextInput" },
|
||||
{ value: TextLabel, name: "TextLabel" },
|
||||
];
|
||||
export type WidgetPropsSet = InstanceType<typeof widgetSubTypes[number]["value"]>;
|
||||
|
||||
export class Widget {
|
||||
constructor(props: WidgetPropsSet, widgetId: bigint) {
|
||||
this.props = props;
|
||||
this.widgetId = widgetId;
|
||||
}
|
||||
|
||||
@Type(() => WidgetProps, { discriminator: { property: "kind", subTypes: widgetSubTypes }, keepDiscriminatorProperty: true })
|
||||
props!: WidgetPropsSet;
|
||||
|
||||
widgetId!: bigint;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function hoistWidgetHolders(widgetHolders: any[]): Widget[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return widgetHolders.map((widgetHolder: any) => {
|
||||
const kind = Object.keys(widgetHolder.widget)[0];
|
||||
const props = widgetHolder.widget[kind];
|
||||
props.kind = kind;
|
||||
|
||||
const { widgetId } = widgetHolder;
|
||||
|
||||
return plainToClass(Widget, { props, widgetId });
|
||||
});
|
||||
}
|
||||
|
||||
// WIDGET LAYOUT
|
||||
|
||||
export interface WidgetLayout {
|
||||
layout: LayoutGroup[];
|
||||
layout_target: unknown;
|
||||
layout: LayoutGroup[];
|
||||
}
|
||||
|
||||
export function defaultWidgetLayout(): WidgetLayout {
|
||||
return {
|
||||
layout: [],
|
||||
layout_target: null,
|
||||
layout: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -400,107 +651,27 @@ export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSect
|
||||
return Boolean((layoutRow as WidgetSection).layout);
|
||||
}
|
||||
|
||||
export type WidgetKind =
|
||||
| "CheckboxInput"
|
||||
| "ColorInput"
|
||||
| "DropdownInput"
|
||||
| "FontInput"
|
||||
| "IconButton"
|
||||
| "IconLabel"
|
||||
| "NumberInput"
|
||||
| "OptionalInput"
|
||||
| "PopoverButton"
|
||||
| "RadioInput"
|
||||
| "Separator"
|
||||
| "TextAreaInput"
|
||||
| "TextButton"
|
||||
| "TextInput"
|
||||
| "TextLabel";
|
||||
|
||||
export interface Widget {
|
||||
kind: WidgetKind;
|
||||
widget_id: bigint;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
props: any;
|
||||
}
|
||||
|
||||
export class UpdateDialogDetails extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateDocumentModeLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateDocumentBarLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateToolShelfLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdatePropertyPanelSectionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateLayerTreeOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
// Unpacking rust types to more usable type in the frontend
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function createWidgetLayout(widgetLayout: any[]): LayoutGroup[] {
|
||||
return widgetLayout.map((layoutType): LayoutGroup => {
|
||||
if (layoutType.Column) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const columnWidgets = hoistWidgetHolders(layoutType.Column.columnWidgets);
|
||||
if (layoutType.column) {
|
||||
const columnWidgets = hoistWidgetHolders(layoutType.column.columnWidgets);
|
||||
|
||||
const result: WidgetColumn = { columnWidgets };
|
||||
return result;
|
||||
}
|
||||
|
||||
if (layoutType.Row) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rowWidgets = hoistWidgetHolders(layoutType.Row.rowWidgets);
|
||||
if (layoutType.row) {
|
||||
const rowWidgets = hoistWidgetHolders(layoutType.row.rowWidgets);
|
||||
|
||||
const result: WidgetRow = { rowWidgets };
|
||||
return result;
|
||||
}
|
||||
|
||||
if (layoutType.Section) {
|
||||
const { name } = layoutType.Section;
|
||||
const layout = createWidgetLayout(layoutType.Section.layout);
|
||||
if (layoutType.section) {
|
||||
const { name } = layoutType.section;
|
||||
const layout = createWidgetLayout(layoutType.section.layout);
|
||||
|
||||
const result: WidgetSection = { name, layout };
|
||||
return result;
|
||||
@@ -509,25 +680,104 @@ function createWidgetLayout(widgetLayout: any[]): LayoutGroup[] {
|
||||
throw new Error("Layout row type does not exist");
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function hoistWidgetHolders(widgetHolders: any[]): Widget[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return widgetHolders.map((widgetHolder: any) => {
|
||||
const { widget_id } = widgetHolder;
|
||||
const kind = Object.keys(widgetHolder.widget)[0];
|
||||
const props = widgetHolder.widget[kind];
|
||||
|
||||
return { widget_id, kind, props } as Widget;
|
||||
});
|
||||
// WIDGET LAYOUTS
|
||||
|
||||
export class UpdateDialogDetails extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateDocumentModeLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateDocumentBarLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateToolShelfLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateWorkingColorsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdatePropertyPanelSectionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateLayerTreeOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
|
||||
layout!: LayoutGroup[];
|
||||
}
|
||||
|
||||
export class UpdateMenuBarLayout extends JsMessage {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createMenuLayout(value))
|
||||
// TODO: Replace `any` with correct typing
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Transform(({ value }: { value: any }) => createMenuLayout(value))
|
||||
layout!: MenuColumn[];
|
||||
}
|
||||
|
||||
export type MenuColumn = {
|
||||
label: string;
|
||||
children: MenuEntry[][];
|
||||
};
|
||||
|
||||
export type MenuEntry = {
|
||||
shortcut: string[] | undefined;
|
||||
action: Widget;
|
||||
@@ -536,11 +786,6 @@ export type MenuEntry = {
|
||||
children: undefined | MenuEntry[][];
|
||||
};
|
||||
|
||||
export type MenuColumn = {
|
||||
label: string;
|
||||
children: MenuEntry[][];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function createMenuLayout(menuLayout: any[]): MenuColumn[] {
|
||||
return menuLayout.map((column) => ({ ...column, children: createMenuLayoutRecursive(column.children) }));
|
||||
@@ -556,18 +801,6 @@ function createMenuLayoutRecursive(subLayout: any[][]): MenuEntry[][] {
|
||||
);
|
||||
}
|
||||
|
||||
export class TriggerTextCommit extends JsMessage {}
|
||||
|
||||
export class TriggerTextCopy extends JsMessage {
|
||||
readonly copy_text!: string;
|
||||
}
|
||||
|
||||
export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
|
||||
readonly commit_date!: string;
|
||||
}
|
||||
|
||||
export class TriggerViewportResize extends JsMessage {}
|
||||
|
||||
// `any` is used since the type of the object should be known from the Rust side
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type JSMessageFactory = (data: any, wasm: WasmRawInstance, instance: WasmEditorInstance) => JsMessage;
|
||||
@@ -588,6 +821,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
TriggerIndexedDbWriteDocument,
|
||||
TriggerPaste,
|
||||
TriggerRasterDownload,
|
||||
TriggerRefreshBoundsOfViewports,
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy,
|
||||
TriggerAboutGraphiteLocalizedCommitDate,
|
||||
@@ -612,7 +846,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
UpdateLayerTreeOptionsLayout,
|
||||
UpdateDocumentModeLayout,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateWorkingColors,
|
||||
UpdateWorkingColorsLayout,
|
||||
UpdateMenuBarLayout,
|
||||
} as const;
|
||||
export type JsMessageType = keyof typeof messageMakers;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use crate::helpers::{translate_key, Error};
|
||||
use crate::{EDITOR_HAS_CRASHED, EDITOR_INSTANCES, JS_EDITOR_HANDLES};
|
||||
|
||||
use editor::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION};
|
||||
use editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION};
|
||||
use editor::input::input_preprocessor::ModifierKeys;
|
||||
use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
|
||||
use editor::message_prelude::*;
|
||||
@@ -26,7 +26,7 @@ pub fn set_random_seed(seed: u64) {
|
||||
editor::communication::set_uuid_seed(seed);
|
||||
}
|
||||
|
||||
/// Access a handle to WASM memory
|
||||
/// Provides a handle to access the raw WASM memory
|
||||
#[wasm_bindgen]
|
||||
pub fn wasm_memory() -> JsValue {
|
||||
wasm_bindgen::memory()
|
||||
@@ -105,29 +105,8 @@ impl JsEditorHandle {
|
||||
// the backend from the web frontend.
|
||||
// ========================================================================
|
||||
|
||||
pub fn init_app(&self) {
|
||||
let message = PortfolioMessage::UpdateOpenDocumentsList;
|
||||
self.dispatch(message);
|
||||
|
||||
let message = PortfolioMessage::UpdateDocumentWidgets;
|
||||
self.dispatch(message);
|
||||
|
||||
let message = PropertiesPanelMessage::Init;
|
||||
self.dispatch(message);
|
||||
|
||||
let message = ToolMessage::InitTools;
|
||||
self.dispatch(message);
|
||||
|
||||
// A default font
|
||||
let font = graphene::layers::text_layer::Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into());
|
||||
let message = FrontendMessage::TriggerFontLoad { font, is_default: true };
|
||||
self.dispatch(message);
|
||||
|
||||
let message = MovementMessage::TranslateCanvas { delta: (0., 0.).into() };
|
||||
self.dispatch(message);
|
||||
|
||||
let message = MenuBarMessage::SendLayout;
|
||||
self.dispatch(message);
|
||||
pub fn init_after_frontend_ready(&self) {
|
||||
self.dispatch(Message::Init);
|
||||
}
|
||||
|
||||
/// Displays a dialog with an error message
|
||||
@@ -170,6 +149,16 @@ impl JsEditorHandle {
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
pub fn new_document_dialog(&self) {
|
||||
let message = DialogMessage::RequestNewDocumentDialog;
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
pub fn open_file_upload(&self) {
|
||||
let message = FrontendMessage::TriggerFileUpload;
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
pub fn open_document_file(&self, document_name: String, document_serialized_content: String) {
|
||||
let message = PortfolioMessage::OpenDocumentFile {
|
||||
document_name,
|
||||
@@ -345,18 +334,6 @@ impl JsEditorHandle {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Swap primary and secondary color
|
||||
pub fn swap_colors(&self) {
|
||||
let message = ToolMessage::SwapColors;
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Reset primary and secondary colors to their defaults
|
||||
pub fn reset_colors(&self) {
|
||||
let message = ToolMessage::ResetColors;
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Paste layers from a serialized json representation
|
||||
pub fn paste_serialized_data(&self, data: String) {
|
||||
let message = PortfolioMessage::PasteSerializedData { data };
|
||||
|
||||
@@ -8,16 +8,17 @@ use wasm_bindgen::prelude::*;
|
||||
|
||||
/// When a panic occurs, notify the user and log the error to the JS console before the backend dies
|
||||
pub fn panic_hook(info: &panic::PanicInfo) {
|
||||
let panic_info = info.to_string();
|
||||
let title = "The editor crashed — sorry about that".to_string();
|
||||
let description = "An internal error occurred. Reload the editor to continue. Please report this by filing an issue on GitHub.".to_string();
|
||||
let header = "The editor crashed — sorry about that";
|
||||
let description = "An internal error occurred. Reload the editor to continue. Please report this by filing an issue on GitHub.";
|
||||
|
||||
log::error!("{}", info);
|
||||
|
||||
JS_EDITOR_HANDLES.with(|instances| {
|
||||
instances.borrow_mut().values_mut().for_each(|instance| {
|
||||
instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic {
|
||||
panic_info: panic_info.clone(),
|
||||
title: title.clone(),
|
||||
description: description.clone(),
|
||||
panic_info: info.to_string(),
|
||||
header: header.to_string(),
|
||||
description: description.to_string(),
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user