Restructure the entire editor codebase to consistently match the message hierarchy

Closes #744
This commit is contained in:
Keavon Chambers
2022-08-05 05:38:11 -07:00
parent 78f07439db
commit e4306c30e2
154 changed files with 4199 additions and 4064 deletions
@@ -0,0 +1,37 @@
use crate::messages::prelude::*;
use graphene::LayerId;
use graphene::Operation as DocumentOperation;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Artboard)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum ArtboardMessage {
// Sub-messages
#[remain::unsorted]
DispatchOperation(Box<DocumentOperation>),
// Messages
AddArtboard {
id: Option<LayerId>,
position: (f64, f64),
size: (f64, f64),
},
DeleteArtboard {
artboard: LayerId,
},
RenderArtboards,
ResizeArtboard {
artboard: LayerId,
position: (f64, f64),
size: (f64, f64),
},
}
impl From<DocumentOperation> for ArtboardMessage {
fn from(operation: DocumentOperation) -> Self {
Self::DispatchOperation(Box::new(operation))
}
}
@@ -0,0 +1,116 @@
use crate::messages::prelude::*;
use crate::application::generate_uuid;
use graphene::color::Color;
use graphene::document::Document as GrapheneDocument;
use graphene::layers::style::{self, Fill, RenderData, ViewMode};
use graphene::layers::text_layer::FontCache;
use graphene::DocumentResponse;
use graphene::LayerId;
use graphene::Operation as DocumentOperation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ArtboardMessageHandler {
pub artboards_graphene_document: GrapheneDocument,
pub artboard_ids: Vec<LayerId>,
}
impl MessageHandler<ArtboardMessage, &FontCache> for ArtboardMessageHandler {
#[remain::check]
fn process_message(&mut self, message: ArtboardMessage, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
use ArtboardMessage::*;
#[remain::sorted]
match message {
// Sub-messages
#[remain::unsorted]
DispatchOperation(operation) => match self.artboards_graphene_document.handle_operation(*operation, font_cache) {
Ok(Some(document_responses)) => {
for response in document_responses {
match &response {
DocumentResponse::LayerChanged { path } => responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: path.clone() }.into()),
DocumentResponse::DeletedLayer { path } => responses.push_back(PropertiesPanelMessage::CheckSelectedWasDeleted { path: path.clone() }.into()),
DocumentResponse::DocumentChanged => responses.push_back(ArtboardMessage::RenderArtboards.into()),
_ => {}
};
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
}
Ok(None) => {}
Err(e) => log::error!("Artboard Error: {:?}", e),
},
// Messages
AddArtboard { id, position, size } => {
let artboard_id = id.unwrap_or_else(generate_uuid);
self.artboard_ids.push(artboard_id);
responses.push_back(
ArtboardMessage::DispatchOperation(
DocumentOperation::AddRect {
path: vec![artboard_id],
insert_index: -1,
transform: DAffine2::from_scale_angle_translation(size.into(), 0., position.into()).to_cols_array(),
style: style::PathStyle::new(None, Fill::solid(Color::WHITE)),
}
.into(),
)
.into(),
);
responses.push_back(DocumentMessage::RenderDocument.into());
}
DeleteArtboard { artboard } => {
self.artboard_ids.retain(|&id| id != artboard);
responses.push_back(ArtboardMessage::DispatchOperation(Box::new(DocumentOperation::DeleteLayer { path: vec![artboard] })).into());
responses.push_back(DocumentMessage::RenderDocument.into());
}
RenderArtboards => {
// Render an infinite canvas if there are no artboards
if self.artboard_ids.is_empty() {
responses.push_back(
FrontendMessage::UpdateDocumentArtboards {
svg: r##"<rect width="100%" height="100%" fill="#ffffff" />"##.to_string(),
}
.into(),
)
} else {
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false);
responses.push_back(
FrontendMessage::UpdateDocumentArtboards {
svg: self.artboards_graphene_document.render_root(render_data),
}
.into(),
);
}
}
ResizeArtboard { artboard, position, size } => {
responses.push_back(
ArtboardMessage::DispatchOperation(Box::new(DocumentOperation::SetLayerTransform {
path: vec![artboard],
transform: DAffine2::from_scale_angle_translation(size.into(), 0., position.into()).to_cols_array(),
}))
.into(),
);
responses.push_back(DocumentMessage::RenderDocument.into());
}
}
}
fn actions(&self) -> ActionList {
actions!(ArtboardMessageDiscriminant;)
}
}
impl ArtboardMessageHandler {
pub fn is_infinite_canvas(&self) -> bool {
self.artboard_ids.is_empty()
}
}
@@ -0,0 +1,7 @@
mod artboard_message;
mod artboard_message_handler;
#[doc(inline)]
pub use artboard_message::{ArtboardMessage, ArtboardMessageDiscriminant};
#[doc(inline)]
pub use artboard_message_handler::ArtboardMessageHandler;
@@ -0,0 +1,185 @@
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::utility_types::layer_panel::LayerMetadata;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
use crate::messages::prelude::*;
use graphene::boolean_ops::BooleanOperation as BooleanOperationType;
use graphene::layers::blend_mode::BlendMode;
use graphene::layers::style::ViewMode;
use graphene::LayerId;
use graphene::Operation as DocumentOperation;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, PortfolioMessage, Document)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum DocumentMessage {
// Sub-messages
#[remain::unsorted]
DispatchOperation(Box<DocumentOperation>),
#[remain::unsorted]
#[child]
Artboard(ArtboardMessage),
#[remain::unsorted]
#[child]
Movement(MovementMessage),
#[remain::unsorted]
#[child]
Overlays(OverlaysMessage),
#[remain::unsorted]
#[child]
TransformLayer(TransformLayerMessage),
#[remain::unsorted]
#[child]
PropertiesPanel(PropertiesPanelMessage),
// Messages
AbortTransaction,
AddSelectedLayers {
additional_layers: Vec<Vec<LayerId>>,
},
AlignSelectedLayers {
axis: AlignAxis,
aggregate: AlignAggregate,
},
BooleanOperation(BooleanOperationType),
CommitTransaction,
CreateEmptyFolder {
container_path: Vec<LayerId>,
},
DebugPrintDocument,
DeleteLayer {
layer_path: Vec<LayerId>,
},
DeleteSelectedLayers,
DeleteSelectedManipulatorPoints,
DeselectAllLayers,
DeselectAllManipulatorPoints,
DirtyRenderDocument,
DirtyRenderDocumentInOutlineView,
DocumentHistoryBackward,
DocumentHistoryForward,
DocumentStructureChanged,
DuplicateSelectedLayers,
ExportDocument {
file_name: String,
file_type: FileType,
scale_factor: f64,
bounds: ExportBounds,
},
FlipSelectedLayers {
flip_axis: FlipAxis,
},
FolderChanged {
affected_folder_path: Vec<LayerId>,
},
GroupSelectedLayers,
LayerChanged {
affected_layer_path: Vec<LayerId>,
},
MoveSelectedLayersTo {
folder_path: Vec<LayerId>,
insert_index: isize,
reverse_index: bool,
},
MoveSelectedManipulatorPoints {
layer_path: Vec<LayerId>,
delta: (f64, f64),
absolute_position: (f64, f64),
},
NudgeSelectedLayers {
delta_x: f64,
delta_y: f64,
},
PasteImage {
mime: String,
image_data: Vec<u8>,
mouse: Option<(f64, f64)>,
},
Redo,
RenameLayer {
layer_path: Vec<LayerId>,
new_name: String,
},
RenderDocument,
RollbackTransaction,
SaveDocument,
SelectAllLayers,
SelectedLayersLower,
SelectedLayersLowerToBack,
SelectedLayersRaise,
SelectedLayersRaiseToFront,
SelectedLayersReorder {
relative_index_offset: isize,
},
SelectLayer {
layer_path: Vec<LayerId>,
ctrl: bool,
shift: bool,
},
SetBlendModeForSelectedLayers {
blend_mode: BlendMode,
},
SetLayerExpansion {
layer_path: Vec<LayerId>,
set_expanded: bool,
},
SetLayerName {
layer_path: Vec<LayerId>,
name: String,
},
SetOpacityForSelectedLayers {
opacity: f64,
},
SetOverlaysVisibility {
visible: bool,
},
SetSelectedLayers {
replacement_selected_layers: Vec<Vec<LayerId>>,
},
SetSnapping {
snap: bool,
},
SetTexboxEditability {
path: Vec<LayerId>,
editable: bool,
},
SetViewMode {
view_mode: ViewMode,
},
StartTransaction,
ToggleLayerExpansion {
layer_path: Vec<LayerId>,
},
ToggleLayerVisibility {
layer_path: Vec<LayerId>,
},
ToggleSelectedHandleMirroring {
layer_path: Vec<LayerId>,
toggle_distance: bool,
toggle_angle: bool,
},
Undo,
UngroupLayers {
folder_path: Vec<LayerId>,
},
UngroupSelectedLayers,
UpdateLayerMetadata {
layer_path: Vec<LayerId>,
layer_metadata: LayerMetadata,
},
ZoomCanvasToFitAll,
}
impl From<DocumentOperation> for DocumentMessage {
fn from(operation: DocumentOperation) -> DocumentMessage {
DocumentMessage::DispatchOperation(Box::new(operation))
}
}
impl From<DocumentOperation> for Message {
fn from(operation: DocumentOperation) -> Message {
DocumentMessage::DispatchOperation(Box::new(operation)).into()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
mod document_message;
mod document_message_handler;
pub mod artboard;
pub mod movement;
pub mod overlays;
pub mod properties_panel;
pub mod transform_layer;
pub mod utility_types;
#[doc(inline)]
pub use document_message::{DocumentMessage, DocumentMessageDiscriminant};
#[doc(inline)]
pub use document_message_handler::DocumentMessageHandler;
@@ -0,0 +1,7 @@
mod movement_message;
mod movement_message_handler;
#[doc(inline)]
pub use movement_message::{MovementMessage, MovementMessageDiscriminant};
#[doc(inline)]
pub use movement_message_handler::MovementMessageHandler;
@@ -0,0 +1,49 @@
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::prelude::*;
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Movement)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum MovementMessage {
// Messages
DecreaseCanvasZoom {
center_on_mouse: bool,
},
FitViewportToBounds {
bounds: [DVec2; 2],
padding_scale_factor: Option<f32>,
prevent_zoom_past_100: bool,
},
IncreaseCanvasZoom {
center_on_mouse: bool,
},
PointerMove {
snap_angle: Key,
wait_for_snap_angle_release: bool,
snap_zoom: Key,
zoom_from_viewport: Option<DVec2>,
},
RotateCanvasBegin,
SetCanvasRotation {
angle_radians: f64,
},
SetCanvasZoom {
zoom_factor: f64,
},
TransformCanvasEnd,
TranslateCanvas {
delta: DVec2,
},
TranslateCanvasBegin,
TranslateCanvasByViewportFraction {
delta: DVec2,
},
WheelCanvasTranslate {
use_y_as_x: bool,
},
WheelCanvasZoom,
ZoomCanvasBegin,
}
@@ -0,0 +1,353 @@
use crate::consts::{VIEWPORT_ROTATE_SNAP_INTERVAL, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_LEVELS, VIEWPORT_ZOOM_MOUSE_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_WHEEL_RATE};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup};
use crate::messages::input_mapper::utility_types::input_mouse::{ViewportBounds, ViewportPosition};
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::document::Document;
use graphene::Operation as DocumentOperation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MovementMessageHandler {
pub pan: DVec2,
panning: bool,
snap_tilt: bool,
snap_tilt_released: bool,
pub tilt: f64,
tilting: bool,
pub zoom: f64,
zooming: bool,
snap_zoom: bool,
mouse_position: ViewportPosition,
}
impl Default for MovementMessageHandler {
fn default() -> Self {
Self {
pan: DVec2::ZERO,
panning: false,
snap_tilt: false,
snap_tilt_released: false,
tilt: 0.,
tilting: false,
zoom: 1.,
zooming: false,
snap_zoom: false,
mouse_position: ViewportPosition::default(),
}
}
}
impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandler)> for MovementMessageHandler {
#[remain::check]
fn process_message(&mut self, message: MovementMessage, data: (&Document, &InputPreprocessorMessageHandler), responses: &mut VecDeque<Message>) {
use MovementMessage::*;
let (document, ipp) = data;
#[remain::sorted]
match message {
DecreaseCanvasZoom { center_on_mouse } => {
let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < self.zoom).unwrap_or(&self.zoom);
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 { zoom_factor: new_scale }.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(BroadcastEvent::DocumentIsDirty.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
IncreaseCanvasZoom { center_on_mouse } => {
let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > self.zoom).unwrap_or(&self.zoom);
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 { zoom_factor: new_scale }.into());
}
PointerMove {
snap_angle,
wait_for_snap_angle_release,
snap_zoom,
zoom_from_viewport,
} => {
if self.panning {
let delta = ipp.mouse.position - self.mouse_position;
responses.push_back(TranslateCanvas { delta }.into());
}
if self.tilting {
let new_snap = ipp.keyboard.get(snap_angle as usize);
if !(wait_for_snap_angle_release && new_snap && !self.snap_tilt_released) {
// When disabling snap, keep the viewed rotation as it was previously.
if !new_snap && self.snap_tilt {
self.tilt = self.snapped_angle();
}
self.snap_tilt = new_snap;
self.snap_tilt_released = true;
}
let half_viewport = ipp.viewport_bounds.size() / 2.;
let rotation = {
let start_offset = self.mouse_position - half_viewport;
let end_offset = ipp.mouse.position - half_viewport;
start_offset.angle_between(end_offset)
};
responses.push_back(SetCanvasRotation { angle_radians: self.tilt + rotation }.into());
}
if self.zooming {
let zoom_start = self.snapped_scale();
let new_snap = ipp.keyboard.get(snap_zoom as usize);
// When disabling snap, keep the viewed zoom as it was previously
if !new_snap && self.snap_zoom {
self.zoom = self.snapped_scale();
}
self.snap_zoom = new_snap;
let difference = self.mouse_position.y as f64 - ipp.mouse.position.y as f64;
let amount = 1. + difference * VIEWPORT_ZOOM_MOUSE_RATE;
self.zoom *= amount;
if let Some(mouse) = zoom_from_viewport {
let zoom_factor = self.snapped_scale() / zoom_start;
responses.push_back(SetCanvasZoom { zoom_factor: self.zoom }.into());
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, mouse));
} else {
responses.push_back(SetCanvasZoom { zoom_factor: self.zoom }.into());
}
}
self.mouse_position = ipp.mouse.position;
}
RotateCanvasBegin => {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
responses.push_back(
FrontendMessage::UpdateInputHints {
hint_data: HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap 15°"),
plus: false,
}])]),
}
.into(),
);
self.tilting = true;
self.mouse_position = ipp.mouse.position;
}
SetCanvasRotation { angle_radians } => {
self.tilt = angle_radians;
self.create_document_transform(&ipp.viewport_bounds, responses);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
}
SetCanvasZoom { zoom_factor } => {
self.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
TransformCanvasEnd => {
self.tilt = self.snapped_angle();
self.zoom = self.snapped_scale();
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
responses.push_back(ToolMessage::UpdateCursor.into());
responses.push_back(ToolMessage::UpdateHints.into());
self.snap_tilt = false;
self.snap_tilt_released = false;
self.snap_zoom = false;
self.panning = false;
self.tilting = false;
self.zooming = false;
}
TranslateCanvas { delta } => {
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
self.pan += transformed_delta;
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
self.create_document_transform(&ipp.viewport_bounds, responses);
}
TranslateCanvasBegin => {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Grabbing }.into());
responses.push_back(FrontendMessage::UpdateInputHints { hint_data: HintData(Vec::new()) }.into());
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(BroadcastEvent::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 => {
let scroll = ipp.mouse.scroll_delta.scroll_delta();
let mut zoom_factor = 1. + scroll.abs() * VIEWPORT_ZOOM_WHEEL_RATE;
if ipp.mouse.scroll_delta.y > 0 {
zoom_factor = 1. / zoom_factor
};
responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, ipp.mouse.position));
responses.push_back(SetCanvasZoom { zoom_factor: self.zoom * zoom_factor }.into());
}
ZoomCanvasBegin => {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::ZoomIn }.into());
responses.push_back(
FrontendMessage::UpdateInputHints {
hint_data: HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None,
label: String::from("Snap Increments"),
plus: false,
}])]),
}
.into(),
);
self.zooming = true;
self.mouse_position = ipp.mouse.position;
}
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(MovementMessageDiscriminant;
TranslateCanvasBegin,
RotateCanvasBegin,
ZoomCanvasBegin,
SetCanvasZoom,
SetCanvasRotation,
WheelCanvasZoom,
IncreaseCanvasZoom,
DecreaseCanvasZoom,
WheelCanvasTranslate,
TranslateCanvas,
TranslateCanvasByViewportFraction,
);
if self.panning || self.tilting || self.zooming {
let transforming = actions!(MovementMessageDiscriminant;
PointerMove,
TransformCanvasEnd,
);
common.extend(transforming);
}
common
}
}
impl MovementMessageHandler {
pub fn snapped_angle(&self) -> f64 {
let increment_radians: f64 = VIEWPORT_ROTATE_SNAP_INTERVAL.to_radians();
if self.snap_tilt {
(self.tilt / increment_radians).round() * increment_radians
} else {
self.tilt
}
}
pub fn snapped_scale(&self) -> f64 {
if self.snap_zoom {
*VIEWPORT_ZOOM_LEVELS
.iter()
.min_by(|a, b| (**a - self.zoom).abs().partial_cmp(&(**b - self.zoom).abs()).unwrap())
.unwrap_or(&self.zoom)
} else {
self.zoom
}
}
pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 {
// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
let offset_transform = DAffine2::from_translation(offset);
let scale_transform = DAffine2::from_scale(DVec2::splat(self.snapped_scale()));
let angle_transform = DAffine2::from_angle(self.snapped_angle());
let translation_transform = DAffine2::from_translation(self.pan);
scale_transform * offset_transform * angle_transform * translation_transform
}
fn create_document_transform(&self, viewport_bounds: &ViewportBounds, responses: &mut VecDeque<Message>) {
let half_viewport = viewport_bounds.size() / 2.;
let scaled_half_viewport = half_viewport / self.snapped_scale();
responses.push_back(
DocumentOperation::SetLayerTransform {
path: vec![],
transform: self.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
}
.into(),
);
responses.push_back(
ArtboardMessage::DispatchOperation(
DocumentOperation::SetLayerTransform {
path: vec![],
transform: self.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
}
.into(),
)
.into(),
);
}
pub fn center_zoom(&self, viewport_bounds: DVec2, zoom_factor: f64, mouse: DVec2) -> Message {
let new_viewport_bounds = viewport_bounds / zoom_factor;
let delta_size = viewport_bounds - new_viewport_bounds;
let mouse_fraction = mouse / viewport_bounds;
let delta = delta_size * (DVec2::splat(0.5) - mouse_fraction);
MovementMessage::TranslateCanvas { delta }.into()
}
}
@@ -0,0 +1,7 @@
mod overlays_message;
mod overlays_message_handler;
#[doc(inline)]
pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant};
#[doc(inline)]
pub use overlays_message_handler::OverlaysMessageHandler;
@@ -0,0 +1,24 @@
use crate::messages::prelude::*;
use graphene::Operation as DocumentOperation;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, Overlays)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum OverlaysMessage {
// Sub-messages
#[remain::unsorted]
DispatchOperation(Box<DocumentOperation>),
// Messages
ClearAllOverlays,
Rerender,
}
impl From<DocumentOperation> for OverlaysMessage {
fn from(operation: DocumentOperation) -> OverlaysMessage {
Self::DispatchOperation(Box::new(operation))
}
}
@@ -0,0 +1,51 @@
use crate::messages::prelude::*;
use graphene::document::Document as GrapheneDocument;
use graphene::layers::style::{RenderData, ViewMode};
use graphene::layers::text_layer::FontCache;
#[derive(Debug, Clone, Default)]
pub struct OverlaysMessageHandler {
pub overlays_graphene_document: GrapheneDocument,
}
impl MessageHandler<OverlaysMessage, (bool, &FontCache, &InputPreprocessorMessageHandler)> for OverlaysMessageHandler {
#[remain::check]
fn process_message(&mut self, message: OverlaysMessage, (overlays_visible, font_cache, ipp): (bool, &FontCache, &InputPreprocessorMessageHandler), responses: &mut VecDeque<Message>) {
use OverlaysMessage::*;
#[remain::sorted]
match message {
// Sub-messages
#[remain::unsorted]
DispatchOperation(operation) => match self.overlays_graphene_document.handle_operation(*operation, font_cache) {
Ok(_) => responses.push_back(OverlaysMessage::Rerender.into()),
Err(e) => log::error!("OverlaysError: {:?}", e),
},
// Messages
ClearAllOverlays => todo!(),
Rerender =>
// Render overlays
{
responses.push_back(
FrontendMessage::UpdateDocumentOverlays {
svg: if overlays_visible {
let render_data = RenderData::new(ViewMode::Normal, font_cache, Some(ipp.document_bounds()), false);
self.overlays_graphene_document.render_root(render_data)
} else {
String::from("")
},
}
.into(),
)
}
}
}
fn actions(&self) -> ActionList {
actions!(OverlaysMessageDiscriminant;
ClearAllOverlays,
)
}
}
@@ -0,0 +1,10 @@
mod properties_panel_message;
mod properties_panel_message_handler;
pub mod utility_functions;
pub mod utility_types;
#[doc(inline)]
pub use properties_panel_message::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant};
#[doc(inline)]
pub use properties_panel_message_handler::PropertiesPanelMessageHandler;
@@ -0,0 +1,29 @@
use super::utility_types::TransformOp;
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
use crate::messages::prelude::*;
use graphene::layers::style::{Fill, Stroke};
use graphene::LayerId;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, PropertiesPanel)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum PropertiesPanelMessage {
// Messages
CheckSelectedWasDeleted { path: Vec<LayerId> },
CheckSelectedWasUpdated { path: Vec<LayerId> },
ClearSelection,
Deactivate,
Init,
ModifyFill { fill: Fill },
ModifyFont { font_family: String, font_style: String, size: f64 },
ModifyName { name: String },
ModifyStroke { stroke: Stroke },
ModifyText { new_text: String },
ModifyTransform { value: f64, transform_op: TransformOp },
ResendActiveProperties,
SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument },
UpdateSelectedDocumentProperties,
}
@@ -0,0 +1,166 @@
use super::utility_functions::{register_artboard_layer_properties, register_artwork_layer_properties};
use super::utility_types::PropertiesPanelMessageHandlerData;
use crate::messages::layout::utility_types::layout_widget::{Layout, WidgetLayout};
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::portfolio::document::properties_panel::utility_functions::apply_transform_operation;
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
use crate::messages::prelude::*;
use graphene::{LayerId, Operation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PropertiesPanelMessageHandler {
active_selection: Option<(Vec<LayerId>, TargetDocument)>,
}
impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerData<'a>> for PropertiesPanelMessageHandler {
#[remain::check]
fn process_message(&mut self, message: PropertiesPanelMessage, data: PropertiesPanelMessageHandlerData, responses: &mut VecDeque<Message>) {
let PropertiesPanelMessageHandlerData {
artwork_document,
artboard_document,
selected_layers,
font_cache,
} = data;
let get_document = |document_selector: TargetDocument| match document_selector {
TargetDocument::Artboard => artboard_document,
TargetDocument::Artwork => artwork_document,
};
use PropertiesPanelMessage::*;
match message {
SetActiveLayers { paths, document } => {
if paths.len() != 1 {
// TODO: Allow for multiple selected layers
responses.push_back(PropertiesPanelMessage::ClearSelection.into())
} else {
let path = paths.into_iter().next().unwrap();
self.active_selection = Some((path, document));
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into())
}
}
ClearSelection => {
responses.push_back(
LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout_target: LayoutTarget::PropertiesOptions,
}
.into(),
);
responses.push_back(
LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout_target: LayoutTarget::PropertiesSections,
}
.into(),
);
self.active_selection = None;
}
Deactivate => responses.push_back(
BroadcastMessage::UnsubscribeEvent {
on: BroadcastEvent::SelectionChanged,
message: Box::new(PropertiesPanelMessage::UpdateSelectedDocumentProperties.into()),
}
.into(),
),
Init => responses.push_back(
BroadcastMessage::SubscribeEvent {
on: BroadcastEvent::SelectionChanged,
send: Box::new(PropertiesPanelMessage::UpdateSelectedDocumentProperties.into()),
}
.into(),
),
ModifyFont { font_family, font_style, size } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(self.create_document_operation(Operation::ModifyFont { path, font_family, font_style, size }));
responses.push_back(ResendActiveProperties.into());
}
ModifyTransform { value, transform_op } => {
let (path, target_document) = self.active_selection.as_ref().expect("Received update for properties panel with no active layer");
let layer = get_document(*target_document).layer(path).unwrap();
let transform = apply_transform_operation(layer, transform_op, value, font_cache);
responses.push_back(self.create_document_operation(Operation::SetLayerTransform { path: path.clone(), transform }));
}
ModifyName { name } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(self.create_document_operation(Operation::SetLayerName { path, name }))
}
ModifyFill { fill } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(self.create_document_operation(Operation::SetLayerFill { path, fill }));
}
ModifyStroke { stroke } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(self.create_document_operation(Operation::SetLayerStroke { path, stroke }))
}
ModifyText { new_text } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::SetTextContent { path, new_text }.into())
}
CheckSelectedWasUpdated { path } => {
if self.matches_selected(&path) {
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into())
}
}
CheckSelectedWasDeleted { path } => {
if self.matches_selected(&path) {
self.active_selection = None;
responses.push_back(
LayoutMessage::SendLayout {
layout_target: LayoutTarget::PropertiesOptions,
layout: Layout::WidgetLayout(WidgetLayout::default()),
}
.into(),
);
responses.push_back(
LayoutMessage::SendLayout {
layout_target: LayoutTarget::PropertiesSections,
layout: Layout::WidgetLayout(WidgetLayout::default()),
}
.into(),
);
}
}
ResendActiveProperties => {
if let Some((path, target_document)) = self.active_selection.clone() {
let layer = get_document(target_document).layer(&path).unwrap();
match target_document {
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, font_cache),
TargetDocument::Artwork => register_artwork_layer_properties(layer, responses, font_cache),
}
}
}
UpdateSelectedDocumentProperties => responses.push_back(
PropertiesPanelMessage::SetActiveLayers {
paths: selected_layers.map(|path| path.to_vec()).collect(),
document: TargetDocument::Artwork,
}
.into(),
),
}
}
fn actions(&self) -> ActionList {
actions!(PropertiesMessageDiscriminant;)
}
}
impl PropertiesPanelMessageHandler {
fn matches_selected(&self, path: &[LayerId]) -> bool {
let last_active_path_id = self.active_selection.as_ref().and_then(|(v, _)| v.last().copied());
let last_modified = path.last().copied();
matches!((last_active_path_id, last_modified), (Some(active_last), Some(modified_last)) if active_last == modified_last)
}
fn create_document_operation(&self, operation: Operation) -> Message {
let (_, target_document) = self.active_selection.as_ref().unwrap();
match *target_document {
TargetDocument::Artboard => ArtboardMessage::DispatchOperation(Box::new(operation)).into(),
TargetDocument::Artwork => DocumentMessage::DispatchOperation(Box::new(operation)).into(),
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
use graphene::document::Document as GrapheneDocument;
use graphene::layers::text_layer::FontCache;
use graphene::LayerId;
use serde::{Deserialize, Serialize};
pub struct PropertiesPanelMessageHandlerData<'a> {
pub artwork_document: &'a GrapheneDocument,
pub artboard_document: &'a GrapheneDocument,
pub selected_layers: &'a mut dyn Iterator<Item = &'a [LayerId]>,
pub font_cache: &'a FontCache,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
pub enum TransformOp {
X,
Y,
ScaleX,
ScaleY,
Width,
Height,
Rotation,
}
@@ -0,0 +1,7 @@
mod transform_layer_message;
mod transform_layer_message_handler;
#[doc(inline)]
pub use transform_layer_message::{TransformLayerMessage, TransformLayerMessageDiscriminant};
#[doc(inline)]
pub use transform_layer_message_handler::TransformLayerMessageHandler;
@@ -0,0 +1,23 @@
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, DocumentMessage, TransformLayer)]
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub enum TransformLayerMessage {
// Messages
ApplyTransformOperation,
BeginGrab,
BeginRotate,
BeginScale,
CancelTransformOperation,
ConstrainX,
ConstrainY,
PointerMove { slow_key: Key, snap_key: Key },
TypeBackspace,
TypeDecimalPoint,
TypeDigit { digit: u8 },
TypeNegate,
}
@@ -0,0 +1,186 @@
use crate::consts::SLOWING_DIVISOR;
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::utility_types::layer_panel::LayerMetadata;
use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
use crate::messages::prelude::*;
use graphene::document::Document;
use graphene::LayerId;
use glam::DVec2;
use graphene::layers::text_layer::FontCache;
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TransformLayerMessageHandler {
transform_operation: TransformOperation,
slow: bool,
snap: bool,
typing: Typing,
mouse_position: ViewportPosition,
start_mouse: ViewportPosition,
original_transforms: OriginalTransforms,
pivot: DVec2,
}
type TransformData<'a> = (&'a mut HashMap<Vec<LayerId>, LayerMetadata>, &'a mut Document, &'a InputPreprocessorMessageHandler, &'a FontCache);
impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformLayerMessageHandler {
#[remain::check]
fn process_message(&mut self, message: TransformLayerMessage, (layer_metadata, document, ipp, font_cache): TransformData, responses: &mut VecDeque<Message>) {
use TransformLayerMessage::*;
let selected_layers = layer_metadata.iter().filter_map(|(layer_path, data)| data.selected.then(|| layer_path)).collect::<Vec<_>>();
let mut selected = Selected::new(&mut self.original_transforms, &mut self.pivot, &selected_layers, responses, document);
let mut begin_operation = |operation: TransformOperation, typing: &mut Typing, mouse_position: &mut DVec2, start_mouse: &mut DVec2| {
if operation != TransformOperation::None {
selected.revert_operation();
typing.clear();
} else {
*selected.pivot = selected.calculate_pivot(font_cache);
}
*mouse_position = ipp.mouse.position;
*start_mouse = ipp.mouse.position;
};
#[remain::sorted]
match message {
ApplyTransformOperation => {
self.original_transforms.clear();
self.typing.clear();
self.transform_operation = TransformOperation::None;
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
BeginGrab => {
if let TransformOperation::Grabbing(_) = self.transform_operation {
return;
}
begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
self.transform_operation = TransformOperation::Grabbing(Default::default());
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
BeginRotate => {
if let TransformOperation::Rotating(_) = self.transform_operation {
return;
}
begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
self.transform_operation = TransformOperation::Rotating(Default::default());
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
BeginScale => {
if let TransformOperation::Scaling(_) = self.transform_operation {
return;
}
begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
self.transform_operation = TransformOperation::Scaling(Default::default());
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
CancelTransformOperation => {
selected.revert_operation();
selected.original_transforms.clear();
self.typing.clear();
self.transform_operation = TransformOperation::None;
responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
ConstrainX => self.transform_operation.constrain_axis(Axis::X, &mut selected, self.snap),
ConstrainY => self.transform_operation.constrain_axis(Axis::Y, &mut selected, self.snap),
PointerMove { slow_key, snap_key } => {
self.slow = ipp.keyboard.get(slow_key as usize);
let new_snap = ipp.keyboard.get(snap_key as usize);
if new_snap != self.snap {
self.snap = new_snap;
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
}
if self.typing.digits.is_empty() {
let delta_pos = ipp.mouse.position - self.mouse_position;
match self.transform_operation {
TransformOperation::None => unreachable!(),
TransformOperation::Grabbing(translation) => {
let change = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos };
self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(change));
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
}
TransformOperation::Rotating(rotation) => {
let selected_pivot = selected.calculate_pivot(font_cache);
let angle = {
let start_offset = self.mouse_position - selected_pivot;
let end_offset = ipp.mouse.position - selected_pivot;
start_offset.angle_between(end_offset)
};
let change = if self.slow { angle / SLOWING_DIVISOR } else { angle };
self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change));
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
}
TransformOperation::Scaling(scale) => {
let change = {
let previous_frame_dist = (self.mouse_position - *selected.pivot).length();
let current_frame_dist = (ipp.mouse.position - *selected.pivot).length();
let start_transform_dist = (self.start_mouse - *selected.pivot).length();
(current_frame_dist - previous_frame_dist) / start_transform_dist
};
let change = if self.slow { change / SLOWING_DIVISOR } else { change };
self.transform_operation = TransformOperation::Scaling(scale.increment_amount(change));
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
}
};
}
self.mouse_position = ipp.mouse.position;
}
TypeBackspace => self.transform_operation.handle_typed(self.typing.type_backspace(), &mut selected, self.snap),
TypeDecimalPoint => self.transform_operation.handle_typed(self.typing.type_decimal_point(), &mut selected, self.snap),
TypeDigit { digit } => self.transform_operation.handle_typed(self.typing.type_number(digit), &mut selected, self.snap),
TypeNegate => self.transform_operation.handle_typed(self.typing.type_negate(), &mut selected, self.snap),
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(TransformLayerMessageDiscriminant;
BeginGrab,
BeginScale,
BeginRotate,
);
if self.transform_operation != TransformOperation::None {
let active = actions!(TransformLayerMessageDiscriminant;
PointerMove,
CancelTransformOperation,
ApplyTransformOperation,
TypeDigit,
TypeBackspace,
TypeDecimalPoint,
TypeNegate,
ConstrainX,
ConstrainY,
);
common.extend(active);
}
common
}
}
@@ -0,0 +1,23 @@
use super::layer_panel::LayerMetadata;
use graphene::layers::layer_info::Layer;
use serde::{Deserialize, Serialize};
#[repr(u8)]
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
pub enum Clipboard {
Internal,
_InternalClipboardCount, // Keep this as the last entry in internal clipboards since it is used for counting the number of enum variants
Device,
}
pub const INTERNAL_CLIPBOARD_COUNT: u8 = Clipboard::_InternalClipboardCount as u8;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CopyBufferEntry {
pub layer: Layer,
pub layer_metadata: LayerMetadata,
}
@@ -0,0 +1,41 @@
use graphene::color::Color;
use graphene::DocumentError;
use thiserror::Error;
/// The error type used by the Graphite editor.
#[derive(Clone, Debug, Error)]
pub enum EditorError {
#[error("Failed to execute operation:\n{0}")]
InvalidOperation(String),
#[error("Tried to construct an invalid color:\n{0:?}")]
Color(String),
#[error("The requested tool does not exist")]
UnknownTool,
#[error("The operation caused a document error:\n{0:?}")]
Document(String),
#[error("A rollback was initiated but no transaction was in progress")]
NoTransactionInProgress,
#[error("{0}")]
Misc(String),
}
macro_rules! derive_from {
($type:ty, $kind:ident) => {
impl From<$type> for EditorError {
fn from(error: $type) -> Self {
EditorError::$kind(format!("{:?}", error))
}
}
};
}
derive_from!(&str, Misc);
derive_from!(String, Misc);
derive_from!(Color, Color);
derive_from!(DocumentError, Document);
@@ -0,0 +1,92 @@
use graphene::layers::layer_info::{Layer, LayerData, LayerDataTypeDiscriminant};
use graphene::layers::style::{RenderData, ViewMode};
use graphene::layers::text_layer::FontCache;
use graphene::LayerId;
use glam::{DAffine2, DVec2};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct RawBuffer(Vec<u8>);
impl From<Vec<u64>> for RawBuffer {
fn from(iter: Vec<u64>) -> Self {
// https://github.com/rust-lang/rust-clippy/issues/4484
let v_from_raw: Vec<u8> = unsafe {
// prepare for an auto-forget of the initial vec:
let v_orig: &mut Vec<_> = &mut *std::mem::ManuallyDrop::new(iter);
Vec::from_raw_parts(v_orig.as_mut_ptr() as *mut u8, v_orig.len() * 8, v_orig.capacity() * 8)
// v_orig is never used again, so no aliasing issue
};
Self(v_from_raw)
}
}
impl Serialize for RawBuffer {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut buffer = serializer.serialize_struct("Buffer", 2)?;
buffer.serialize_field("pointer", &(self.0.as_ptr() as usize))?;
buffer.serialize_field("length", &(self.0.len()))?;
buffer.end()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy)]
pub struct LayerMetadata {
pub selected: bool,
pub expanded: bool,
}
impl LayerMetadata {
pub fn new(expanded: bool) -> Self {
Self { selected: false, expanded }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LayerPanelEntry {
pub name: String,
pub visible: bool,
pub layer_type: LayerDataTypeDiscriminant,
pub layer_metadata: LayerMetadata,
pub path: Vec<LayerId>,
pub thumbnail: String,
}
impl LayerPanelEntry {
pub fn new(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>, font_cache: &FontCache) -> Self {
let name = layer.name.clone().unwrap_or_else(|| String::from(""));
let arr = layer.data.bounding_box(transform, font_cache).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
let mut thumbnail = String::new();
let mut svg_defs = String::new();
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false);
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], render_data);
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}"><defs>{}</defs><g transform="matrix({})">{}</g></svg>"#,
x_min,
y_min,
x_max - x_min,
y_max - y_min,
svg_defs,
transform,
thumbnail,
)
} else {
String::new()
};
LayerPanelEntry {
name,
visible: layer.visible,
layer_type: (&layer.data).into(),
layer_metadata: *layer_metadata,
path,
thumbnail,
}
}
}
@@ -0,0 +1,95 @@
pub use super::layer_panel::{LayerMetadata, LayerPanelEntry};
use graphene::document::Document as GrapheneDocument;
use graphene::LayerId;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
pub type DocumentSave = (GrapheneDocument, HashMap<Vec<LayerId>, LayerMetadata>);
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum FlipAxis {
X,
Y,
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum AlignAxis {
X,
Y,
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum AlignAggregate {
Min,
Max,
Center,
Average,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
pub enum TargetDocument {
Artboard,
Artwork,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
pub enum DocumentMode {
DesignMode,
SelectMode,
GuideMode,
}
impl fmt::Display for DocumentMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
DocumentMode::DesignMode => "Design Mode".to_string(),
DocumentMode::SelectMode => "Select Mode".to_string(),
DocumentMode::GuideMode => "Guide Mode".to_string(),
};
write!(f, "{}", text)
}
}
impl DocumentMode {
pub fn icon_name(&self) -> String {
match self {
DocumentMode::DesignMode => "ViewportDesignMode".to_string(),
DocumentMode::SelectMode => "ViewportSelectMode".to_string(),
DocumentMode::GuideMode => "ViewportGuideMode".to_string(),
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)]
pub enum Platform {
#[default]
Unknown,
Windows,
Mac,
Linux,
}
impl Platform {
pub fn as_keyboard_platform_layout(&self) -> KeyboardPlatformLayout {
match self {
Platform::Mac => KeyboardPlatformLayout::Mac,
Platform::Unknown => {
log::warn!("The platform has not been set, remember to send `PortfolioMessage::SetPlatform` during editor initialization.");
KeyboardPlatformLayout::Standard
}
_ => KeyboardPlatformLayout::Standard,
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)]
pub enum KeyboardPlatformLayout {
/// Standard keyboard mapping used by Windows and Linux
#[default]
Standard,
/// Keyboard mapping used by Macs where Command is sometimes used in favor of Control
Mac,
}
@@ -0,0 +1,6 @@
pub mod clipboards;
pub mod error;
pub mod layer_panel;
pub mod misc;
pub mod transformation;
pub mod vectorize_layer_metadata;
@@ -0,0 +1,361 @@
use crate::consts::{ROTATE_SNAP_ANGLE, SCALE_SNAP_INTERVAL};
use crate::messages::prelude::*;
use graphene::document::Document;
use graphene::layers::text_layer::FontCache;
use graphene::LayerId;
use graphene::Operation as DocumentOperation;
use glam::{DAffine2, DVec2};
use std::collections::{HashMap, VecDeque};
pub type OriginalTransforms = HashMap<Vec<LayerId>, DAffine2>;
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum Axis {
Both,
X,
Y,
}
impl Default for Axis {
fn default() -> Self {
Self::Both
}
}
impl Axis {
pub fn set_or_toggle(&mut self, target: Axis) {
// If constrained to an axis and target is requesting the same axis, toggle back to Both
if *self == target {
*self = Axis::Both;
}
// If current axis is different from the target axis, switch to the target
else {
*self = target;
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Copy)]
pub struct Translation {
pub dragged_distance: DVec2,
pub typed_distance: Option<f64>,
pub constraint: Axis,
}
impl Translation {
pub fn to_dvec(self) -> DVec2 {
if let Some(value) = self.typed_distance {
if self.constraint == Axis::Y {
return DVec2::new(0., value);
} else {
return DVec2::new(value, 0.);
}
}
match self.constraint {
Axis::Both => self.dragged_distance,
Axis::X => DVec2::new(self.dragged_distance.x, 0.),
Axis::Y => DVec2::new(0., self.dragged_distance.y),
}
}
#[must_use]
pub fn increment_amount(self, delta: DVec2) -> Self {
Self {
dragged_distance: self.dragged_distance + delta,
typed_distance: None,
constraint: self.constraint,
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Copy)]
pub struct Rotation {
pub dragged_angle: f64,
pub typed_angle: Option<f64>,
}
impl Rotation {
pub fn to_f64(self, snap: bool) -> f64 {
if let Some(value) = self.typed_angle {
value.to_radians()
} else if snap {
let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
(self.dragged_angle / snap_resolution).round() * snap_resolution
} else {
self.dragged_angle
}
}
#[must_use]
pub fn increment_amount(self, delta: f64) -> Self {
Self {
dragged_angle: self.dragged_angle + delta,
typed_angle: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub struct Scale {
pub dragged_factor: f64,
pub typed_factor: Option<f64>,
pub constraint: Axis,
}
impl Default for Scale {
fn default() -> Self {
Self {
dragged_factor: 1.,
typed_factor: None,
constraint: Axis::default(),
}
}
}
impl Scale {
pub fn to_dvec(self, snap: bool) -> DVec2 {
let factor = if let Some(value) = self.typed_factor { value } else { self.dragged_factor };
let factor = if snap { (factor / SCALE_SNAP_INTERVAL).round() * SCALE_SNAP_INTERVAL } else { factor };
match self.constraint {
Axis::Both => DVec2::splat(factor),
Axis::X => DVec2::new(factor, 1.),
Axis::Y => DVec2::new(1., factor),
}
}
#[must_use]
pub fn increment_amount(self, delta: f64) -> Self {
Self {
dragged_factor: self.dragged_factor + delta,
typed_factor: None,
constraint: self.constraint,
}
}
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum TransformOperation {
None,
Grabbing(Translation),
Rotating(Rotation),
Scaling(Scale),
}
impl Default for TransformOperation {
fn default() -> Self {
TransformOperation::None
}
}
impl TransformOperation {
pub fn apply_transform_operation(&self, selected: &mut Selected, snapping: bool) {
if self != &TransformOperation::None {
let transformation = match self {
TransformOperation::Grabbing(translation) => DAffine2::from_translation(translation.to_dvec()),
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(snapping)),
TransformOperation::Scaling(scale) => DAffine2::from_scale(scale.to_dvec(snapping)),
TransformOperation::None => unreachable!(),
};
selected.update_transforms(transformation);
}
}
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, snapping: bool) {
match self {
TransformOperation::None => (),
TransformOperation::Grabbing(translation) => translation.constraint.set_or_toggle(axis),
TransformOperation::Rotating(_) => (),
TransformOperation::Scaling(scale) => scale.constraint.set_or_toggle(axis),
};
self.apply_transform_operation(selected, snapping);
}
pub fn handle_typed(&mut self, typed: Option<f64>, selected: &mut Selected, snapping: bool) {
match self {
TransformOperation::None => (),
TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
TransformOperation::Rotating(rotation) => rotation.typed_angle = typed,
TransformOperation::Scaling(scale) => scale.typed_factor = typed,
};
self.apply_transform_operation(selected, snapping);
}
}
pub struct Selected<'a> {
pub selected: &'a [&'a Vec<LayerId>],
pub responses: &'a mut VecDeque<Message>,
pub document: &'a Document,
pub original_transforms: &'a mut OriginalTransforms,
pub pivot: &'a mut DVec2,
}
impl<'a> Selected<'a> {
pub fn new(original_transforms: &'a mut OriginalTransforms, pivot: &'a mut DVec2, selected: &'a [&'a Vec<LayerId>], responses: &'a mut VecDeque<Message>, document: &'a Document) -> Self {
for path in selected {
if !original_transforms.contains_key(*path) {
if let Ok(layer) = document.layer(path) {
original_transforms.insert(path.to_vec(), layer.transform);
} else {
log::warn!("Didn't find a layer for {:?}", path);
}
}
}
Self {
selected,
responses,
document,
original_transforms,
pivot,
}
}
pub fn calculate_pivot(&mut self, font_cache: &FontCache) -> DVec2 {
let xy_summation = self
.selected
.iter()
.map(|path| {
let multiplied_transform = self.document.multiply_transforms(path).unwrap();
let bounds = self
.document
.layer(path)
.unwrap()
.aabb_for_transform(multiplied_transform, font_cache)
.unwrap_or([multiplied_transform.translation; 2]);
(bounds[0] + bounds[1]) / 2.
})
.fold(DVec2::ZERO, |summation, next| summation + next);
xy_summation / self.selected.len() as f64
}
pub fn update_transforms(&mut self, delta: DAffine2) {
if !self.selected.is_empty() {
let pivot = DAffine2::from_translation(*self.pivot);
let transformation = pivot * delta * pivot.inverse();
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
for layer_path in Document::shallowest_unique_layers(self.selected.iter()) {
let parent_folder_path = &layer_path[..layer_path.len() - 1];
let original_layer_transforms = *self.original_transforms.get(*layer_path).unwrap();
let to = self.document.generate_transform_across_scope(parent_folder_path, None).unwrap();
let new = to.inverse() * transformation * to * original_layer_transforms;
self.responses.push_back(
DocumentOperation::SetLayerTransform {
path: layer_path.to_vec(),
transform: new.to_cols_array(),
}
.into(),
);
}
self.responses.push_back(BroadcastEvent::DocumentIsDirty.into());
}
}
pub fn revert_operation(&mut self) {
for path in self.selected {
if let Some(transform) = self.original_transforms.get(*path) {
// Push front to stop document switching before sending the transform
self.responses.push_front(
DocumentOperation::SetLayerTransform {
path: path.to_vec(),
transform: transform.to_cols_array(),
}
.into(),
);
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Typing {
pub digits: Vec<u8>,
pub contains_decimal: bool,
pub negative: bool,
}
const DECIMAL_POINT: u8 = 10;
impl Typing {
pub fn type_number(&mut self, number: u8) -> Option<f64> {
self.digits.push(number);
self.evaluate()
}
pub fn type_backspace(&mut self) -> Option<f64> {
if self.digits.is_empty() {
return None;
}
match self.digits.pop() {
Some(DECIMAL_POINT) => self.contains_decimal = false,
Some(_) => (),
None => self.negative = false,
}
self.evaluate()
}
pub fn type_decimal_point(&mut self) -> Option<f64> {
if !self.contains_decimal {
self.contains_decimal = true;
self.digits.push(DECIMAL_POINT);
}
self.evaluate()
}
pub fn type_negate(&mut self) -> Option<f64> {
self.negative = !self.negative;
self.evaluate()
}
pub fn evaluate(&self) -> Option<f64> {
if self.digits.is_empty() {
return None;
}
let mut result = 0_f64;
let mut running_decimal_place = 0_i32;
for digit in &self.digits {
if *digit == DECIMAL_POINT {
if running_decimal_place == 0 {
running_decimal_place = 1;
}
} else if running_decimal_place == 0 {
result *= 10.;
result += *digit as f64;
} else {
result += *digit as f64 * 0.1_f64.powi(running_decimal_place);
running_decimal_place += 1;
}
}
if self.negative {
result = -result;
}
Some(result)
}
pub fn clear(&mut self) {
self.digits.clear();
self.contains_decimal = false;
self.negative = false;
}
}
@@ -0,0 +1,25 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::iter::FromIterator;
/// Necessary because serde can't serialize hashmaps when the keys don't implement display.
pub fn serialize<'a, T, K, V, S>(target: T, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: IntoIterator<Item = (&'a K, &'a V)>,
K: Serialize + 'a,
V: Serialize + 'a,
{
let container: Vec<_> = target.into_iter().collect();
serde::Serialize::serialize(&container, ser)
}
pub fn deserialize<'de, T, K, V, D>(des: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromIterator<(K, V)>,
K: Deserialize<'de>,
V: Deserialize<'de>,
{
let container: Vec<_> = serde::Deserialize::deserialize(des)?;
Ok(T::from_iter(container.into_iter()))
}
@@ -0,0 +1,11 @@
use crate::messages::prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, PortfolioMessage, MenuBar)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum MenuBarMessage {
// Messages
SendLayout,
}
@@ -0,0 +1,334 @@
use super::MenuBarMessage;
use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::layout_widget::{Layout, PropertyHolder};
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::layout::utility_types::widgets::menu_widgets::{MenuColumn, MenuEntry, MenuEntryGroups, MenuLayout};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::prelude::*;
use std::collections::VecDeque;
#[derive(Debug, Clone, Default)]
pub struct MenuBarMessageHandler {}
impl MessageHandler<MenuBarMessage, ()> for MenuBarMessageHandler {
#[remain::check]
fn process_message(&mut self, message: MenuBarMessage, _data: (), responses: &mut VecDeque<Message>) {
use MenuBarMessage::*;
#[remain::sorted]
match message {
SendLayout => self.register_properties(responses, LayoutTarget::MenuBar),
}
}
fn actions(&self) -> ActionList {
actions!(MenuBarMessageDiscriminant;)
}
}
impl PropertyHolder for MenuBarMessageHandler {
fn properties(&self) -> Layout {
Layout::MenuLayout(MenuLayout::new(vec![
MenuColumn {
label: "File".into(),
children: MenuEntryGroups(vec![
vec![
MenuEntry {
label: "New…".into(),
icon: Some("File".into()),
action: MenuEntry::create_action(|_| DialogMessage::RequestNewDocumentDialog.into()),
shortcut: action_keys!(DialogMessageDiscriminant::RequestNewDocumentDialog),
children: MenuEntryGroups::empty(),
},
MenuEntry {
label: "Open…".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::OpenDocument),
action: MenuEntry::create_action(|_| PortfolioMessage::OpenDocument.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Open Recent".into(),
shortcut: None,
action: MenuEntry::no_action(),
icon: None,
children: MenuEntryGroups(vec![
vec![
MenuEntry {
label: "Reopen Last Closed".into(),
// shortcut: [Key::KeyControl, Key::KeyShift, Key::KeyT],
..MenuEntry::default()
},
MenuEntry {
label: "Clear Recently Opened".into(),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Some Recent File.gdd".into(),
..MenuEntry::default()
},
MenuEntry {
label: "Another Recent File.gdd".into(),
..MenuEntry::default()
},
MenuEntry {
label: "An Older File.gdd".into(),
..MenuEntry::default()
},
MenuEntry {
label: "Some Other Older File.gdd".into(),
..MenuEntry::default()
},
MenuEntry {
label: "Yet Another Older File.gdd".into(),
..MenuEntry::default()
},
],
]),
},
],
vec![
MenuEntry {
label: "Close".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::CloseActiveDocumentWithConfirmation),
action: MenuEntry::create_action(|_| PortfolioMessage::CloseActiveDocumentWithConfirmation.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Close All".into(),
shortcut: action_keys!(DialogMessageDiscriminant::CloseAllDocumentsWithConfirmation),
action: MenuEntry::create_action(|_| DialogMessage::CloseAllDocumentsWithConfirmation.into()),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Save".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
action: MenuEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Save As…".into(),
// shortcut: [Key::KeyControl, Key::KeyShift, Key::KeyS],
..MenuEntry::default()
},
MenuEntry {
label: "Save All".into(),
// shortcut: [Key::KeyControl, Key::KeyAlt, Key::KeyS],
..MenuEntry::default()
},
MenuEntry {
label: "Auto-Save".into(),
icon: Some("CheckboxChecked".into()),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Import…".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::Import),
action: MenuEntry::create_action(|_| PortfolioMessage::Import.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Export…".into(),
shortcut: action_keys!(DialogMessageDiscriminant::RequestExportDialog),
action: MenuEntry::create_action(|_| DialogMessage::RequestExportDialog.into()),
..MenuEntry::default()
},
],
vec![MenuEntry {
label: "Quit".into(),
// shortcut: [Key::KeyControl, Key::KeyQ],
..MenuEntry::default()
}],
]),
},
MenuColumn {
label: "Edit".into(),
children: MenuEntryGroups(vec![
vec![
MenuEntry {
label: "Undo".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::Undo),
action: MenuEntry::create_action(|_| DocumentMessage::Undo.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Redo".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::Redo),
action: MenuEntry::create_action(|_| DocumentMessage::Redo.into()),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Cut".into(),
shortcut: action_keys!(PortfolioMessageDiscriminant::Cut),
action: MenuEntry::create_action(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Copy".into(),
icon: Some("Copy".into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::Copy),
action: MenuEntry::create_action(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Paste".into(),
icon: Some("Paste".into()),
shortcut: action_keys!(FrontendMessageDiscriminant::TriggerPaste),
action: MenuEntry::create_action(|_| FrontendMessage::TriggerPaste.into()),
..MenuEntry::default()
},
],
]),
},
MenuColumn {
label: "Layer".into(),
children: MenuEntryGroups(vec![vec![
MenuEntry {
label: "Select All".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectAllLayers),
action: MenuEntry::create_action(|_| DocumentMessage::SelectAllLayers.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Deselect All".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::DeselectAllLayers),
action: MenuEntry::create_action(|_| DocumentMessage::DeselectAllLayers.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Order".into(),
action: MenuEntry::no_action(),
children: MenuEntryGroups(vec![vec![
MenuEntry {
label: "Raise To Front".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaiseToFront),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersRaiseToFront.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Raise".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaise),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersRaise.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Lower".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLower),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersLower.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Lower to Back".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLowerToBack),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersLowerToBack.into()),
..MenuEntry::default()
},
]]),
..MenuEntry::default()
},
]]),
},
MenuColumn {
label: "Document".into(),
children: MenuEntryGroups(vec![vec![MenuEntry {
label: "Menu entries coming soon".into(),
..MenuEntry::default()
}]]),
},
MenuColumn {
label: "View".into(),
children: MenuEntryGroups(vec![vec![MenuEntry {
label: "Show/Hide Node Graph (In Development)".into(),
action: MenuEntry::create_action(|_| WorkspaceMessage::NodeGraphToggleVisibility.into()),
..MenuEntry::default()
}]]),
},
MenuColumn {
label: "Help".into(),
children: MenuEntryGroups(vec![
vec![MenuEntry {
label: "About Graphite".into(),
action: MenuEntry::create_action(|_| DialogMessage::RequestAboutGraphiteDialog.into()),
..MenuEntry::default()
}],
vec![
MenuEntry {
label: "Report a Bug".into(),
action: MenuEntry::create_action(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(),
}
.into()
}),
..MenuEntry::default()
},
MenuEntry {
label: "Visit on GitHub".into(),
action: MenuEntry::create_action(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite".into(),
}
.into()
}),
..MenuEntry::default()
},
],
vec![
MenuEntry {
label: "Debug: Print Messages".into(),
action: MenuEntry::no_action(),
children: MenuEntryGroups(vec![vec![
MenuEntry {
label: "Off".into(),
// icon: Some("Checkmark".into()), // TODO: Find a way to set this icon on the active mode
shortcut: action_keys!(DebugMessageDiscriminant::MessageOff),
action: MenuEntry::create_action(|_| DebugMessage::MessageOff.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Only Names".into(),
shortcut: action_keys!(DebugMessageDiscriminant::MessageNames),
action: MenuEntry::create_action(|_| DebugMessage::MessageNames.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Full Contents".into(),
shortcut: action_keys!(DebugMessageDiscriminant::MessageContents),
action: MenuEntry::create_action(|_| DebugMessage::MessageContents.into()),
..MenuEntry::default()
},
]]),
..MenuEntry::default()
},
MenuEntry {
label: "Debug: Print Trace Logs".into(),
icon: Some(if let log::LevelFilter::Trace = log::max_level() { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(DebugMessageDiscriminant::ToggleTraceLogs),
action: MenuEntry::create_action(|_| DebugMessage::ToggleTraceLogs.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Debug: Print Document".into(),
shortcut: action_keys!(DocumentMessageDiscriminant::DebugPrintDocument),
action: MenuEntry::create_action(|_| DocumentMessage::DebugPrintDocument.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Debug: Panic (DANGER)".into(),
action: MenuEntry::create_action(|_| panic!()),
..MenuEntry::default()
},
],
]),
},
]))
}
}
@@ -0,0 +1,7 @@
mod menu_bar_message;
mod menu_bar_message_handler;
#[doc(inline)]
pub use menu_bar_message::{MenuBarMessage, MenuBarMessageDiscriminant};
#[doc(inline)]
pub use menu_bar_message_handler::MenuBarMessageHandler;
+10
View File
@@ -0,0 +1,10 @@
mod portfolio_message;
mod portfolio_message_handler;
pub mod document;
pub mod menu_bar;
#[doc(inline)]
pub use portfolio_message::{PortfolioMessage, PortfolioMessageDiscriminant};
#[doc(inline)]
pub use portfolio_message_handler::PortfolioMessageHandler;
@@ -0,0 +1,92 @@
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::document::utility_types::misc::Platform;
use crate::messages::prelude::*;
use graphene::layers::text_layer::Font;
use graphene::LayerId;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[impl_message(Message, Portfolio)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum PortfolioMessage {
// Sub-messages
#[remain::unsorted]
#[child]
Document(DocumentMessage),
#[remain::unsorted]
#[child]
MenuBar(MenuBarMessage),
// Messages
AutoSaveActiveDocument,
AutoSaveDocument {
document_id: u64,
},
CloseActiveDocumentWithConfirmation,
CloseAllDocuments,
CloseDocument {
document_id: u64,
},
CloseDocumentWithConfirmation {
document_id: u64,
},
Copy {
clipboard: Clipboard,
},
Cut {
clipboard: Clipboard,
},
DestroyAllDocuments,
FontLoaded {
font_family: String,
font_style: String,
preview_url: String,
data: Vec<u8>,
is_default: bool,
},
Import,
LoadFont {
font: Font,
is_default: bool,
},
NewDocumentWithName {
name: String,
},
NextDocument,
OpenDocument,
OpenDocumentFile {
document_name: String,
document_serialized_content: String,
},
OpenDocumentFileWithId {
document_id: u64,
document_name: String,
document_is_saved: bool,
document_serialized_content: String,
},
Paste {
clipboard: Clipboard,
},
PasteIntoFolder {
clipboard: Clipboard,
folder_path: Vec<LayerId>,
insert_index: isize,
},
PasteSerializedData {
data: String,
},
PrevDocument,
SelectDocument {
document_id: u64,
},
SetActiveDocument {
document_id: u64,
},
SetPlatform {
platform: Platform,
},
UpdateDocumentWidgets,
UpdateOpenDocumentsList,
}
@@ -0,0 +1,526 @@
use crate::application::generate_uuid;
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::FrontendDocumentDetails;
use crate::messages::layout::utility_types::layout_widget::PropertyHolder;
use crate::messages::layout::utility_types::misc::LayoutTarget;
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::utility_types::misc::Platform;
use crate::messages::prelude::*;
use graphene::layers::layer_info::LayerDataTypeDiscriminant;
use graphene::layers::text_layer::{Font, FontCache};
use graphene::Operation as DocumentOperation;
use log::warn;
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Default)]
pub struct PortfolioMessageHandler {
menu_bar_message_handler: MenuBarMessageHandler,
documents: HashMap<u64, DocumentMessageHandler>,
document_ids: Vec<u64>,
active_document_id: Option<u64>,
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
font_cache: FontCache,
pub platform: Platform,
}
impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for PortfolioMessageHandler {
#[remain::check]
fn process_message(&mut self, message: PortfolioMessage, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
use DocumentMessage::*;
use PortfolioMessage::*;
#[remain::sorted]
match message {
// Sub-messages
#[remain::unsorted]
Document(message) => {
if let Some(document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) {
document.process_message(message, (ipp, &self.font_cache), responses)
}
}
#[remain::unsorted]
MenuBar(message) => self.menu_bar_message_handler.process_message(message, (), responses),
// Messages
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(
FrontendMessage::TriggerIndexedDbWriteDocument {
document: document.serialize_document(),
details: FrontendDocumentDetails {
is_saved: document.is_saved(),
id: document_id,
name: document.name.clone(),
},
version: GRAPHITE_DOCUMENT_VERSION.to_string(),
}
.into(),
)
}
CloseActiveDocumentWithConfirmation => {
if let Some(document_id) = self.active_document_id {
responses.push_back(PortfolioMessage::CloseDocumentWithConfirmation { document_id }.into());
}
}
CloseAllDocuments => {
if self.active_document_id.is_some() {
responses.push_back(PropertiesPanelMessage::Deactivate.into());
responses.push_back(BroadcastEvent::ToolAbort.into());
responses.push_back(ToolMessage::DeactivateTools.into());
}
for document_id in &self.document_ids {
responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id: *document_id }.into());
}
responses.push_back(PortfolioMessage::DestroyAllDocuments.into());
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
}
CloseDocument { document_id } => {
let document_index = self.document_index(document_id);
self.documents.remove(&document_id);
self.document_ids.remove(document_index);
if self.document_ids.is_empty() {
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(),
);
}
}
// Send the new list of document tab names
responses.push_back(UpdateOpenDocumentsList.into());
responses.push_back(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id }.into());
responses.push_back(RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.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 } => {
let target_document = self.documents.get(&document_id).unwrap();
if target_document.is_saved() {
responses.push_back(BroadcastEvent::ToolAbort.into());
responses.push_back(PortfolioMessage::CloseDocument { document_id }.into());
} else {
let dialog = simple_dialogs::CloseDocumentDialog {
document_name: target_document.name.clone(),
document_id,
};
dialog.register_properties(responses, LayoutTarget::DialogDetails);
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
// Select the document being closed
responses.push_back(PortfolioMessage::SelectDocument { document_id }.into());
}
}
Copy { clipboard } => {
// We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self`
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),
}
}
};
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,
preview_url,
data,
is_default,
} => {
self.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default);
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());
}
}
Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
if self.active_document().is_some() {
responses.push_back(FrontendMessage::TriggerImport.into());
}
}
LoadFont { font, is_default } => {
if !self.font_cache.loaded_font(&font) {
responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default }.into());
}
}
NewDocumentWithName { name } => {
let new_document = DocumentMessageHandler::with_name(name, ipp);
let document_id = generate_uuid();
if self.active_document().is_some() {
responses.push_back(BroadcastEvent::ToolAbort.into());
responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
}
self.load_document(new_document, document_id, responses);
}
NextDocument => {
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());
}
}
OpenDocument => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
responses.push_back(FrontendMessage::TriggerOpenDocument.into());
}
OpenDocumentFile {
document_name,
document_serialized_content,
} => {
responses.push_back(
PortfolioMessage::OpenDocumentFileWithId {
document_id: generate_uuid(),
document_name,
document_is_saved: true,
document_serialized_content,
}
.into(),
);
}
OpenDocumentFileWithId {
document_id,
document_name,
document_is_saved,
document_serialized_content,
} => {
let document = DocumentMessageHandler::with_name_and_content(document_name, document_serialized_content);
match document {
Ok(mut document) => {
document.set_save_state(document_is_saved);
self.load_document(document, document_id, responses);
}
Err(e) => responses.push_back(
DialogMessage::DisplayDialogError {
title: "Failed to open document".to_string(),
description: e.to_string(),
}
.into(),
),
}
}
Paste { clipboard } => {
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,
folder_path: path,
insert_index,
} => {
let paste = |entry: &CopyBufferEntry, responses: &mut VecDeque<_>| {
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();
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 {
for entry in self.copy_buffer[clipboard as usize].iter().rev() {
paste(entry, responses)
}
} else {
for entry in self.copy_buffer[clipboard as usize].iter() {
paste(entry, responses)
}
}
}
PasteSerializedData { data } => {
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();
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(),
);
}
}
if self.active_document().is_some() {
responses.push_back(BroadcastEvent::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());
for layer in self.documents.get(&document_id).unwrap().layer_metadata.keys() {
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
}
responses.push_back(BroadcastEvent::SelectionChanged.into());
responses.push_back(BroadcastEvent::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 = Some(document_id),
SetPlatform { platform } => self.platform = platform,
UpdateDocumentWidgets => {
if let Some(document) = self.active_document() {
document.update_document_widgets(responses);
}
}
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 {
let mut common = actions!(PortfolioMessageDiscriminant;
CloseActiveDocumentWithConfirmation,
CloseAllDocuments,
Import,
NextDocument,
OpenDocument,
Paste,
PasteIntoFolder,
PrevDocument,
);
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
}
}
impl PortfolioMessageHandler {
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) -> Option<&mut DocumentMessageHandler> {
self.active_document_id.and_then(|id| self.documents.get_mut(&id))
}
pub fn generate_new_document_name(&self) -> String {
let mut doc_title_numbers = self
.ordered_document_iterator()
.filter_map(|doc| {
doc.name
.rsplit_once(DEFAULT_DOCUMENT_NAME)
.map(|(prefix, number)| (prefix.is_empty()).then(|| number.trim().parse::<isize>().ok()).flatten().unwrap_or(1))
})
.collect::<Vec<isize>>();
doc_title_numbers.sort_unstable();
doc_title_numbers.iter_mut().enumerate().for_each(|(i, number)| *number = *number - i as isize - 2);
// Uses binary search to find the index of the element where number is bigger than i
let new_doc_title_num = doc_title_numbers.binary_search(&0).map_or_else(|e| e, |v| v) + 1;
match new_doc_title_num {
1 => DEFAULT_DOCUMENT_NAME.to_string(),
_ => format!("{} {}", DEFAULT_DOCUMENT_NAME, new_doc_title_num),
}
}
// 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, responses: &mut VecDeque<Message>) {
self.document_ids.push(document_id);
responses.extend(
new_document
.layer_metadata
.keys()
.filter_map(|path| new_document.layer_panel_entry_from_path(path, &self.font_cache))
.map(|entry| FrontendMessage::UpdateDocumentLayerDetails { data: entry }.into())
.collect::<Vec<_>>(),
);
new_document.update_layer_tree_options_bar_widgets(responses, &self.font_cache);
new_document.load_layer_resources(responses, &new_document.graphene_document.root.data, Vec::new());
self.documents.insert(document_id, new_document);
if self.active_document().is_some() {
responses.push_back(PropertiesPanelMessage::Deactivate.into());
responses.push_back(BroadcastEvent::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.
pub fn ordered_document_iterator(&self) -> impl Iterator<Item = &DocumentMessageHandler> {
self.document_ids.iter().map(|id| self.documents.get(id).expect("document id was not found in the document hashmap"))
}
fn document_index(&self, document_id: u64) -> usize {
self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids")
}
pub fn font_cache(&self) -> &FontCache {
&self.font_cache
}
}