Sort messages and message handlers

This commit is contained in:
Keavon Chambers
2022-01-12 16:17:40 -08:00
parent e70858884d
commit a535f5c1c1
24 changed files with 834 additions and 781 deletions
Generated
+12
View File
@@ -96,6 +96,7 @@ dependencies = [
"kurbo", "kurbo",
"log", "log",
"rand_chacha", "rand_chacha",
"remain",
"serde", "serde",
"serde_json", "serde_json",
"spin", "spin",
@@ -268,6 +269,17 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "remain"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ba1e78fa68412cb93ef642fd4d20b9a941be49ee9333875ebaf13112673ea7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.9" version = "1.0.9"
+1
View File
@@ -23,6 +23,7 @@ spin = "0.9.2"
kurbo = { git = "https://github.com/GraphiteEditor/kurbo.git", features = [ kurbo = { git = "https://github.com/GraphiteEditor/kurbo.git", features = [
"serde", "serde",
] } ] }
remain = "0.2.2"
[dependencies.graphene] [dependencies.graphene]
path = "../graphene" path = "../graphene"
@@ -10,11 +10,12 @@ use graphene::Operation as DocumentOperation;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::VecDeque; use std::collections::VecDeque;
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Artboard)] #[impl_message(Message, DocumentMessage, Artboard)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum ArtboardMessage { pub enum ArtboardMessage {
DispatchOperation(Box<DocumentOperation>),
AddArtboard { top: f64, left: f64, height: f64, width: f64 }, AddArtboard { top: f64, left: f64, height: f64, width: f64 },
DispatchOperation(Box<DocumentOperation>),
RenderArtboards, RenderArtboards,
} }
@@ -37,14 +38,12 @@ impl ArtboardMessageHandler {
} }
impl MessageHandler<ArtboardMessage, (&mut LayerMetadata, &GrapheneDocument, &InputPreprocessor)> for ArtboardMessageHandler { impl MessageHandler<ArtboardMessage, (&mut LayerMetadata, &GrapheneDocument, &InputPreprocessor)> for ArtboardMessageHandler {
#[remain::check]
fn process_action(&mut self, message: ArtboardMessage, _data: (&mut LayerMetadata, &GrapheneDocument, &InputPreprocessor), responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: ArtboardMessage, _data: (&mut LayerMetadata, &GrapheneDocument, &InputPreprocessor), responses: &mut VecDeque<Message>) {
// let (layer_metadata, document, ipp) = data; // let (layer_metadata, document, ipp) = data;
use ArtboardMessage::*; use ArtboardMessage::*;
#[remain::sorted]
match message { match message {
DispatchOperation(operation) => match self.artboards_graphene_document.handle_operation(&operation) {
Ok(_) => (),
Err(e) => log::error!("Artboard Error: {:?}", e),
},
AddArtboard { top, left, height, width } => { AddArtboard { top, left, height, width } => {
let artboard_id = generate_uuid(); let artboard_id = generate_uuid();
self.artboard_ids.push(artboard_id); self.artboard_ids.push(artboard_id);
@@ -64,6 +63,10 @@ impl MessageHandler<ArtboardMessage, (&mut LayerMetadata, &GrapheneDocument, &In
responses.push_back(DocumentMessage::RenderDocument.into()); responses.push_back(DocumentMessage::RenderDocument.into());
} }
DispatchOperation(operation) => match self.artboards_graphene_document.handle_operation(&operation) {
Ok(_) => (),
Err(e) => log::error!("Artboard Error: {:?}", e),
},
RenderArtboards => { RenderArtboards => {
// Render an infinite canvas if there are no artboards // Render an infinite canvas if there are no artboards
if self.artboard_ids.is_empty() { if self.artboard_ids.is_empty() {
+401 -398
View File
@@ -113,68 +113,69 @@ impl Default for DocumentMessageHandler {
} }
} }
#[remain::sorted]
#[impl_message(Message, PortfolioMessage, Document)] #[impl_message(Message, PortfolioMessage, Document)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum DocumentMessage { pub enum DocumentMessage {
#[child] AbortTransaction,
Movement(MovementMessage), AddSelectedLayers(Vec<Vec<LayerId>>),
#[child] AlignSelectedLayers(AlignAxis, AlignAggregate),
TransformLayers(TransformLayerMessage),
DispatchOperation(Box<DocumentOperation>),
#[child]
Overlay(OverlayMessage),
#[child] #[child]
Artboard(ArtboardMessage), Artboard(ArtboardMessage),
UpdateLayerMetadata { CommitTransaction,
layer_path: Vec<LayerId>, CreateEmptyFolder(Vec<LayerId>),
layer_metadata: LayerMetadata,
},
SetSelectedLayers(Vec<Vec<LayerId>>),
AddSelectedLayers(Vec<Vec<LayerId>>),
SelectAllLayers,
DebugPrintDocument, DebugPrintDocument,
SelectLayer(Vec<LayerId>, bool, bool),
SelectionChanged,
DeselectAllLayers,
DeleteLayer(Vec<LayerId>), DeleteLayer(Vec<LayerId>),
DeleteSelectedLayers, DeleteSelectedLayers,
DuplicateSelectedLayers, DeselectAllLayers,
CreateEmptyFolder(Vec<LayerId>),
SetBlendModeForSelectedLayers(BlendMode),
SetOpacityForSelectedLayers(f64),
RenameLayer(Vec<LayerId>, String),
ToggleLayerVisibility(Vec<LayerId>),
FlipSelectedLayers(FlipAxis),
ToggleLayerExpansion(Vec<LayerId>),
SetLayerExpansion(Vec<LayerId>, bool),
FolderChanged(Vec<LayerId>),
LayerChanged(Vec<LayerId>),
DocumentStructureChanged,
StartTransaction,
RollbackTransaction,
GroupSelectedLayers,
UngroupSelectedLayers,
UngroupLayers(Vec<LayerId>),
AbortTransaction,
CommitTransaction,
ExportDocument,
SaveDocument,
RenderDocument,
DirtyRenderDocument, DirtyRenderDocument,
DirtyRenderDocumentInOutlineView, DirtyRenderDocumentInOutlineView,
SetViewMode(ViewMode), DispatchOperation(Box<DocumentOperation>),
Undo,
Redo,
DocumentHistoryBackward, DocumentHistoryBackward,
DocumentHistoryForward, DocumentHistoryForward,
NudgeSelectedLayers(f64, f64), DocumentStructureChanged,
AlignSelectedLayers(AlignAxis, AlignAggregate), DuplicateSelectedLayers,
ExportDocument,
FlipSelectedLayers(FlipAxis),
FolderChanged(Vec<LayerId>),
GroupSelectedLayers,
LayerChanged(Vec<LayerId>),
#[child]
Movement(MovementMessage),
MoveSelectedLayersTo { MoveSelectedLayersTo {
path: Vec<LayerId>, path: Vec<LayerId>,
insert_index: isize, insert_index: isize,
}, },
NudgeSelectedLayers(f64, f64),
#[child]
Overlay(OverlayMessage),
Redo,
RenameLayer(Vec<LayerId>, String),
RenderDocument,
ReorderSelectedLayers(i32), // relative_position, ReorderSelectedLayers(i32), // relative_position,
RollbackTransaction,
SaveDocument,
SelectAllLayers,
SelectionChanged,
SelectLayer(Vec<LayerId>, bool, bool),
SetBlendModeForSelectedLayers(BlendMode),
SetLayerExpansion(Vec<LayerId>, bool),
SetOpacityForSelectedLayers(f64),
SetSelectedLayers(Vec<Vec<LayerId>>),
SetSnapping(bool), SetSnapping(bool),
SetViewMode(ViewMode),
StartTransaction,
ToggleLayerExpansion(Vec<LayerId>),
ToggleLayerVisibility(Vec<LayerId>),
#[child]
TransformLayers(TransformLayerMessage),
Undo,
UngroupLayers(Vec<LayerId>),
UngroupSelectedLayers,
UpdateLayerMetadata {
layer_path: Vec<LayerId>,
layer_metadata: LayerMetadata,
},
ZoomCanvasToFitAll, ZoomCanvasToFitAll,
} }
@@ -552,243 +553,15 @@ impl DocumentMessageHandler {
} }
impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHandler { impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHandler {
#[remain::check]
fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
use DocumentMessage::*; use DocumentMessage::*;
#[remain::sorted]
match message { match message {
Movement(message) => self.movement_handler.process_action(message, (&self.graphene_document, ipp), responses),
TransformLayers(message) => self
.transform_layer_handler
.process_action(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp), responses),
DeleteLayer(path) => responses.push_front(DocumentOperation::DeleteLayer { path }.into()),
StartTransaction => self.backup(responses),
RollbackTransaction => {
self.rollback(responses).unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
}
AbortTransaction => { AbortTransaction => {
self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e)); self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
} }
CommitTransaction => (),
Overlay(message) => {
self.overlay_message_handler.process_action(
message,
(Self::layer_metadata_mut_no_borrow_self(&mut self.layer_metadata, &[]), &self.graphene_document, ipp),
responses,
);
// responses.push_back(OverlayMessage::RenderOverlays.into());
}
Artboard(message) => {
self.artboard_message_handler.process_action(
message,
(Self::layer_metadata_mut_no_borrow_self(&mut self.layer_metadata, &[]), &self.graphene_document, ipp),
responses,
);
}
ExportDocument => {
// TODO(MFISH33): Add Dialog to select artboards
let bbox = self.document_bounds().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]);
let size = bbox[1] - bbox[0];
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX),
false => self.name.clone() + FILE_EXPORT_SUFFIX,
};
responses.push_back(
FrontendMessage::TriggerFileDownload {
document: format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}">{}{}</svg>"#,
bbox[0].x,
bbox[0].y,
size.x,
size.y,
"\n",
self.graphene_document.render_root(self.view_mode)
),
name,
}
.into(),
)
}
SaveDocument => {
self.set_save_state(true);
responses.push_back(PortfolioMessage::AutoSaveActiveDocument.into());
// Update the save status of the just saved document
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
true => self.name.clone(),
false => self.name.clone() + FILE_SAVE_SUFFIX,
};
responses.push_back(
FrontendMessage::TriggerFileDownload {
document: self.serialize_document(),
name,
}
.into(),
)
}
CreateEmptyFolder(mut path) => {
let id = generate_uuid();
path.push(id);
responses.push_back(DocumentOperation::CreateFolder { path: path.clone() }.into());
responses.push_back(DocumentMessage::SetLayerExpansion(path, true).into());
}
GroupSelectedLayers => {
let mut new_folder_path: Vec<u64> = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec();
// Required for grouping parent folders with their own children
if !new_folder_path.is_empty() && self.selected_layers_contains(&new_folder_path) {
new_folder_path.remove(new_folder_path.len() - 1);
}
new_folder_path.push(generate_uuid());
responses.push_back(PortfolioMessage::Copy(Clipboard::System).into());
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
responses.push_back(DocumentOperation::CreateFolder { path: new_folder_path.clone() }.into());
responses.push_back(DocumentMessage::ToggleLayerExpansion(new_folder_path.clone()).into());
responses.push_back(
PortfolioMessage::PasteIntoFolder {
clipboard: Clipboard::System,
path: new_folder_path.clone(),
insert_index: -1,
}
.into(),
);
responses.push_back(DocumentMessage::SetSelectedLayers(vec![new_folder_path]).into());
}
UngroupLayers(folder_path) => {
// Select all the children of the folder
let to_select = self.graphene_document.folder_children_paths(&folder_path);
let message_buffer = [
// Copy them
DocumentMessage::SetSelectedLayers(to_select).into(),
PortfolioMessage::Copy(Clipboard::System).into(),
// Paste them into the folder above
PortfolioMessage::PasteIntoFolder {
clipboard: Clipboard::System,
path: folder_path[..folder_path.len() - 1].to_vec(),
insert_index: -1,
}
.into(),
// Delete parent folder
DocumentMessage::DeleteLayer(folder_path).into(),
];
// Push these messages in reverse due to push_front
for message in message_buffer.into_iter().rev() {
responses.push_front(message);
}
}
UngroupSelectedLayers => {
responses.push_back(DocumentMessage::StartTransaction.into());
let folder_paths = self.graphene_document.sorted_folders_by_depth(self.selected_layers());
for folder_path in folder_paths {
responses.push_back(DocumentMessage::UngroupLayers(folder_path.to_vec()).into());
}
responses.push_back(DocumentMessage::CommitTransaction.into());
}
SetBlendModeForSelectedLayers(blend_mode) => {
self.backup(responses);
for path in self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
}
}
SetOpacityForSelectedLayers(opacity) => {
self.backup(responses);
let opacity = opacity.clamp(0., 1.);
for path in self.selected_layers().map(|path| path.to_vec()) {
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
}
}
ToggleLayerVisibility(path) => {
responses.push_back(DocumentOperation::ToggleLayerVisibility { path }.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
ToggleLayerExpansion(path) => {
self.layer_metadata_mut(&path).expanded ^= true;
responses.push_back(DocumentStructureChanged.into());
responses.push_back(LayerChanged(path).into())
}
SetLayerExpansion(path, is_expanded) => {
self.layer_metadata_mut(&path).expanded = is_expanded;
responses.push_back(DocumentStructureChanged.into());
responses.push_back(LayerChanged(path).into())
}
SelectionChanged => {
// TODO: Hoist this duplicated code into wider system
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
DeleteSelectedLayers => {
self.backup(responses);
for path in self.selected_layers_without_children() {
responses.push_front(DocumentOperation::DeleteLayer { path: path.to_vec() }.into());
}
responses.push_front(ToolMessage::DocumentIsDirty.into());
}
SetViewMode(mode) => {
self.view_mode = mode;
responses.push_front(DocumentMessage::DirtyRenderDocument.into());
}
DuplicateSelectedLayers => {
self.backup(responses);
for path in self.selected_layers_sorted() {
responses.push_back(DocumentOperation::DuplicateLayer { path: path.to_vec() }.into());
}
}
SelectLayer(selected, ctrl, shift) => {
let mut paths = vec![];
let last_selection_exists = !self.layer_range_selection_reference.is_empty();
// If we have shift pressed and a layer already selected then fill the range
if shift && last_selection_exists {
// Fill the selection range
self.layer_metadata
.iter()
.filter(|(target, _)| self.graphene_document.layer_is_between(target, &selected, &self.layer_range_selection_reference))
.for_each(|(layer_path, _)| {
paths.push(layer_path.clone());
});
} else {
if ctrl {
// Toggle selection when holding ctrl
let layer = self.layer_metadata_mut(&selected);
layer.selected = !layer.selected;
responses.push_back(LayerChanged(selected.clone()).into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
} else {
paths.push(selected.clone());
}
// Set our last selection reference
self.layer_range_selection_reference = selected;
}
// Don't create messages for empty operations
if !paths.is_empty() {
// Add or set our selected layers
if ctrl {
responses.push_front(AddSelectedLayers(paths).into());
} else {
responses.push_front(SetSelectedLayers(paths).into());
}
}
}
UpdateLayerMetadata { layer_path: path, layer_metadata } => {
self.layer_metadata.insert(path, layer_metadata);
}
SetSelectedLayers(paths) => {
let selected = self.layer_metadata.iter_mut().filter(|(_, layer_metadata)| layer_metadata.selected);
selected.for_each(|(path, layer_metadata)| {
layer_metadata.selected = false;
responses.push_back(LayerChanged(path.clone()).into())
});
responses.push_front(AddSelectedLayers(paths).into());
}
AddSelectedLayers(paths) => { AddSelectedLayers(paths) => {
for path in paths { for path in paths {
responses.extend(self.select_layer(&path)); responses.extend(self.select_layer(&path));
@@ -797,44 +570,83 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_back(FolderChanged(Vec::new()).into()); responses.push_back(FolderChanged(Vec::new()).into());
responses.push_back(ToolMessage::DocumentIsDirty.into()); responses.push_back(ToolMessage::DocumentIsDirty.into());
} }
AlignSelectedLayers(axis, aggregate) => {
self.backup(responses);
let (paths, boxes): (Vec<_>, Vec<_>) = self
.selected_layers()
.filter_map(|path| self.graphene_document.viewport_bounding_box(path).ok()?.map(|b| (path, b)))
.unzip();
let axis = match axis {
AlignAxis::X => DVec2::X,
AlignAxis::Y => DVec2::Y,
};
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5);
if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers()) {
let aggregated = match aggregate {
AlignAggregate::Min => combined_box[0],
AlignAggregate::Max => combined_box[1],
AlignAggregate::Center => lerp(&combined_box),
AlignAggregate::Average => boxes.iter().map(|b| lerp(b)).reduce(|a, b| a + b).map(|b| b / boxes.len() as f64).unwrap(),
};
for (path, bbox) in paths.into_iter().zip(boxes) {
let center = match aggregate {
AlignAggregate::Min => bbox[0],
AlignAggregate::Max => bbox[1],
_ => lerp(&bbox),
};
let translation = (aggregated - center) * axis;
responses.push_back(
DocumentOperation::TransformLayerInViewport {
path: path.to_vec(),
transform: DAffine2::from_translation(translation).to_cols_array(),
}
.into(),
);
}
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
}
Artboard(message) => {
self.artboard_message_handler.process_action(
message,
(Self::layer_metadata_mut_no_borrow_self(&mut self.layer_metadata, &[]), &self.graphene_document, ipp),
responses,
);
}
CommitTransaction => (),
CreateEmptyFolder(mut path) => {
let id = generate_uuid();
path.push(id);
responses.push_back(DocumentOperation::CreateFolder { path: path.clone() }.into());
responses.push_back(DocumentMessage::SetLayerExpansion(path, true).into());
}
DebugPrintDocument => { DebugPrintDocument => {
log::debug!("{:#?}\n{:#?}", self.graphene_document, self.layer_metadata); log::debug!("{:#?}\n{:#?}", self.graphene_document, self.layer_metadata);
} }
SelectAllLayers => { DeleteLayer(path) => responses.push_front(DocumentOperation::DeleteLayer { path }.into()),
let all_layer_paths = self.all_layers(); DeleteSelectedLayers => {
responses.push_front(SetSelectedLayers(all_layer_paths.map(|path| path.to_vec()).collect()).into()); self.backup(responses);
for path in self.selected_layers_without_children() {
responses.push_front(DocumentOperation::DeleteLayer { path: path.to_vec() }.into());
}
responses.push_front(ToolMessage::DocumentIsDirty.into());
} }
DeselectAllLayers => { DeselectAllLayers => {
responses.push_front(SetSelectedLayers(vec![]).into()); responses.push_front(SetSelectedLayers(vec![]).into());
self.layer_range_selection_reference.clear(); self.layer_range_selection_reference.clear();
} }
DocumentHistoryBackward => self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e)), DirtyRenderDocument => {
DocumentHistoryForward => self.redo(responses).unwrap_or_else(|e| log::warn!("{}", e)), // Mark all non-overlay caches as dirty
Undo => { GrapheneDocument::visit_all_shapes(&mut self.graphene_document.root, &mut |_| {});
responses.push_back(SelectMessage::Abort.into());
responses.push_back(DocumentHistoryBackward.into()); responses.push_back(DocumentMessage::RenderDocument.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(RenderDocument.into());
responses.push_back(FolderChanged(vec![]).into());
} }
Redo => { DirtyRenderDocumentInOutlineView => {
responses.push_back(SelectMessage::Abort.into()); if self.view_mode == ViewMode::Outline {
responses.push_back(DocumentHistoryForward.into()); responses.push_front(DocumentMessage::DirtyRenderDocument.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(RenderDocument.into());
responses.push_back(FolderChanged(vec![]).into());
}
FolderChanged(path) => {
let _ = self.graphene_document.render_root(self.view_mode);
responses.extend([LayerChanged(path).into(), DocumentStructureChanged.into()]);
}
DocumentStructureChanged => {
let data_buffer: RawBuffer = self.serialize_root().into();
responses.push_back(FrontendMessage::DisplayDocumentLayerTreeStructure { data_buffer }.into())
}
LayerChanged(path) => {
if let Ok(layer_entry) = self.layer_panel_entry(path) {
responses.push_back(FrontendMessage::UpdateDocumentLayer { data: layer_entry }.into());
} }
} }
DispatchOperation(op) => match self.graphene_document.handle_operation(&op) { DispatchOperation(op) => match self.graphene_document.handle_operation(&op) {
@@ -864,6 +676,144 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
Err(e) => log::error!("DocumentError: {:?}", e), Err(e) => log::error!("DocumentError: {:?}", e),
Ok(_) => (), Ok(_) => (),
}, },
DocumentHistoryBackward => self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e)),
DocumentHistoryForward => self.redo(responses).unwrap_or_else(|e| log::warn!("{}", e)),
DocumentStructureChanged => {
let data_buffer: RawBuffer = self.serialize_root().into();
responses.push_back(FrontendMessage::DisplayDocumentLayerTreeStructure { data_buffer }.into())
}
DuplicateSelectedLayers => {
self.backup(responses);
for path in self.selected_layers_sorted() {
responses.push_back(DocumentOperation::DuplicateLayer { path: path.to_vec() }.into());
}
}
ExportDocument => {
// TODO(MFISH33): Add Dialog to select artboards
let bbox = self.document_bounds().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]);
let size = bbox[1] - bbox[0];
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX),
false => self.name.clone() + FILE_EXPORT_SUFFIX,
};
responses.push_back(
FrontendMessage::TriggerFileDownload {
document: format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}">{}{}</svg>"#,
bbox[0].x,
bbox[0].y,
size.x,
size.y,
"\n",
self.graphene_document.render_root(self.view_mode)
),
name,
}
.into(),
)
}
FlipSelectedLayers(axis) => {
self.backup(responses);
let scale = match axis {
FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.),
};
if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers()) {
let center = (max + min) / 2.;
let bbox_trans = DAffine2::from_translation(-center);
for path in self.selected_layers() {
responses.push_back(
DocumentOperation::TransformLayerInScope {
path: path.to_vec(),
transform: DAffine2::from_scale(scale).to_cols_array(),
scope: bbox_trans.to_cols_array(),
}
.into(),
);
}
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
}
FolderChanged(path) => {
let _ = self.graphene_document.render_root(self.view_mode);
responses.extend([LayerChanged(path).into(), DocumentStructureChanged.into()]);
}
GroupSelectedLayers => {
let mut new_folder_path: Vec<u64> = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec();
// Required for grouping parent folders with their own children
if !new_folder_path.is_empty() && self.selected_layers_contains(&new_folder_path) {
new_folder_path.remove(new_folder_path.len() - 1);
}
new_folder_path.push(generate_uuid());
responses.push_back(PortfolioMessage::Copy(Clipboard::System).into());
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
responses.push_back(DocumentOperation::CreateFolder { path: new_folder_path.clone() }.into());
responses.push_back(DocumentMessage::ToggleLayerExpansion(new_folder_path.clone()).into());
responses.push_back(
PortfolioMessage::PasteIntoFolder {
clipboard: Clipboard::System,
path: new_folder_path.clone(),
insert_index: -1,
}
.into(),
);
responses.push_back(DocumentMessage::SetSelectedLayers(vec![new_folder_path]).into());
}
LayerChanged(path) => {
if let Ok(layer_entry) = self.layer_panel_entry(path) {
responses.push_back(FrontendMessage::UpdateDocumentLayer { data: layer_entry }.into());
}
}
Movement(message) => self.movement_handler.process_action(message, (&self.graphene_document, ipp), responses),
MoveSelectedLayersTo { path, insert_index } => {
let layers = self.selected_layers().collect::<Vec<_>>();
// Trying to insert into self.
if layers.iter().any(|layer| path.starts_with(layer)) {
return;
}
let insert_index = self.update_insert_index(&layers, &path, insert_index).unwrap();
responses.push_back(PortfolioMessage::Copy(Clipboard::System).into());
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
responses.push_back(
PortfolioMessage::PasteIntoFolder {
clipboard: Clipboard::System,
path,
insert_index,
}
.into(),
);
}
NudgeSelectedLayers(x, y) => {
self.backup(responses);
for path in self.selected_layers().map(|path| path.to_vec()) {
let operation = DocumentOperation::TransformLayerInViewport {
path,
transform: DAffine2::from_translation((x, y).into()).to_cols_array(),
};
responses.push_back(operation.into());
}
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
Overlay(message) => {
self.overlay_message_handler.process_action(
message,
(Self::layer_metadata_mut_no_borrow_self(&mut self.layer_metadata, &[]), &self.graphene_document, ipp),
responses,
);
// responses.push_back(OverlayMessage::RenderOverlays.into());
}
Redo => {
responses.push_back(SelectMessage::Abort.into());
responses.push_back(DocumentHistoryForward.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(RenderDocument.into());
responses.push_back(FolderChanged(vec![]).into());
}
RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()),
RenderDocument => { RenderDocument => {
responses.push_back( responses.push_back(
FrontendMessage::UpdateDocumentArtwork { FrontendMessage::UpdateDocumentArtwork {
@@ -909,47 +859,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
.into(), .into(),
); );
} }
DirtyRenderDocument => {
// Mark all non-overlay caches as dirty
GrapheneDocument::visit_all_shapes(&mut self.graphene_document.root, &mut |_| {});
responses.push_back(DocumentMessage::RenderDocument.into());
}
DirtyRenderDocumentInOutlineView => {
if self.view_mode == ViewMode::Outline {
responses.push_front(DocumentMessage::DirtyRenderDocument.into());
}
}
NudgeSelectedLayers(x, y) => {
self.backup(responses);
for path in self.selected_layers().map(|path| path.to_vec()) {
let operation = DocumentOperation::TransformLayerInViewport {
path,
transform: DAffine2::from_translation((x, y).into()).to_cols_array(),
};
responses.push_back(operation.into());
}
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
MoveSelectedLayersTo { path, insert_index } => {
let layers = self.selected_layers().collect::<Vec<_>>();
// Trying to insert into self.
if layers.iter().any(|layer| path.starts_with(layer)) {
return;
}
let insert_index = self.update_insert_index(&layers, &path, insert_index).unwrap();
responses.push_back(PortfolioMessage::Copy(Clipboard::System).into());
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
responses.push_back(
PortfolioMessage::PasteIntoFolder {
clipboard: Clipboard::System,
path,
insert_index,
}
.into(),
);
}
ReorderSelectedLayers(relative_position) => { ReorderSelectedLayers(relative_position) => {
self.backup(responses); self.backup(responses);
let all_layer_paths = self.all_layers_sorted(); let all_layer_paths = self.all_layers_sorted();
@@ -981,69 +890,163 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
} }
} }
FlipSelectedLayers(axis) => { RollbackTransaction => {
self.backup(responses); self.rollback(responses).unwrap_or_else(|e| log::warn!("{}", e));
let scale = match axis { responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.),
};
if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers()) {
let center = (max + min) / 2.;
let bbox_trans = DAffine2::from_translation(-center);
for path in self.selected_layers() {
responses.push_back(
DocumentOperation::TransformLayerInScope {
path: path.to_vec(),
transform: DAffine2::from_scale(scale).to_cols_array(),
scope: bbox_trans.to_cols_array(),
}
.into(),
);
}
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
} }
AlignSelectedLayers(axis, aggregate) => { SaveDocument => {
self.backup(responses); self.set_save_state(true);
let (paths, boxes): (Vec<_>, Vec<_>) = self responses.push_back(PortfolioMessage::AutoSaveActiveDocument.into());
.selected_layers() // Update the save status of the just saved document
.filter_map(|path| self.graphene_document.viewport_bounding_box(path).ok()?.map(|b| (path, b))) responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
.unzip();
let axis = match axis { let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
AlignAxis::X => DVec2::X, true => self.name.clone(),
AlignAxis::Y => DVec2::Y, false => self.name.clone() + FILE_SAVE_SUFFIX,
}; };
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5); responses.push_back(
if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers()) { FrontendMessage::TriggerFileDownload {
let aggregated = match aggregate { document: self.serialize_document(),
AlignAggregate::Min => combined_box[0], name,
AlignAggregate::Max => combined_box[1], }
AlignAggregate::Center => lerp(&combined_box), .into(),
AlignAggregate::Average => boxes.iter().map(|b| lerp(b)).reduce(|a, b| a + b).map(|b| b / boxes.len() as f64).unwrap(), )
}; }
for (path, bbox) in paths.into_iter().zip(boxes) { SelectAllLayers => {
let center = match aggregate { let all_layer_paths = self.all_layers();
AlignAggregate::Min => bbox[0], responses.push_front(SetSelectedLayers(all_layer_paths.map(|path| path.to_vec()).collect()).into());
AlignAggregate::Max => bbox[1], }
_ => lerp(&bbox), SelectionChanged => {
}; // TODO: Hoist this duplicated code into wider system
let translation = (aggregated - center) * axis; responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back( }
DocumentOperation::TransformLayerInViewport { SelectLayer(selected, ctrl, shift) => {
path: path.to_vec(), let mut paths = vec![];
transform: DAffine2::from_translation(translation).to_cols_array(), let last_selection_exists = !self.layer_range_selection_reference.is_empty();
}
.into(), // If we have shift pressed and a layer already selected then fill the range
); if shift && last_selection_exists {
// Fill the selection range
self.layer_metadata
.iter()
.filter(|(target, _)| self.graphene_document.layer_is_between(target, &selected, &self.layer_range_selection_reference))
.for_each(|(layer_path, _)| {
paths.push(layer_path.clone());
});
} else {
if ctrl {
// Toggle selection when holding ctrl
let layer = self.layer_metadata_mut(&selected);
layer.selected = !layer.selected;
responses.push_back(LayerChanged(selected.clone()).into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
} else {
paths.push(selected.clone());
}
// Set our last selection reference
self.layer_range_selection_reference = selected;
}
// Don't create messages for empty operations
if !paths.is_empty() {
// Add or set our selected layers
if ctrl {
responses.push_front(AddSelectedLayers(paths).into());
} else {
responses.push_front(SetSelectedLayers(paths).into());
} }
responses.push_back(ToolMessage::DocumentIsDirty.into());
} }
} }
RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()), SetBlendModeForSelectedLayers(blend_mode) => {
self.backup(responses);
for path in self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
}
}
SetLayerExpansion(path, is_expanded) => {
self.layer_metadata_mut(&path).expanded = is_expanded;
responses.push_back(DocumentStructureChanged.into());
responses.push_back(LayerChanged(path).into())
}
SetOpacityForSelectedLayers(opacity) => {
self.backup(responses);
let opacity = opacity.clamp(0., 1.);
for path in self.selected_layers().map(|path| path.to_vec()) {
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
}
}
SetSelectedLayers(paths) => {
let selected = self.layer_metadata.iter_mut().filter(|(_, layer_metadata)| layer_metadata.selected);
selected.for_each(|(path, layer_metadata)| {
layer_metadata.selected = false;
responses.push_back(LayerChanged(path.clone()).into())
});
responses.push_front(AddSelectedLayers(paths).into());
}
SetSnapping(new_status) => { SetSnapping(new_status) => {
self.snapping_enabled = new_status; self.snapping_enabled = new_status;
} }
SetViewMode(mode) => {
self.view_mode = mode;
responses.push_front(DocumentMessage::DirtyRenderDocument.into());
}
StartTransaction => self.backup(responses),
ToggleLayerExpansion(path) => {
self.layer_metadata_mut(&path).expanded ^= true;
responses.push_back(DocumentStructureChanged.into());
responses.push_back(LayerChanged(path).into())
}
ToggleLayerVisibility(path) => {
responses.push_back(DocumentOperation::ToggleLayerVisibility { path }.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
TransformLayers(message) => self
.transform_layer_handler
.process_action(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp), responses),
Undo => {
responses.push_back(SelectMessage::Abort.into());
responses.push_back(DocumentHistoryBackward.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(RenderDocument.into());
responses.push_back(FolderChanged(vec![]).into());
}
UngroupLayers(folder_path) => {
// Select all the children of the folder
let to_select = self.graphene_document.folder_children_paths(&folder_path);
let message_buffer = [
// Copy them
DocumentMessage::SetSelectedLayers(to_select).into(),
PortfolioMessage::Copy(Clipboard::System).into(),
// Paste them into the folder above
PortfolioMessage::PasteIntoFolder {
clipboard: Clipboard::System,
path: folder_path[..folder_path.len() - 1].to_vec(),
insert_index: -1,
}
.into(),
// Delete parent folder
DocumentMessage::DeleteLayer(folder_path).into(),
];
// Push these messages in reverse due to push_front
for message in message_buffer.into_iter().rev() {
responses.push_front(message);
}
}
UngroupSelectedLayers => {
responses.push_back(DocumentMessage::StartTransaction.into());
let folder_paths = self.graphene_document.sorted_folders_by_depth(self.selected_layers());
for folder_path in folder_paths {
responses.push_back(DocumentMessage::UngroupLayers(folder_path.to_vec()).into());
}
responses.push_back(DocumentMessage::CommitTransaction.into());
}
UpdateLayerMetadata { layer_path: path, layer_metadata } => {
self.layer_metadata.insert(path, layer_metadata);
}
ZoomCanvasToFitAll => { ZoomCanvasToFitAll => {
if let Some(bounds) = self.document_bounds() { if let Some(bounds) = self.document_bounds() {
responses.push_back( responses.push_back(
+108 -105
View File
@@ -14,38 +14,39 @@ use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::VecDeque; use std::collections::VecDeque;
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Movement)] #[impl_message(Message, DocumentMessage, Movement)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum MovementMessage { pub enum MovementMessage {
DecreaseCanvasZoom {
center_on_mouse: bool,
},
FitViewportToBounds {
bounds: [DVec2; 2],
padding_scale_factor: Option<f32>,
prevent_zoom_past_100: bool,
},
IncreaseCanvasZoom {
center_on_mouse: bool,
},
MouseMove { MouseMove {
snap_angle: Key, snap_angle: Key,
wait_for_snap_angle_release: bool, wait_for_snap_angle_release: bool,
snap_zoom: Key, snap_zoom: Key,
zoom_from_viewport: Option<DVec2>, zoom_from_viewport: Option<DVec2>,
}, },
RotateCanvasBegin,
SetCanvasRotation(f64),
SetCanvasZoom(f64),
TransformCanvasEnd,
TranslateCanvas(DVec2),
TranslateCanvasBegin, TranslateCanvasBegin,
TranslateCanvasByViewportFraction(DVec2),
WheelCanvasTranslate { WheelCanvasTranslate {
use_y_as_x: bool, use_y_as_x: bool,
}, },
RotateCanvasBegin,
ZoomCanvasBegin,
TransformCanvasEnd,
SetCanvasRotation(f64),
SetCanvasZoom(f64),
IncreaseCanvasZoom {
center_on_mouse: bool,
},
DecreaseCanvasZoom {
center_on_mouse: bool,
},
WheelCanvasZoom, WheelCanvasZoom,
FitViewportToBounds { ZoomCanvasBegin,
bounds: [DVec2; 2],
padding_scale_factor: Option<f32>,
prevent_zoom_past_100: bool,
},
TranslateCanvas(DVec2),
TranslateCanvasByViewportFraction(DVec2),
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -148,32 +149,54 @@ impl MovementMessageHandler {
} }
impl MessageHandler<MovementMessage, (&Document, &InputPreprocessor)> for MovementMessageHandler { impl MessageHandler<MovementMessage, (&Document, &InputPreprocessor)> for MovementMessageHandler {
#[remain::check]
fn process_action(&mut self, message: MovementMessage, data: (&Document, &InputPreprocessor), responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: MovementMessage, data: (&Document, &InputPreprocessor), responses: &mut VecDeque<Message>) {
let (document, ipp) = data; let (document, ipp) = data;
use MovementMessage::*; use MovementMessage::*;
#[remain::sorted]
match message { match message {
TranslateCanvasBegin => { DecreaseCanvasZoom { center_on_mouse } => {
self.panning = true; let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < self.zoom).unwrap_or(&self.zoom);
self.mouse_position = ipp.mouse.position; if center_on_mouse {
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position));
}
responses.push_back(SetCanvasZoom(new_scale).into());
} }
RotateCanvasBegin => { FitViewportToBounds {
self.tilting = true; bounds: [bounds_corner_a, bounds_corner_b],
self.mouse_position = ipp.mouse.position; padding_scale_factor,
} prevent_zoom_past_100,
ZoomCanvasBegin => { } => {
self.zooming = true; let pos1 = document.root.transform.inverse().transform_point2(bounds_corner_a);
self.mouse_position = ipp.mouse.position; let pos2 = document.root.transform.inverse().transform_point2(bounds_corner_b);
} let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO);
TransformCanvasEnd => { let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_bounds.size());
self.tilt = self.snapped_angle();
self.zoom = self.snapped_scale(); let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5);
let size = (pos2 - pos1) / (v2 - v1);
let size = 1. / size;
let new_scale = size.min_element();
self.pan += center;
self.zoom *= new_scale;
self.zoom /= padding_scale_factor.unwrap_or(1.) as f64;
if self.zoom > 1. && prevent_zoom_past_100 {
self.zoom = 1.
}
responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.zoom }.into());
responses.push_back(ToolMessage::DocumentIsDirty.into()); responses.push_back(ToolMessage::DocumentIsDirty.into());
self.snap_tilt = false; responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
self.snap_tilt_released = false; self.create_document_transform(&ipp.viewport_bounds, responses);
self.snap_zoom = false; }
self.panning = false; IncreaseCanvasZoom { center_on_mouse } => {
self.tilting = false; let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > self.zoom).unwrap_or(&self.zoom);
self.zooming = false; if center_on_mouse {
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position));
}
responses.push_back(SetCanvasZoom(new_scale).into());
} }
MouseMove { MouseMove {
snap_angle, snap_angle,
@@ -233,6 +256,16 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessor)> for Moveme
} }
self.mouse_position = ipp.mouse.position; self.mouse_position = ipp.mouse.position;
} }
RotateCanvasBegin => {
self.tilting = true;
self.mouse_position = ipp.mouse.position;
}
SetCanvasRotation(new_radians) => {
self.tilt = new_radians;
self.create_document_transform(&ipp.viewport_bounds, responses);
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(FrontendMessage::UpdateCanvasRotation { angle_radians: self.snapped_angle() }.into());
}
SetCanvasZoom(new) => { SetCanvasZoom(new) => {
self.zoom = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); self.zoom = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.snapped_scale() }.into()); responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.snapped_scale() }.into());
@@ -240,19 +273,41 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessor)> for Moveme
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
self.create_document_transform(&ipp.viewport_bounds, responses); self.create_document_transform(&ipp.viewport_bounds, responses);
} }
IncreaseCanvasZoom { center_on_mouse } => { TransformCanvasEnd => {
let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > self.zoom).unwrap_or(&self.zoom); self.tilt = self.snapped_angle();
if center_on_mouse { self.zoom = self.snapped_scale();
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position)); responses.push_back(ToolMessage::DocumentIsDirty.into());
} self.snap_tilt = false;
responses.push_back(SetCanvasZoom(new_scale).into()); self.snap_tilt_released = false;
self.snap_zoom = false;
self.panning = false;
self.tilting = false;
self.zooming = false;
} }
DecreaseCanvasZoom { center_on_mouse } => { TranslateCanvas(delta) => {
let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < self.zoom).unwrap_or(&self.zoom); let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
if center_on_mouse {
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position)); self.pan += transformed_delta;
} responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(SetCanvasZoom(new_scale).into()); self.create_document_transform(&ipp.viewport_bounds, responses);
}
TranslateCanvasBegin => {
self.panning = true;
self.mouse_position = ipp.mouse.position;
}
TranslateCanvasByViewportFraction(delta) => {
let transformed_delta = document.root.transform.inverse().transform_vector2(delta * ipp.viewport_bounds.size());
self.pan += transformed_delta;
responses.push_back(ToolMessage::DocumentIsDirty.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
WheelCanvasTranslate { use_y_as_x } => {
let delta = match use_y_as_x {
false => -ipp.mouse.scroll_delta.as_dvec2(),
true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(),
} * VIEWPORT_SCROLL_RATE;
responses.push_back(TranslateCanvas(delta).into());
} }
WheelCanvasZoom => { WheelCanvasZoom => {
let scroll = ipp.mouse.scroll_delta.scroll_delta(); let scroll = ipp.mouse.scroll_delta.scroll_delta();
@@ -264,61 +319,9 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessor)> for Moveme
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, ipp.mouse.position)); responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, ipp.mouse.position));
responses.push_back(SetCanvasZoom(self.zoom * zoom_factor).into()); responses.push_back(SetCanvasZoom(self.zoom * zoom_factor).into());
} }
WheelCanvasTranslate { use_y_as_x } => { ZoomCanvasBegin => {
let delta = match use_y_as_x { self.zooming = true;
false => -ipp.mouse.scroll_delta.as_dvec2(), self.mouse_position = ipp.mouse.position;
true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(),
} * VIEWPORT_SCROLL_RATE;
responses.push_back(TranslateCanvas(delta).into());
}
SetCanvasRotation(new_radians) => {
self.tilt = new_radians;
self.create_document_transform(&ipp.viewport_bounds, responses);
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(FrontendMessage::UpdateCanvasRotation { angle_radians: self.snapped_angle() }.into());
}
FitViewportToBounds {
bounds: [bounds_corner_a, bounds_corner_b],
padding_scale_factor,
prevent_zoom_past_100,
} => {
let pos1 = document.root.transform.inverse().transform_point2(bounds_corner_a);
let pos2 = document.root.transform.inverse().transform_point2(bounds_corner_b);
let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO);
let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_bounds.size());
let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5);
let size = (pos2 - pos1) / (v2 - v1);
let size = 1. / size;
let new_scale = size.min_element();
self.pan += center;
self.zoom *= new_scale;
self.zoom /= padding_scale_factor.unwrap_or(1.) as f64;
if self.zoom > 1. && prevent_zoom_past_100 {
self.zoom = 1.
}
responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.zoom }.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
TranslateCanvas(delta) => {
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
self.pan += transformed_delta;
responses.push_back(ToolMessage::DocumentIsDirty.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
TranslateCanvasByViewportFraction(delta) => {
let transformed_delta = document.root.transform.inverse().transform_vector2(delta * ipp.viewport_bounds.size());
self.pan += transformed_delta;
responses.push_back(ToolMessage::DocumentIsDirty.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
} }
} }
} }
@@ -9,11 +9,12 @@ use graphene::document::Document as GrapheneDocument;
use graphene::layers::style::ViewMode; use graphene::layers::style::ViewMode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Overlay)] #[impl_message(Message, DocumentMessage, Overlay)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum OverlayMessage { pub enum OverlayMessage {
DispatchOperation(Box<DocumentOperation>),
ClearAllOverlays, ClearAllOverlays,
DispatchOperation(Box<DocumentOperation>),
} }
impl From<DocumentOperation> for OverlayMessage { impl From<DocumentOperation> for OverlayMessage {
@@ -28,15 +29,17 @@ pub struct OverlayMessageHandler {
} }
impl MessageHandler<OverlayMessage, (&mut LayerMetadata, &Document, &InputPreprocessor)> for OverlayMessageHandler { impl MessageHandler<OverlayMessage, (&mut LayerMetadata, &Document, &InputPreprocessor)> for OverlayMessageHandler {
#[remain::check]
fn process_action(&mut self, message: OverlayMessage, _data: (&mut LayerMetadata, &Document, &InputPreprocessor), responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: OverlayMessage, _data: (&mut LayerMetadata, &Document, &InputPreprocessor), responses: &mut VecDeque<Message>) {
// let (layer_metadata, document, ipp) = data; // let (layer_metadata, document, ipp) = data;
use OverlayMessage::*; use OverlayMessage::*;
#[remain::sorted]
match message { match message {
ClearAllOverlays => todo!(),
DispatchOperation(operation) => match self.overlays_graphene_document.handle_operation(&operation) { DispatchOperation(operation) => match self.overlays_graphene_document.handle_operation(&operation) {
Ok(_) => (), Ok(_) => (),
Err(e) => log::error!("OverlayError: {:?}", e), Err(e) => log::error!("OverlayError: {:?}", e),
}, },
ClearAllOverlays => todo!(),
} }
// Render overlays // Render overlays
+115 -112
View File
@@ -21,40 +21,41 @@ pub enum Clipboard {
const CLIPBOARD_COUNT: u8 = Clipboard::_ClipboardCount as u8; const CLIPBOARD_COUNT: u8 = Clipboard::_ClipboardCount as u8;
#[remain::sorted]
#[impl_message(Message, Portfolio)] #[impl_message(Message, Portfolio)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum PortfolioMessage { pub enum PortfolioMessage {
AutoSaveActiveDocument,
AutoSaveDocument(u64),
CloseActiveDocumentWithConfirmation,
CloseAllDocuments,
CloseAllDocumentsWithConfirmation,
CloseDocument(u64),
CloseDocumentWithConfirmation(u64),
Copy(Clipboard), Copy(Clipboard),
Cut(Clipboard), Cut(Clipboard),
PasteIntoFolder {
clipboard: Clipboard,
path: Vec<LayerId>,
insert_index: isize,
},
Paste(Clipboard),
SelectDocument(u64),
CloseDocument(u64),
#[child] #[child]
Document(DocumentMessage), Document(DocumentMessage),
CloseActiveDocumentWithConfirmation,
CloseDocumentWithConfirmation(u64),
CloseAllDocumentsWithConfirmation,
CloseAllDocuments,
RequestAboutGraphiteDialog,
NewDocument, NewDocument,
NextDocument,
OpenDocument, OpenDocument,
OpenDocumentFile(String, String),
OpenDocumentFileWithId { OpenDocumentFileWithId {
document: String, document: String,
document_name: String, document_name: String,
document_id: u64, document_id: u64,
document_is_saved: bool, document_is_saved: bool,
}, },
OpenDocumentFile(String, String), Paste(Clipboard),
UpdateOpenDocumentsList, PasteIntoFolder {
AutoSaveDocument(u64), clipboard: Clipboard,
AutoSaveActiveDocument, path: Vec<LayerId>,
NextDocument, insert_index: isize,
},
PrevDocument, PrevDocument,
RequestAboutGraphiteDialog,
SelectDocument(u64),
UpdateOpenDocumentsList,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -176,44 +177,31 @@ impl Default for PortfolioMessageHandler {
} }
impl MessageHandler<PortfolioMessage, &InputPreprocessor> for PortfolioMessageHandler { impl MessageHandler<PortfolioMessage, &InputPreprocessor> for PortfolioMessageHandler {
#[remain::check]
fn process_action(&mut self, message: PortfolioMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: PortfolioMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
use DocumentMessage::*; use DocumentMessage::*;
use PortfolioMessage::*; use PortfolioMessage::*;
#[remain::sorted]
match message { match message {
RequestAboutGraphiteDialog => { AutoSaveActiveDocument => responses.push_back(PortfolioMessage::AutoSaveDocument(self.active_document_id).into()),
responses.push_back(FrontendMessage::DisplayDialogAboutGraphite.into()); AutoSaveDocument(id) => {
} let document = self.documents.get(&id).unwrap();
Document(message) => self.active_document_mut().process_action(message, ipp, responses), responses.push_back(
SelectDocument(id) => { FrontendMessage::TriggerIndexedDbWriteDocument {
let active_document = self.active_document(); document: document.serialize_document(),
if !active_document.is_saved() { details: FrontendDocumentDetails {
responses.push_back(PortfolioMessage::AutoSaveDocument(self.active_document_id).into()); is_saved: document.is_saved(),
} id,
self.active_document_id = id; name: document.name.clone(),
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id: id }.into()); },
responses.push_back(RenderDocument.into()); version: GRAPHITE_DOCUMENT_VERSION.to_string(),
responses.push_back(DocumentMessage::DocumentStructureChanged.into()); }
for layer in self.active_document().layer_metadata.keys() { .into(),
responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into()); )
}
responses.push_back(ToolMessage::DocumentIsDirty.into());
} }
CloseActiveDocumentWithConfirmation => { CloseActiveDocumentWithConfirmation => {
responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation(self.active_document_id).into()); responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation(self.active_document_id).into());
} }
CloseDocumentWithConfirmation(id) => {
let target_document = self.documents.get(&id).unwrap();
if target_document.is_saved() {
responses.push_back(PortfolioMessage::CloseDocument(id).into());
} else {
responses.push_back(FrontendMessage::DisplayConfirmationToCloseDocument { document_id: id }.into());
// Select the document being closed
responses.push_back(PortfolioMessage::SelectDocument(id).into());
}
}
CloseAllDocumentsWithConfirmation => {
responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into());
}
CloseAllDocuments => { CloseAllDocuments => {
// Empty the list of internal document data // Empty the list of internal document data
self.documents.clear(); self.documents.clear();
@@ -222,6 +210,9 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessor> for PortfolioMessageHa
// Create a new blank document // Create a new blank document
responses.push_back(NewDocument.into()); responses.push_back(NewDocument.into());
} }
CloseAllDocumentsWithConfirmation => {
responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into());
}
CloseDocument(id) => { CloseDocument(id) => {
let document_index = self.document_index(id); let document_index = self.document_index(id);
self.documents.remove(&id); self.documents.remove(&id);
@@ -267,6 +258,37 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessor> for PortfolioMessageHa
responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into()); responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into());
} }
} }
CloseDocumentWithConfirmation(id) => {
let target_document = self.documents.get(&id).unwrap();
if target_document.is_saved() {
responses.push_back(PortfolioMessage::CloseDocument(id).into());
} else {
responses.push_back(FrontendMessage::DisplayConfirmationToCloseDocument { document_id: id }.into());
// Select the document being closed
responses.push_back(PortfolioMessage::SelectDocument(id).into());
}
}
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_buffer = &mut self.copy_buffer;
copy_buffer[clipboard as usize].clear();
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) => {
copy_buffer[clipboard as usize].push(CopyBufferEntry { layer, layer_metadata });
}
(Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e),
}
}
}
Cut(clipboard) => {
responses.push_back(Copy(clipboard).into());
responses.push_back(DeleteSelectedLayers.into());
}
Document(message) => self.active_document_mut().process_action(message, ipp, responses),
NewDocument => { NewDocument => {
let name = self.generate_new_document_name(); let name = self.generate_new_document_name();
let new_document = DocumentMessageHandler::with_name(name, ipp); let new_document = DocumentMessageHandler::with_name(name, ipp);
@@ -274,6 +296,12 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessor> for PortfolioMessageHa
self.active_document_id = document_id; self.active_document_id = document_id;
self.load_document(new_document, document_id, false, responses); self.load_document(new_document, document_id, false, responses);
} }
NextDocument => {
let current_index = self.document_index(self.active_document_id);
let next_index = (current_index + 1) % self.document_ids.len();
let next_id = self.document_ids[next_index];
responses.push_back(PortfolioMessage::SelectDocument(next_id).into());
}
OpenDocument => { OpenDocument => {
responses.push_back(FrontendMessage::TriggerFileUpload.into()); responses.push_back(FrontendMessage::TriggerFileUpload.into());
} }
@@ -309,70 +337,6 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessor> for PortfolioMessageHa
), ),
} }
} }
UpdateOpenDocumentsList => {
// Send the list of document tab names
let open_documents = self
.document_ids
.iter()
.filter_map(|id| {
self.documents.get(id).map(|doc| FrontendDocumentDetails {
is_saved: doc.is_saved(),
id: *id,
name: doc.name.clone(),
})
})
.collect::<Vec<_>>();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
}
AutoSaveDocument(id) => {
let document = self.documents.get(&id).unwrap();
responses.push_back(
FrontendMessage::TriggerIndexedDbWriteDocument {
document: document.serialize_document(),
details: FrontendDocumentDetails {
is_saved: document.is_saved(),
id,
name: document.name.clone(),
},
version: GRAPHITE_DOCUMENT_VERSION.to_string(),
}
.into(),
)
}
AutoSaveActiveDocument => responses.push_back(PortfolioMessage::AutoSaveDocument(self.active_document_id).into()),
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];
responses.push_back(PortfolioMessage::SelectDocument(next_id).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(prev_id).into());
}
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_buffer = &mut self.copy_buffer;
copy_buffer[clipboard as usize].clear();
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) => {
copy_buffer[clipboard as usize].push(CopyBufferEntry { layer, layer_metadata });
}
(Err(e), _) => warn!("Could not access selected layer {:?}: {:?}", layer_path, e),
}
}
}
Cut(clipboard) => {
responses.push_back(Copy(clipboard).into());
responses.push_back(DeleteSelectedLayers.into());
}
Paste(clipboard) => { Paste(clipboard) => {
let document = self.active_document(); let document = self.active_document();
let shallowest_common_folder = document let shallowest_common_folder = document
@@ -424,6 +388,45 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessor> for PortfolioMessageHa
} }
} }
} }
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(prev_id).into());
}
RequestAboutGraphiteDialog => {
responses.push_back(FrontendMessage::DisplayDialogAboutGraphite.into());
}
SelectDocument(id) => {
let active_document = self.active_document();
if !active_document.is_saved() {
responses.push_back(PortfolioMessage::AutoSaveDocument(self.active_document_id).into());
}
self.active_document_id = id;
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id: 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(layer.clone()).into());
}
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
UpdateOpenDocumentsList => {
// Send the list of document tab names
let open_documents = self
.document_ids
.iter()
.filter_map(|id| {
self.documents.get(id).map(|doc| FrontendDocumentDetails {
is_saved: doc.is_saved(),
id: *id,
name: doc.name.clone(),
})
})
.collect::<Vec<_>>();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
}
} }
} }
fn actions(&self) -> ActionList { fn actions(&self) -> ActionList {
+22 -23
View File
@@ -358,25 +358,22 @@ impl Typing {
} }
} }
#[remain::sorted]
#[impl_message(Message, DocumentMessage, TransformLayers)] #[impl_message(Message, DocumentMessage, TransformLayers)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum TransformLayerMessage { pub enum TransformLayerMessage {
BeginGrab,
BeginScale,
BeginRotate,
CancelOperation,
ApplyOperation, ApplyOperation,
BeginGrab,
TypeNumber(u8), BeginRotate,
BeginScale,
CancelOperation,
ConstrainX,
ConstrainY,
MouseMove { slow_key: Key, snap_key: Key },
TypeBackspace, TypeBackspace,
TypeDecimalPoint, TypeDecimalPoint,
TypeNegate, TypeNegate,
TypeNumber(u8),
ConstrainX,
ConstrainY,
MouseMove { slow_key: Key, snap_key: Key },
} }
#[derive(Debug, Clone, Default, PartialEq)] #[derive(Debug, Clone, Default, PartialEq)]
@@ -395,6 +392,7 @@ pub struct TransformLayerMessageHandler {
} }
impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMetadata>, &mut Document, &InputPreprocessor)> for TransformLayerMessageHandler { impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMetadata>, &mut Document, &InputPreprocessor)> for TransformLayerMessageHandler {
#[remain::check]
fn process_action(&mut self, message: TransformLayerMessage, data: (&mut HashMap<Vec<LayerId>, LayerMetadata>, &mut Document, &InputPreprocessor), responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: TransformLayerMessage, data: (&mut HashMap<Vec<LayerId>, LayerMetadata>, &mut Document, &InputPreprocessor), responses: &mut VecDeque<Message>) {
use TransformLayerMessage::*; use TransformLayerMessage::*;
@@ -413,7 +411,16 @@ impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMeta
*start_mouse = ipp.mouse.position; *start_mouse = ipp.mouse.position;
}; };
#[remain::sorted]
match message { match message {
ApplyOperation => {
self.original_transforms.clear();
self.typing.clear();
self.operation = Operation::None;
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
BeginGrab => { BeginGrab => {
if let Operation::Grabbing(_) = self.operation { if let Operation::Grabbing(_) = self.operation {
return; return;
@@ -458,14 +465,8 @@ impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMeta
responses.push_back(ToolMessage::DocumentIsDirty.into()); responses.push_back(ToolMessage::DocumentIsDirty.into());
} }
ApplyOperation => { ConstrainX => self.operation.constrain_axis(Axis::X, &mut selected, self.snap),
self.original_transforms.clear(); ConstrainY => self.operation.constrain_axis(Axis::Y, &mut selected, self.snap),
self.typing.clear();
self.operation = Operation::None;
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
MouseMove { slow_key, snap_key } => { MouseMove { slow_key, snap_key } => {
self.slow = ipp.keyboard.get(slow_key as usize); self.slow = ipp.keyboard.get(slow_key as usize);
@@ -515,12 +516,10 @@ impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMeta
} }
self.mouse_position = ipp.mouse.position; self.mouse_position = ipp.mouse.position;
} }
TypeNumber(number) => self.operation.handle_typed(self.typing.type_number(number), &mut selected, self.snap),
TypeBackspace => self.operation.handle_typed(self.typing.type_backspace(), &mut selected, self.snap), TypeBackspace => self.operation.handle_typed(self.typing.type_backspace(), &mut selected, self.snap),
TypeDecimalPoint => self.operation.handle_typed(self.typing.type_decimal_point(), &mut selected, self.snap), TypeDecimalPoint => self.operation.handle_typed(self.typing.type_decimal_point(), &mut selected, self.snap),
TypeNegate => self.operation.handle_typed(self.typing.type_negate(), &mut selected, self.snap), TypeNegate => self.operation.handle_typed(self.typing.type_negate(), &mut selected, self.snap),
ConstrainX => self.operation.constrain_axis(Axis::X, &mut selected, self.snap), TypeNumber(number) => self.operation.handle_typed(self.typing.type_number(number), &mut selected, self.snap),
ConstrainY => self.operation.constrain_axis(Axis::Y, &mut selected, self.snap),
} }
} }
+23 -20
View File
@@ -12,33 +12,36 @@ pub struct FrontendDocumentDetails {
pub id: u64, pub id: u64,
} }
#[remain::sorted]
#[impl_message(Message, Frontend)] #[impl_message(Message, Frontend)]
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] #[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
pub enum FrontendMessage { pub enum FrontendMessage {
// Display prefix: make the frontend show something, like a dialog // Display prefix: make the frontend show something, like a dialog
// Update prefix: give the frontend a new value or state for it to use DisplayConfirmationToCloseAllDocuments,
// Trigger prefix: cause a browser API to do something DisplayConfirmationToCloseDocument { document_id: u64 },
DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer }, DisplayDialogAboutGraphite,
UpdateActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
UpdateActiveDocument { document_id: u64 },
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
UpdateInputHints { hint_data: HintData },
DisplayDialogError { title: String, description: String }, DisplayDialogError { title: String, description: String },
DisplayDialogPanic { panic_info: String, title: String, description: String }, DisplayDialogPanic { panic_info: String, title: String, description: String },
DisplayConfirmationToCloseDocument { document_id: u64 }, DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer },
DisplayConfirmationToCloseAllDocuments,
DisplayDialogAboutGraphite, // Trigger prefix: cause a browser API to do something
UpdateDocumentLayer { data: LayerPanelEntry },
UpdateDocumentArtwork { svg: String },
UpdateDocumentOverlays { svg: String },
UpdateDocumentArtboards { svg: String },
UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 },
TriggerFileUpload,
TriggerFileDownload { document: String, name: String }, TriggerFileDownload { document: String, name: String },
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String }, TriggerFileUpload,
TriggerIndexedDbRemoveDocument { document_id: u64 }, TriggerIndexedDbRemoveDocument { document_id: u64 },
UpdateWorkingColors { primary: Color, secondary: Color }, TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
UpdateCanvasZoom { factor: f64 },
// Update prefix: give the frontend a new value or state for it to use
UpdateActiveDocument { document_id: u64 },
UpdateActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
UpdateCanvasRotation { angle_radians: f64 }, UpdateCanvasRotation { angle_radians: f64 },
UpdateCanvasZoom { factor: f64 },
UpdateDocumentArtboards { svg: String },
UpdateDocumentArtwork { svg: String },
UpdateDocumentLayer { data: LayerPanelEntry },
UpdateDocumentOverlays { svg: String },
UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 },
UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
UpdateInputHints { hint_data: HintData },
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
UpdateWorkingColors { primary: Color, secondary: Color },
} }
+8 -5
View File
@@ -2,11 +2,12 @@ use crate::message_prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::VecDeque; use std::collections::VecDeque;
#[remain::sorted]
#[impl_message(Message, Global)] #[impl_message(Message, Global)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum GlobalMessage { pub enum GlobalMessage {
LogInfo,
LogDebug, LogDebug,
LogInfo,
LogTrace, LogTrace,
} }
@@ -14,17 +15,19 @@ pub enum GlobalMessage {
pub struct GlobalMessageHandler {} pub struct GlobalMessageHandler {}
impl MessageHandler<GlobalMessage, ()> for GlobalMessageHandler { impl MessageHandler<GlobalMessage, ()> for GlobalMessageHandler {
#[remain::check]
fn process_action(&mut self, message: GlobalMessage, _data: (), _responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: GlobalMessage, _data: (), _responses: &mut VecDeque<Message>) {
use GlobalMessage::*; use GlobalMessage::*;
#[remain::sorted]
match message { match message {
LogInfo => {
log::set_max_level(log::LevelFilter::Info);
log::info!("set log verbosity to info");
}
LogDebug => { LogDebug => {
log::set_max_level(log::LevelFilter::Debug); log::set_max_level(log::LevelFilter::Debug);
log::info!("set log verbosity to debug"); log::info!("set log verbosity to debug");
} }
LogInfo => {
log::set_max_level(log::LevelFilter::Info);
log::info!("set log verbosity to info");
}
LogTrace => { LogTrace => {
log::set_max_level(log::LevelFilter::Trace); log::set_max_level(log::LevelFilter::Trace);
log::info!("set log verbosity to trace"); log::info!("set log verbosity to trace");
+7 -6
View File
@@ -14,15 +14,16 @@ use std::fmt::Write;
const NUDGE_AMOUNT: f64 = 1.; const NUDGE_AMOUNT: f64 = 1.;
const SHIFT_NUDGE_AMOUNT: f64 = 10.; const SHIFT_NUDGE_AMOUNT: f64 = 10.;
#[remain::sorted]
#[impl_message(Message, InputMapper)] #[impl_message(Message, InputMapper)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum InputMapperMessage { pub enum InputMapperMessage {
PointerMove,
MouseScroll,
#[child]
KeyUp(Key),
#[child] #[child]
KeyDown(Key), KeyDown(Key),
#[child]
KeyUp(Key),
MouseScroll,
PointerMove,
} }
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
@@ -116,8 +117,8 @@ macro_rules! mapping {
let arr = match entry.trigger { let arr = match entry.trigger {
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize], InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize], InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
InputMapperMessage::PointerMove => &mut pointer_move,
InputMapperMessage::MouseScroll => &mut mouse_scroll, InputMapperMessage::MouseScroll => &mut mouse_scroll,
InputMapperMessage::PointerMove => &mut pointer_move,
}; };
arr.push(entry.clone()); arr.push(entry.clone());
} }
@@ -341,8 +342,8 @@ impl Mapping {
let list = match message { let list = match message {
KeyDown(key) => &self.key_down[key as usize], KeyDown(key) => &self.key_down[key as usize],
KeyUp(key) => &self.key_up[key as usize], KeyUp(key) => &self.key_up[key as usize],
PointerMove => &self.pointer_move,
MouseScroll => &self.mouse_scroll, MouseScroll => &self.mouse_scroll,
PointerMove => &self.pointer_move,
}; };
list.match_mapping(keys, actions) list.match_mapping(keys, actions)
} }
+54 -51
View File
@@ -9,16 +9,17 @@ use bitflags::bitflags;
pub use graphene::DocumentResponse; pub use graphene::DocumentResponse;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, InputPreprocessor)] #[impl_message(Message, InputPreprocessor)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum InputPreprocessorMessage { pub enum InputPreprocessorMessage {
BoundsOfViewports(Vec<ViewportBounds>),
KeyDown(Key, ModifierKeys),
KeyUp(Key, ModifierKeys),
MouseDown(EditorMouseState, ModifierKeys), MouseDown(EditorMouseState, ModifierKeys),
MouseUp(EditorMouseState, ModifierKeys),
MouseMove(EditorMouseState, ModifierKeys), MouseMove(EditorMouseState, ModifierKeys),
MouseScroll(EditorMouseState, ModifierKeys), MouseScroll(EditorMouseState, ModifierKeys),
KeyUp(Key, ModifierKeys), MouseUp(EditorMouseState, ModifierKeys),
KeyDown(Key, ModifierKeys),
BoundsOfViewports(Vec<ViewportBounds>),
} }
bitflags! { bitflags! {
@@ -44,55 +45,10 @@ enum KeyPosition {
} }
impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessor { impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessor {
#[remain::check]
fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque<Message>) {
#[remain::sorted]
match message { match message {
InputPreprocessorMessage::MouseMove(editor_mouse_state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
responses.push_back(InputMapperMessage::PointerMove.into());
}
InputPreprocessorMessage::MouseDown(editor_mouse_state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Pressed) {
responses.push_back(message);
}
}
InputPreprocessorMessage::MouseUp(editor_mouse_state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Released) {
responses.push_back(message);
}
}
InputPreprocessorMessage::MouseScroll(editor_mouse_state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
self.mouse.scroll_delta = mouse_state.scroll_delta;
responses.push_back(InputMapperMessage::MouseScroll.into());
}
InputPreprocessorMessage::KeyDown(key, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
self.keyboard.set(key as usize);
responses.push_back(InputMapperMessage::KeyDown(key).into());
}
InputPreprocessorMessage::KeyUp(key, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
self.keyboard.unset(key as usize);
responses.push_back(InputMapperMessage::KeyUp(key).into());
}
InputPreprocessorMessage::BoundsOfViewports(bounds_of_viewports) => { InputPreprocessorMessage::BoundsOfViewports(bounds_of_viewports) => {
assert_eq!(bounds_of_viewports.len(), 1, "Only one viewport is currently supported"); assert_eq!(bounds_of_viewports.len(), 1, "Only one viewport is currently supported");
@@ -134,6 +90,53 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessor {
); );
} }
} }
InputPreprocessorMessage::KeyDown(key, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
self.keyboard.set(key as usize);
responses.push_back(InputMapperMessage::KeyDown(key).into());
}
InputPreprocessorMessage::KeyUp(key, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
self.keyboard.unset(key as usize);
responses.push_back(InputMapperMessage::KeyUp(key).into());
}
InputPreprocessorMessage::MouseDown(editor_mouse_state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Pressed) {
responses.push_back(message);
}
}
InputPreprocessorMessage::MouseMove(editor_mouse_state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
responses.push_back(InputMapperMessage::PointerMove.into());
}
InputPreprocessorMessage::MouseScroll(editor_mouse_state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
self.mouse.scroll_delta = mouse_state.scroll_delta;
responses.push_back(InputMapperMessage::MouseScroll.into());
}
InputPreprocessorMessage::MouseUp(editor_mouse_state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position;
if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Released) {
responses.push_back(message);
}
}
}; };
} }
// clean user input and if possible reconstruct it // clean user input and if possible reconstruct it
+43 -38
View File
@@ -9,63 +9,55 @@ use crate::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::VecDeque; use std::collections::VecDeque;
#[remain::sorted]
#[impl_message(Message, Tool)] #[impl_message(Message, Tool)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum ToolMessage { pub enum ToolMessage {
SelectPrimaryColor(Color),
SelectSecondaryColor(Color),
SwapColors,
ResetColors,
NoOp,
ActivateTool(ToolType), ActivateTool(ToolType),
#[child]
Crop(CropMessage),
DocumentIsDirty, DocumentIsDirty,
UpdateHints,
SetToolOptions(ToolType, ToolOptions),
#[child]
Fill(FillMessage),
#[child]
Rectangle(RectangleMessage),
#[child] #[child]
Ellipse(EllipseMessage), Ellipse(EllipseMessage),
#[child] #[child]
Select(SelectMessage), Eyedropper(EyedropperMessage),
#[child]
Fill(FillMessage),
#[child] #[child]
Line(LineMessage), Line(LineMessage),
#[child] #[child]
Crop(CropMessage),
#[child]
Eyedropper(EyedropperMessage),
#[child]
Navigate(NavigateMessage), Navigate(NavigateMessage),
NoOp,
#[child] #[child]
Path(PathMessage), Path(PathMessage),
#[child] #[child]
Pen(PenMessage), Pen(PenMessage),
#[child] #[child]
Rectangle(RectangleMessage),
ResetColors,
#[child]
Select(SelectMessage),
SelectPrimaryColor(Color),
SelectSecondaryColor(Color),
SetToolOptions(ToolType, ToolOptions),
#[child]
Shape(ShapeMessage), Shape(ShapeMessage),
SwapColors,
UpdateHints,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ToolMessageHandler { pub struct ToolMessageHandler {
tool_state: ToolFsmState, tool_state: ToolFsmState,
} }
impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)> for ToolMessageHandler { impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)> for ToolMessageHandler {
#[remain::check]
fn process_action(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessor), responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessor), responses: &mut VecDeque<Message>) {
let (document, input) = data; let (document, input) = data;
use ToolMessage::*; use ToolMessage::*;
#[remain::sorted]
match message { match message {
SelectPrimaryColor(color) => {
let document_data = &mut self.tool_state.document_tool_data;
document_data.primary_color = color;
update_working_colors(&self.tool_state.document_tool_data, responses);
}
SelectSecondaryColor(color) => {
let document_data = &mut self.tool_state.document_tool_data;
document_data.secondary_color = color;
update_working_colors(document_data, responses);
}
ActivateTool(new_tool) => { ActivateTool(new_tool) => {
let tool_data = &mut self.tool_state.tool_data; let tool_data = &mut self.tool_state.tool_data;
let document_data = &self.tool_state.document_tool_data; let document_data = &self.tool_state.document_tool_data;
@@ -114,13 +106,6 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)>
responses.push_back(message.into()); responses.push_back(message.into());
} }
} }
SwapColors => {
let document_data = &mut self.tool_state.document_tool_data;
std::mem::swap(&mut document_data.primary_color, &mut document_data.secondary_color);
update_working_colors(document_data, responses);
}
ResetColors => { ResetColors => {
let document_data = &mut self.tool_state.document_tool_data; let document_data = &mut self.tool_state.document_tool_data;
@@ -129,24 +114,44 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)>
update_working_colors(document_data, responses); update_working_colors(document_data, responses);
} }
SelectPrimaryColor(color) => {
let document_data = &mut self.tool_state.document_tool_data;
document_data.primary_color = color;
update_working_colors(&self.tool_state.document_tool_data, responses);
}
SelectSecondaryColor(color) => {
let document_data = &mut self.tool_state.document_tool_data;
document_data.secondary_color = color;
update_working_colors(document_data, responses);
}
SetToolOptions(tool_type, tool_options) => { SetToolOptions(tool_type, tool_options) => {
let document_data = &mut self.tool_state.document_tool_data; let document_data = &mut self.tool_state.document_tool_data;
document_data.tool_options.insert(tool_type, tool_options); document_data.tool_options.insert(tool_type, tool_options);
} }
message => { SwapColors => {
let tool_type = message_to_tool_type(&message); let document_data = &mut self.tool_state.document_tool_data;
std::mem::swap(&mut document_data.primary_color, &mut document_data.secondary_color);
update_working_colors(document_data, responses);
}
tool_message => {
let tool_type = message_to_tool_type(&tool_message);
let document_data = &self.tool_state.document_tool_data; let document_data = &self.tool_state.document_tool_data;
let tool_data = &mut self.tool_state.tool_data; let tool_data = &mut self.tool_state.tool_data;
if let Some(tool) = tool_data.tools.get_mut(&tool_type) { if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
if tool_type == tool_data.active_tool_type { if tool_type == tool_data.active_tool_type {
tool.process_action(message, (document, document_data, input), responses); tool.process_action(tool_message, (document, document_data, input), responses);
} }
} }
} }
} }
} }
fn actions(&self) -> ActionList { fn actions(&self) -> ActionList {
let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, ActivateTool, SetToolOptions); let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, ActivateTool, SetToolOptions);
list.extend(self.tool_state.tool_data.active_tool().actions()); list.extend(self.tool_state.tool_data.active_tool().actions());
+1
View File
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
#[derive(Default)] #[derive(Default)]
pub struct Crop; pub struct Crop;
#[remain::sorted]
#[impl_message(Message, ToolMessage, Crop)] #[impl_message(Message, ToolMessage, Crop)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum CropMessage { pub enum CropMessage {
+2 -1
View File
@@ -13,13 +13,14 @@ pub struct Ellipse {
data: EllipseToolData, data: EllipseToolData,
} }
#[remain::sorted]
#[impl_message(Message, ToolMessage, Ellipse)] #[impl_message(Message, ToolMessage, Ellipse)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum EllipseMessage { pub enum EllipseMessage {
Abort,
DragStart, DragStart,
DragStop, DragStop,
Resize { center: Key, lock_ratio: Key }, Resize { center: Key, lock_ratio: Key },
Abort,
} }
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse { impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
+2 -1
View File
@@ -15,12 +15,13 @@ pub struct Eyedropper {
data: EyedropperToolData, data: EyedropperToolData,
} }
#[remain::sorted]
#[impl_message(Message, ToolMessage, Eyedropper)] #[impl_message(Message, ToolMessage, Eyedropper)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum EyedropperMessage { pub enum EyedropperMessage {
Abort,
LeftMouseDown, LeftMouseDown,
RightMouseDown, RightMouseDown,
Abort,
} }
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper { impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
+2 -1
View File
@@ -15,12 +15,13 @@ pub struct Fill {
data: FillToolData, data: FillToolData,
} }
#[remain::sorted]
#[impl_message(Message, ToolMessage, Fill)] #[impl_message(Message, ToolMessage, Fill)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum FillMessage { pub enum FillMessage {
Abort,
LeftMouseDown, LeftMouseDown,
RightMouseDown, RightMouseDown,
Abort,
} }
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill { impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
+2 -1
View File
@@ -15,13 +15,14 @@ pub struct Line {
data: LineToolData, data: LineToolData,
} }
#[remain::sorted]
#[impl_message(Message, ToolMessage, Line)] #[impl_message(Message, ToolMessage, Line)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum LineMessage { pub enum LineMessage {
Abort,
DragStart, DragStart,
DragStop, DragStop,
Redraw { center: Key, lock_angle: Key, snap_angle: Key }, Redraw { center: Key, lock_angle: Key, snap_angle: Key },
Abort,
} }
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line { impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
+4 -3
View File
@@ -11,16 +11,17 @@ pub struct Navigate {
data: NavigateToolData, data: NavigateToolData,
} }
#[remain::sorted]
#[impl_message(Message, ToolMessage, Navigate)] #[impl_message(Message, ToolMessage, Navigate)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum NavigateMessage { pub enum NavigateMessage {
Abort,
ClickZoom { zoom_in: bool }, ClickZoom { zoom_in: bool },
MouseMove { snap_angle: Key, snap_zoom: Key }, MouseMove { snap_angle: Key, snap_zoom: Key },
TranslateCanvasBegin,
RotateCanvasBegin, RotateCanvasBegin,
ZoomCanvasBegin,
TransformCanvasEnd, TransformCanvasEnd,
Abort, TranslateCanvasBegin,
ZoomCanvasBegin,
} }
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate { impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate {
+5 -4
View File
@@ -27,16 +27,17 @@ pub struct Path {
data: PathToolData, data: PathToolData,
} }
#[remain::sorted]
#[impl_message(Message, ToolMessage, Path)] #[impl_message(Message, ToolMessage, Path)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum PathMessage { pub enum PathMessage {
DragStart,
PointerMove,
DragStop,
// Standard messages // Standard messages
Abort, Abort,
DocumentIsDirty, DocumentIsDirty,
DragStart,
DragStop,
PointerMove,
} }
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path { impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
+4 -3
View File
@@ -14,15 +14,16 @@ pub struct Pen {
data: PenToolData, data: PenToolData,
} }
#[remain::sorted]
#[impl_message(Message, ToolMessage, Pen)] #[impl_message(Message, ToolMessage, Pen)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum PenMessage { pub enum PenMessage {
Undo, Abort,
Confirm,
DragStart, DragStart,
DragStop, DragStop,
PointerMove, PointerMove,
Confirm, Undo,
Abort,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
+2 -1
View File
@@ -15,13 +15,14 @@ pub struct Rectangle {
data: RectangleToolData, data: RectangleToolData,
} }
#[remain::sorted]
#[impl_message(Message, ToolMessage, Rectangle)] #[impl_message(Message, ToolMessage, Rectangle)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum RectangleMessage { pub enum RectangleMessage {
Abort,
DragStart, DragStart,
DragStop, DragStop,
Resize { center: Key, lock_ratio: Key }, Resize { center: Key, lock_ratio: Key },
Abort,
} }
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle { impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
+1
View File
@@ -26,6 +26,7 @@ pub struct Select {
data: SelectToolData, data: SelectToolData,
} }
// #[remain::sorted] // https://github.com/dtolnay/remain/issues/16
#[impl_message(Message, ToolMessage, Select)] #[impl_message(Message, ToolMessage, Select)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum SelectMessage { pub enum SelectMessage {
+2 -1
View File
@@ -15,13 +15,14 @@ pub struct Shape {
data: ShapeToolData, data: ShapeToolData,
} }
#[remain::sorted]
#[impl_message(Message, ToolMessage, Shape)] #[impl_message(Message, ToolMessage, Shape)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum ShapeMessage { pub enum ShapeMessage {
Abort,
DragStart, DragStart,
DragStop, DragStop,
Resize { center: Key, lock_ratio: Key }, Resize { center: Key, lock_ratio: Key },
Abort,
} }
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape { impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {