Hook up layer tree structure with frontend (#372)

* Hook up layer tree structure with frontend (decoding and Vue are WIP)

* Fix off by one error

* Avoid leaking memory

* Parse layer structure into list of layers

* Fix thumbnail updates

* Correctly popagate deletions

* Fix selection state in layer tree

* Respect expansion during root serialization

* Allow expanding of subfolders

* Fix arrow direction

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Keavon Chambers 2021-09-11 17:15:51 -07:00
parent c5f44a8c1d
commit 225b46300d
19 changed files with 15777 additions and 336 deletions

View file

@ -21,7 +21,7 @@ const GROUP_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)), MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)),
MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)), MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateLayer), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateLayer),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::ExpandFolder), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::DisplayFolderTreeStructure),
MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectedLayersChanged), MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectedLayersChanged),
]; ];
@ -124,10 +124,10 @@ mod test {
init_logger(); init_logger();
let mut editor = create_editor_with_three_layers(); let mut editor = create_editor_with_three_layers();
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let document_before_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
editor.handle_message(DocumentsMessage::Copy); editor.handle_message(DocumentsMessage::Copy);
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }); editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let document_after_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers(); let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers(); let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
@ -154,14 +154,14 @@ mod test {
init_logger(); init_logger();
let mut editor = create_editor_with_three_layers(); let mut editor = create_editor_with_three_layers();
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let document_before_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1]; let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![shape_id]])); editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![shape_id]]));
editor.handle_message(DocumentsMessage::Copy); editor.handle_message(DocumentsMessage::Copy);
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }); editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let document_after_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers(); let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers(); let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
@ -193,7 +193,7 @@ mod test {
editor.handle_message(DocumentMessage::CreateFolder(vec![])); editor.handle_message(DocumentMessage::CreateFolder(vec![]));
let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().document.clone(); let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX]; let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];
// TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils. // TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils.
@ -215,14 +215,14 @@ mod test {
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![folder_id]])); editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![folder_id]]));
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let document_before_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
editor.handle_message(DocumentsMessage::Copy); editor.handle_message(DocumentsMessage::Copy);
editor.handle_message(DocumentMessage::DeleteSelectedLayers); editor.handle_message(DocumentMessage::DeleteSelectedLayers);
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }); editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }); editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let document_after_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers(); let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers(); let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
@ -273,7 +273,7 @@ mod test {
const SHAPE_INDEX: usize = 1; const SHAPE_INDEX: usize = 1;
const RECT_INDEX: usize = 0; const RECT_INDEX: usize = 0;
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let document_before_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX]; let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX];
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX]; let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
@ -284,7 +284,7 @@ mod test {
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }); editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }); editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 });
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone(); let document_after_copy = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers(); let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers(); let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();

View file

@ -1,26 +1,26 @@
pub use super::layer_panel::*;
use crate::{
consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING},
EditorError,
};
use glam::{DAffine2, DVec2};
use graphene::{document::Document as InternalDocument, layers::LayerDataType, DocumentError, LayerId};
use kurbo::PathSeg;
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use graphene::layers::BlendMode;
use graphene::{DocumentResponse, Operation as DocumentOperation};
use log::warn;
use std::collections::VecDeque; use std::collections::VecDeque;
pub use super::layer_panel::*;
use super::movement_handler::{MovementMessage, MovementMessageHandler}; use super::movement_handler::{MovementMessage, MovementMessageHandler};
use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler}; use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler};
type DocumentSave = (InternalDocument, HashMap<Vec<LayerId>, LayerData>); use crate::consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING};
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use crate::EditorError;
use glam::{DAffine2, DVec2};
use graphene::layers::Folder;
use kurbo::PathSeg;
use log::warn;
use serde::{Deserialize, Serialize};
use graphene::layers::BlendMode;
use graphene::{document::Document as GrapheneDocument, layers::LayerDataType, DocumentError, LayerId};
use graphene::{DocumentResponse, Operation as DocumentOperation};
type DocumentSave = (GrapheneDocument, HashMap<Vec<LayerId>, LayerData>);
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum FlipAxis { pub enum FlipAxis {
@ -58,7 +58,7 @@ pub struct VectorManipulatorShape {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DocumentMessageHandler { pub struct DocumentMessageHandler {
pub document: InternalDocument, pub graphene_document: GrapheneDocument,
pub document_history: Vec<DocumentSave>, pub document_history: Vec<DocumentSave>,
pub document_redo_history: Vec<DocumentSave>, pub document_redo_history: Vec<DocumentSave>,
pub name: String, pub name: String,
@ -70,7 +70,7 @@ pub struct DocumentMessageHandler {
impl Default for DocumentMessageHandler { impl Default for DocumentMessageHandler {
fn default() -> Self { fn default() -> Self {
Self { Self {
document: InternalDocument::default(), graphene_document: GrapheneDocument::default(),
document_history: Vec::new(), document_history: Vec::new(),
document_redo_history: Vec::new(), document_redo_history: Vec::new(),
name: String::from("Untitled Document"), name: String::from("Untitled Document"),
@ -105,6 +105,8 @@ pub enum DocumentMessage {
FlipSelectedLayers(FlipAxis), FlipSelectedLayers(FlipAxis),
ToggleLayerExpansion(Vec<LayerId>), ToggleLayerExpansion(Vec<LayerId>),
FolderChanged(Vec<LayerId>), FolderChanged(Vec<LayerId>),
LayerChanged(Vec<LayerId>),
DocumentStructureChanged,
StartTransaction, StartTransaction,
RollbackTransaction, RollbackTransaction,
GroupSelectedLayers, GroupSelectedLayers,
@ -140,39 +142,26 @@ impl From<DocumentOperation> for Message {
} }
impl DocumentMessageHandler { impl DocumentMessageHandler {
pub fn handle_folder_changed(&mut self, path: Vec<LayerId>) -> Option<Message> {
let _ = self.document.render_root();
self.layer_data(&path).expanded.then(|| {
let children = self.layer_panel(path.as_slice()).expect("The provided Path was not valid");
FrontendMessage::ExpandFolder { path: path.into(), children }.into()
})
}
fn clear_selection(&mut self) {
self.layer_data.values_mut().for_each(|layer_data| layer_data.selected = false);
}
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> { fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
if self.document.layer(path).ok()?.overlay { if self.graphene_document.layer(path).ok()?.overlay {
return None; return None;
} }
self.layer_data(path).selected = true; self.layer_data(path).selected = true;
let data = self.layer_panel_entry(path.to_vec()).ok()?; let data = self.layer_panel_entry(path.to_vec()).ok()?;
// TODO: Add deduplication
(!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec().into(), data }.into()) (!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec().into(), data }.into())
} }
pub fn selected_layers_bounding_box(&self) -> Option<[DVec2; 2]> { pub fn selected_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
let paths = self.selected_layers().map(|vec| &vec[..]); let paths = self.selected_layers();
self.document.combined_viewport_bounding_box(paths) self.graphene_document.combined_viewport_bounding_box(paths)
} }
// TODO: Consider moving this to some kind of overlay manager in the future // TODO: Consider moving this to some kind of overlay manager in the future
pub fn selected_layers_vector_points(&self) -> Vec<VectorManipulatorShape> { pub fn selected_layers_vector_points(&self) -> Vec<VectorManipulatorShape> {
let shapes = self.selected_layers().filter_map(|path_to_shape| { let shapes = self.selected_layers().filter_map(|path_to_shape| {
let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape).ok()?; let viewport_transform = self.graphene_document.generate_transform_relative_to_viewport(path_to_shape).ok()?;
let shape = match &self.document.layer(path_to_shape).ok()?.data { let shape = match &self.graphene_document.layer(path_to_shape).ok()?.data {
LayerDataType::Shape(shape) => Some(shape), LayerDataType::Shape(shape) => Some(shape),
LayerDataType::Folder(_) => None, LayerDataType::Folder(_) => None,
}?; }?;
@ -214,6 +203,59 @@ impl DocumentMessageHandler {
self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice())) self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice()))
} }
fn serialize_structure(&self, folder: &Folder, structure: &mut Vec<u64>, data: &mut Vec<LayerId>, path: &mut Vec<LayerId>) {
let mut space = 0;
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
data.push(*id);
space += 1;
match layer.data {
LayerDataType::Shape(_) => (),
LayerDataType::Folder(ref folder) => {
path.push(*id);
if self.layerdata(path).expanded {
structure.push(space);
self.serialize_structure(folder, structure, data, path);
space = 0;
}
path.pop();
}
}
}
structure.push(space | 1 << 63);
}
/// Serializes the layer structure into a compressed 1d structure
///
/// It is a string of numbers broken into three sections:
/// (4),(2,1,-2,-0),(16533113728871998040,3427872634365736244,18115028555707261608,15878401910454357952,449479075714955186) <- Example encoded data
/// L = 4 = structure.len() <- First value in the encoding: L, the length of the structure section
/// structure = 2,1,-2,-0 <- Subsequent L values: structure section
/// data = 16533113728871998040,3427872634365736244,18115028555707261608,15878401910454357952,449479075714955186 <- Remaining values: data section (layer IDs)
///
/// The data section lists the layer IDs for all folders/layers in the tree as read from top to bottom.
/// The structure section lists signed numbers. The sign indicates a folder indentation change (+ is down a level, - is up a level).
/// the numbers in the structure block encode the indentation,
/// 2 mean read two element from the data section, then place a [
/// -x means read x elements from the data section and then insert a ]
///
/// 2 V 1 V -2 A -0 A
/// 16533113728871998040,3427872634365736244, 18115028555707261608, 15878401910454357952,449479075714955186
/// 16533113728871998040,3427872634365736244,[ 18115028555707261608,[15878401910454357952,449479075714955186] ]
///
/// resulting layer panel:
/// 16533113728871998040
/// 3427872634365736244
/// [3427872634365736244,18115028555707261608]
/// [3427872634365736244,18115028555707261608,15878401910454357952]
/// [3427872634365736244,18115028555707261608,449479075714955186]
pub fn serialize_root(&self) -> Vec<u64> {
let (mut structure, mut data) = (vec![0], Vec::new());
self.serialize_structure(self.graphene_document.root.as_folder().unwrap(), &mut structure, &mut data, &mut vec![]);
structure[0] = structure.len() as u64 - 1;
structure.extend(data);
structure
}
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers. /// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> { fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> {
// Compute the indices for each layer to be able to sort them // Compute the indices for each layer to be able to sort them
@ -227,7 +269,7 @@ impl DocumentMessageHandler {
// Currently it is possible that layer_data contains layers that are don't actually exist (has been partially fixed in #281) // Currently it is possible that layer_data contains layers that are don't actually exist (has been partially fixed in #281)
// and thus indices_for_path can return an error. We currently skip these layers and log a warning. // and thus indices_for_path can return an error. We currently skip these layers and log a warning.
// Once this problem is solved this code can be simplified // Once this problem is solved this code can be simplified
match self.document.indices_for_path(&path) { match self.graphene_document.indices_for_path(&path) {
Err(err) => { Err(err) => {
warn!("layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err); warn!("layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err);
None None
@ -259,7 +301,7 @@ impl DocumentMessageHandler {
pub fn with_name(name: String) -> Self { pub fn with_name(name: String) -> Self {
Self { Self {
document: InternalDocument::default(), graphene_document: GrapheneDocument::default(),
document_history: Vec::new(), document_history: Vec::new(),
document_redo_history: Vec::new(), document_redo_history: Vec::new(),
name, name,
@ -271,10 +313,10 @@ impl DocumentMessageHandler {
pub fn with_name_and_content(name: String, serialized_content: String) -> Result<Self, EditorError> { pub fn with_name_and_content(name: String, serialized_content: String) -> Result<Self, EditorError> {
let mut document = Self::with_name(name); let mut document = Self::with_name(name);
let internal_document = InternalDocument::with_content(&serialized_content); let internal_document = GrapheneDocument::with_content(&serialized_content);
match internal_document { match internal_document {
Ok(handle) => { Ok(handle) => {
document.document = handle; document.graphene_document = handle;
Ok(document) Ok(document)
} }
Err(DocumentError::InvalidFile(msg)) => Err(EditorError::Document(msg)), Err(DocumentError::InvalidFile(msg)) => Err(EditorError::Document(msg)),
@ -291,9 +333,9 @@ impl DocumentMessageHandler {
let new_layer_data = self let new_layer_data = self
.layer_data .layer_data
.iter() .iter()
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value))) .filter_map(|(key, value)| (!self.graphene_document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
.collect(); .collect();
self.document_history.push((self.document.clone_without_overlays(), new_layer_data)) self.document_history.push((self.graphene_document.clone_without_overlays(), new_layer_data))
} }
pub fn rollback(&mut self) -> Result<(), EditorError> { pub fn rollback(&mut self) -> Result<(), EditorError> {
@ -304,7 +346,7 @@ impl DocumentMessageHandler {
pub fn undo(&mut self) -> Result<(), EditorError> { pub fn undo(&mut self) -> Result<(), EditorError> {
match self.document_history.pop() { match self.document_history.pop() {
Some((document, layer_data)) => { Some((document, layer_data)) => {
let document = std::mem::replace(&mut self.document, document); let document = std::mem::replace(&mut self.graphene_document, document);
let layer_data = std::mem::replace(&mut self.layer_data, layer_data); let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
self.document_redo_history.push((document, layer_data)); self.document_redo_history.push((document, layer_data));
Ok(()) Ok(())
@ -316,11 +358,11 @@ impl DocumentMessageHandler {
pub fn redo(&mut self) -> Result<(), EditorError> { pub fn redo(&mut self) -> Result<(), EditorError> {
match self.document_redo_history.pop() { match self.document_redo_history.pop() {
Some((document, layer_data)) => { Some((document, layer_data)) => {
let document = std::mem::replace(&mut self.document, document); let document = std::mem::replace(&mut self.graphene_document, document);
let layer_data = std::mem::replace(&mut self.layer_data, layer_data); let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
let new_layer_data = layer_data let new_layer_data = layer_data
.iter() .iter()
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value))) .filter_map(|(key, value)| (!self.graphene_document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
.collect(); .collect();
self.document_history.push((document.clone_without_overlays(), new_layer_data)); self.document_history.push((document.clone_without_overlays(), new_layer_data));
Ok(()) Ok(())
@ -331,18 +373,18 @@ impl DocumentMessageHandler {
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> { pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
let data: LayerData = *layer_data(&mut self.layer_data, &path); let data: LayerData = *layer_data(&mut self.layer_data, &path);
let layer = self.document.layer(&path)?; let layer = self.graphene_document.layer(&path)?;
let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path)?, layer, path); let entry = layer_panel_entry(&data, self.graphene_document.multiply_transforms(&path)?, layer, path);
Ok(entry) Ok(entry)
} }
/// Returns a list of `LayerPanelEntry`s intended for display purposes. These don't contain /// Returns a list of `LayerPanelEntry`s intended for display purposes. These don't contain
/// any actual data, but rather attributes such as visibility and names of the layers. /// any actual data, but rather attributes such as visibility and names of the layers.
pub fn layer_panel(&mut self, path: &[LayerId]) -> Result<Vec<LayerPanelEntry>, EditorError> { pub fn layer_panel(&mut self, path: &[LayerId]) -> Result<Vec<LayerPanelEntry>, EditorError> {
let folder = self.document.folder(path)?; let folder = self.graphene_document.folder(path)?;
let paths: Vec<Vec<LayerId>> = folder.layer_ids.iter().map(|id| [path, &[*id]].concat()).collect(); let paths: Vec<Vec<LayerId>> = folder.layer_ids.iter().map(|id| [path, &[*id]].concat()).collect();
let data: Vec<LayerData> = paths.iter().map(|path| *layer_data(&mut self.layer_data, path)).collect(); let data: Vec<LayerData> = paths.iter().map(|path| *layer_data(&mut self.layer_data, path)).collect();
let folder = self.document.folder(path)?; let folder = self.graphene_document.folder(path)?;
let entries = folder let entries = folder
.layers() .layers()
.iter() .iter()
@ -352,7 +394,9 @@ impl DocumentMessageHandler {
.map(|(layer, (path, data))| { .map(|(layer, (path, data))| {
layer_panel_entry( layer_panel_entry(
&data, &data,
self.document.generate_transform_across_scope(path, Some(self.document.root.transform.inverse())).unwrap(), self.graphene_document
.generate_transform_across_scope(path, Some(self.graphene_document.root.transform.inverse()))
.unwrap(),
layer, layer,
path.to_vec(), path.to_vec(),
) )
@ -366,21 +410,25 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
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::*;
match message { match message {
Movement(message) => self.movement_handler.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.document, ipp), responses), Movement(message) => self
TransformLayers(message) => self.transform_layer_handler.process_action(message, (&mut self.layer_data, &mut self.document, ipp), responses), .movement_handler
.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.graphene_document, ipp), responses),
TransformLayers(message) => self
.transform_layer_handler
.process_action(message, (&mut self.layer_data, &mut self.graphene_document, ipp), responses),
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()), DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
StartTransaction => self.backup(), StartTransaction => self.backup(),
RollbackTransaction => { RollbackTransaction => {
self.rollback().unwrap_or_else(|e| log::warn!("{}", e)); self.rollback().unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]); responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
} }
AbortTransaction => { AbortTransaction => {
self.undo().unwrap_or_else(|e| log::warn!("{}", e)); self.undo().unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]); responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
} }
CommitTransaction => (), CommitTransaction => (),
ExportDocument => { ExportDocument => {
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]); let bbox = self.graphene_document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]);
let size = bbox[1] - bbox[0]; let size = bbox[1] - bbox[0];
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) { let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX), true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX),
@ -395,7 +443,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
size.x, size.x,
size.y, size.y,
"\n", "\n",
self.document.render_root() self.graphene_document.render_root()
), ),
name, name,
} }
@ -409,7 +457,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}; };
responses.push_back( responses.push_back(
FrontendMessage::SaveDocument { FrontendMessage::SaveDocument {
document: self.document.serialize_document(), document: self.graphene_document.serialize_document(),
name, name,
} }
.into(), .into(),
@ -422,7 +470,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_back(DocumentOperation::CreateFolder { path }.into()) responses.push_back(DocumentOperation::CreateFolder { path }.into())
} }
GroupSelectedLayers => { GroupSelectedLayers => {
let common_prefix = self.document.common_prefix(self.selected_layers()); let common_prefix = self.graphene_document.common_prefix(self.selected_layers());
let (_id, common_prefix) = common_prefix.split_last().unwrap_or((&0, &[])); let (_id, common_prefix) = common_prefix.split_last().unwrap_or((&0, &[]));
let mut new_folder_path = common_prefix.to_vec(); let mut new_folder_path = common_prefix.to_vec();
@ -460,11 +508,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
ToggleLayerExpansion(path) => { ToggleLayerExpansion(path) => {
self.layer_data(&path).expanded ^= true; self.layer_data(&path).expanded ^= true;
match self.layer_data(&path).expanded { responses.push_back(DocumentStructureChanged.into());
true => responses.push_back(FolderChanged(path.clone()).into()), responses.push_back(LayerChanged(path).into())
false => responses.push_back(FrontendMessage::CollapseFolder { path: path.clone().into() }.into()),
}
responses.extend(self.layer_panel_entry(path.clone()).ok().map(|data| FrontendMessage::UpdateLayer { path: path.into(), data }.into()));
} }
SelectionChanged => { SelectionChanged => {
// TODO: Hoist this duplicated code into wider system // TODO: Hoist this duplicated code into wider system
@ -479,7 +524,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
ClearOverlays => { ClearOverlays => {
responses.push_back(ToolMessage::SelectedLayersChanged.into()); responses.push_back(ToolMessage::SelectedLayersChanged.into());
for path in self.layer_data.keys().filter(|path| self.document.layer(path).unwrap().overlay).cloned() { for path in self.layer_data.keys().filter(|path| self.graphene_document.layer(path).unwrap().overlay).cloned() {
responses.push_front(DocumentOperation::DeleteLayer { path }.into()); responses.push_front(DocumentOperation::DeleteLayer { path }.into());
} }
} }
@ -490,7 +535,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
} }
SetSelectedLayers(paths) => { SetSelectedLayers(paths) => {
self.clear_selection(); self.layer_data.iter_mut().filter(|(_, layer_data)| layer_data.selected).for_each(|(path, layer_data)| {
layer_data.selected = false;
responses.push_back(LayerChanged(path.clone()).into())
});
responses.push_front(AddSelectedLayers(paths).into()); responses.push_front(AddSelectedLayers(paths).into());
} }
AddSelectedLayers(paths) => { AddSelectedLayers(paths) => {
@ -505,7 +554,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
let all_layer_paths = self let all_layer_paths = self
.layer_data .layer_data
.keys() .keys()
.filter(|path| !path.is_empty() && !self.document.layer(path).map(|layer| layer.overlay).unwrap_or(false)) .filter(|path| !path.is_empty() && !self.graphene_document.layer(path).map(|layer| layer.overlay).unwrap_or(false))
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
responses.push_front(SetSelectedLayers(all_layer_paths).into()); responses.push_front(SetSelectedLayers(all_layer_paths).into());
@ -527,30 +576,41 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_back(RenderDocument.into()); responses.push_back(RenderDocument.into());
responses.push_back(FolderChanged(vec![]).into()); responses.push_back(FolderChanged(vec![]).into());
} }
FolderChanged(path) => responses.extend(self.handle_folder_changed(path)), FolderChanged(path) => {
DispatchOperation(op) => match self.document.handle_operation(&op) { let _ = self.graphene_document.render_root();
responses.extend([LayerChanged(path).into(), DocumentStructureChanged.into()]);
}
DocumentStructureChanged => {
let data_buffer: RawBuffer = self.serialize_root().into();
responses.push_back(FrontendMessage::DisplayFolderTreeStructure { data_buffer }.into())
}
LayerChanged(path) => {
responses.extend(self.layer_panel_entry(path.clone()).ok().and_then(|entry| {
let overlay = self.graphene_document.layer(&path).unwrap().overlay;
(!overlay).then(|| FrontendMessage::UpdateLayer { path: path.into(), data: entry }.into())
}));
}
DispatchOperation(op) => match self.graphene_document.handle_operation(&op) {
Ok(Some(document_responses)) => { Ok(Some(document_responses)) => {
responses.extend( for response in document_responses {
document_responses match response {
.into_iter() DocumentResponse::FolderChanged { path } => responses.push_back(FolderChanged(path).into()),
.map(|response| match response {
DocumentResponse::FolderChanged { path } => Some(FolderChanged(path).into()),
DocumentResponse::DeletedLayer { path } => { DocumentResponse::DeletedLayer { path } => {
self.layer_data.remove(&path); self.layer_data.remove(&path);
Some(ToolMessage::SelectedLayersChanged.into()) responses.push_back(ToolMessage::SelectedLayersChanged.into())
} }
DocumentResponse::LayerChanged { path } => self.layer_panel_entry(path.clone()).ok().and_then(|entry| { DocumentResponse::LayerChanged { path } => responses.push_back(LayerChanged(path).into()),
let overlay = self.document.layer(&path).unwrap().overlay;
(!overlay).then(|| FrontendMessage::UpdateLayer { path: path.into(), data: entry }.into())
}),
DocumentResponse::CreatedLayer { path } => { DocumentResponse::CreatedLayer { path } => {
self.layer_data.insert(path.clone(), LayerData::new(false)); self.layer_data.insert(path.clone(), LayerData::new(false));
(!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into()) responses.push_back(LayerChanged(path.clone()).into());
if !self.graphene_document.layer(&path).unwrap().overlay {
responses.push_back(SetSelectedLayers(vec![path]).into())
}
}
DocumentResponse::DocumentChanged => responses.push_back(RenderDocument.into()),
};
} }
DocumentResponse::DocumentChanged => Some(RenderDocument.into()),
})
.flatten(),
);
// log::debug!("LayerPanel: {:?}", self.layer_data.keys()); // log::debug!("LayerPanel: {:?}", self.layer_data.keys());
} }
Err(e) => log::error!("DocumentError: {:?}", e), Err(e) => log::error!("DocumentError: {:?}", e),
@ -559,7 +619,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
RenderDocument => { RenderDocument => {
responses.push_back( responses.push_back(
FrontendMessage::UpdateCanvas { FrontendMessage::UpdateCanvas {
document: self.document.render_root(), document: self.graphene_document.render_root(),
} }
.into(), .into(),
); );
@ -567,7 +627,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
let scale = 0.5 + ASYMPTOTIC_EFFECT + self.layerdata(&[]).scale * SCALE_EFFECT; let scale = 0.5 + ASYMPTOTIC_EFFECT + self.layerdata(&[]).scale * SCALE_EFFECT;
let viewport_size = ipp.viewport_bounds.size(); let viewport_size = ipp.viewport_bounds.size();
let viewport_mid = ipp.viewport_bounds.center(); let viewport_mid = ipp.viewport_bounds.center();
let [bounds1, bounds2] = self.document.visible_layers_bounding_box().unwrap_or([viewport_mid; 2]); let [bounds1, bounds2] = self.graphene_document.visible_layers_bounding_box().unwrap_or([viewport_mid; 2]);
let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale; let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale;
let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale; let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale;
let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING); let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING);
@ -619,7 +679,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
let insert = all_layer_paths.get(insert_pos); let insert = all_layer_paths.get(insert_pos);
if let Some(insert_path) = insert { if let Some(insert_path) = insert {
let (id, path) = insert_path.split_last().expect("Can't move the root folder"); let (id, path) = insert_path.split_last().expect("Can't move the root folder");
if let Some(folder) = self.document.layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() { if let Some(folder) = self.graphene_document.layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() {
let selected: Vec<_> = selected_layers let selected: Vec<_> = selected_layers
.iter() .iter()
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1) .filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
@ -641,7 +701,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
FlipAxis::X => DVec2::new(-1., 1.), FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.), FlipAxis::Y => DVec2::new(1., -1.),
}; };
if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) { if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
let center = (max + min) / 2.; let center = (max + min) / 2.;
let bbox_trans = DAffine2::from_translation(-center); let bbox_trans = DAffine2::from_translation(-center);
for path in self.selected_layers() { for path in self.selected_layers() {
@ -659,14 +719,17 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
} }
AlignSelectedLayers(axis, aggregate) => { AlignSelectedLayers(axis, aggregate) => {
self.backup(); self.backup();
let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip(); 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 { let axis = match axis {
AlignAxis::X => DVec2::X, AlignAxis::X => DVec2::X,
AlignAxis::Y => DVec2::Y, AlignAxis::Y => DVec2::Y,
}; };
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5); let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5);
if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) { if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
let aggregated = match aggregate { let aggregated = match aggregate {
AlignAggregate::Min => combined_box[0], AlignAggregate::Min => combined_box[0],
AlignAggregate::Max => combined_box[1], AlignAggregate::Max => combined_box[1],

View file

@ -78,14 +78,12 @@ impl DocumentsMessageHandler {
let open_documents = self.documents.iter().map(|doc| doc.name.clone()).collect(); let open_documents = self.documents.iter().map(|doc| doc.name.clone()).collect();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into()); responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
responses.push_back(
FrontendMessage::ExpandFolder {
path: Vec::new().into(),
children: Vec::new(),
}
.into(),
);
responses.push_back(DocumentsMessage::SelectDocument(self.active_document_index).into()); responses.push_back(DocumentsMessage::SelectDocument(self.active_document_index).into());
responses.push_back(DocumentMessage::RenderDocument.into());
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
for layer in self.active_document().layer_data.keys() {
responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into());
}
} }
} }
@ -115,7 +113,10 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
.into(), .into(),
); );
responses.push_back(RenderDocument.into()); responses.push_back(RenderDocument.into());
responses.extend(self.active_document_mut().handle_folder_changed(vec![])); responses.push_back(DocumentMessage::DocumentStructureChanged.into());
for layer in self.active_document().layer_data.keys() {
responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into());
}
} }
CloseActiveDocumentWithConfirmation => { CloseActiveDocumentWithConfirmation => {
responses.push_back( responses.push_back(
@ -156,14 +157,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
self.active_document_index -= 1; self.active_document_index -= 1;
} }
let lp = self.active_document_mut().layer_panel(&[]).expect("Could not get panel for active doc"); responses.push_back(DocumentMessage::DocumentStructureChanged.into());
responses.push_back(
FrontendMessage::ExpandFolder {
path: Vec::new().into(),
children: lp,
}
.into(),
);
responses.push_back( responses.push_back(
FrontendMessage::SetActiveDocument { FrontendMessage::SetActiveDocument {
document_index: self.active_document_index, document_index: self.active_document_index,
@ -172,7 +166,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
); );
responses.push_back( responses.push_back(
FrontendMessage::UpdateCanvas { FrontendMessage::UpdateCanvas {
document: self.active_document_mut().document.render_root(), document: self.active_document_mut().graphene_document.render_root(),
} }
.into(), .into(),
); );
@ -228,7 +222,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
let paths = self.active_document().selected_layers_sorted(); let paths = self.active_document().selected_layers_sorted();
self.copy_buffer.clear(); self.copy_buffer.clear();
for path in paths { for path in paths {
match self.active_document().document.layer(&path).map(|t| t.clone()) { match self.active_document().graphene_document.layer(&path).map(|t| t.clone()) {
Ok(layer) => { Ok(layer) => {
self.copy_buffer.push(layer); self.copy_buffer.push(layer);
} }
@ -239,7 +233,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
Paste => { Paste => {
let document = self.active_document(); let document = self.active_document();
let shallowest_common_folder = document let shallowest_common_folder = document
.document .graphene_document
.deepest_common_folder(document.selected_layers()) .deepest_common_folder(document.selected_layers())
.expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion"); .expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion");

View file

@ -5,7 +5,10 @@ use graphene::{
layers::{Layer, LayerData as DocumentLayerData}, layers::{Layer, LayerData as DocumentLayerData},
LayerId, LayerId,
}; };
use serde::{ser::SerializeSeq, Deserialize, Serialize}; use serde::{
ser::{SerializeSeq, SerializeStruct},
Deserialize, Serialize,
};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
@ -117,6 +120,33 @@ impl Serialize for Path {
} }
} }
#[derive(Debug, Clone, Deserialize, PartialEq)]
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>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut buffer = serializer.serialize_struct("Buffer", 2)?;
buffer.serialize_field("ptr", &(self.0.as_ptr() as usize))?;
buffer.serialize_field("len", &(self.0.len()))?;
buffer.end()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LayerPanelEntry { pub struct LayerPanelEntry {
pub name: String, pub name: String,

View file

@ -1,4 +1,4 @@
use crate::document::layer_panel::{LayerPanelEntry, Path}; use crate::document::layer_panel::{LayerPanelEntry, Path, RawBuffer};
use crate::message_prelude::*; use crate::message_prelude::*;
use crate::tool::tool_options::ToolOptions; use crate::tool::tool_options::ToolOptions;
use crate::Color; use crate::Color;
@ -7,8 +7,7 @@ use serde::{Deserialize, Serialize};
#[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 {
CollapseFolder { path: Path }, DisplayFolderTreeStructure { data_buffer: RawBuffer },
ExpandFolder { path: Path, children: Vec<LayerPanelEntry> },
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> }, SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
SetActiveDocument { document_index: usize }, SetActiveDocument { document_index: usize },
UpdateOpenDocumentsList { open_documents: Vec<String> }, UpdateOpenDocumentsList { open_documents: Vec<String> },
@ -16,9 +15,9 @@ pub enum FrontendMessage {
DisplayPanic { panic_info: String, title: String, description: String }, DisplayPanic { panic_info: String, title: String, description: String },
DisplayConfirmationToCloseDocument { document_index: usize }, DisplayConfirmationToCloseDocument { document_index: usize },
DisplayConfirmationToCloseAllDocuments, DisplayConfirmationToCloseAllDocuments,
UpdateLayer { path: Path, data: LayerPanelEntry },
UpdateCanvas { document: String }, UpdateCanvas { document: String },
UpdateScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) }, UpdateScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
UpdateLayer { path: Path, data: LayerPanelEntry },
ExportDocument { document: String, name: String }, ExportDocument { document: String, name: String },
SaveDocument { document: String, name: String }, SaveDocument { document: String, name: String },
OpenDocumentBrowse, OpenDocumentBrowse,

View file

@ -22,8 +22,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
let tolerance = DVec2::splat(SELECTION_TOLERANCE); let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
if let Some(path) = data.0.document.intersects_quad_root(quad).last() { if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() {
if let Ok(layer) = data.0.document.layer(path) { if let Ok(layer) = data.0.graphene_document.layer(path) {
if let LayerDataType::Shape(s) = &layer.data { if let LayerDataType::Shape(s) = &layer.data {
s.style.fill().and_then(|fill| { s.style.fill().and_then(|fill| {
fill.color().map(|color| match action { fill.color().map(|color| match action {

View file

@ -20,7 +20,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
let tolerance = DVec2::splat(SELECTION_TOLERANCE); let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
if let Some(path) = data.0.document.intersects_quad_root(quad).last() { if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() {
responses.push_back( responses.push_back(
Operation::SetLayerFill { Operation::SetLayerFill {
path: path.to_vec(), path: path.to_vec(),

View file

@ -66,7 +66,7 @@ impl Fsm for PenToolFsmState {
input: &InputPreprocessor, input: &InputPreprocessor,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
let transform = document.document.root.transform; let transform = document.graphene_document.root.transform;
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position); let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position);
use PenMessage::*; use PenMessage::*;

View file

@ -151,7 +151,7 @@ impl Fsm for SelectToolFsmState {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
let mut selected: Vec<_> = document.selected_layers().map(|path| path.to_vec()).collect(); let mut selected: Vec<_> = document.selected_layers().map(|path| path.to_vec()).collect();
let quad = data.selection_quad(); let quad = data.selection_quad();
let intersection = document.document.intersects_quad_root(quad); let intersection = document.graphene_document.intersects_quad_root(quad);
// If no layer is currently selected and the user clicks on a shape, select that. // If no layer is currently selected and the user clicks on a shape, select that.
if selected.is_empty() { if selected.is_empty() {
if let Some(layer) = intersection.last() { if let Some(layer) = intersection.last() {
@ -214,7 +214,7 @@ impl Fsm for SelectToolFsmState {
} }
(DrawingBox, DragStop) => { (DrawingBox, DragStop) => {
let quad = data.selection_quad(); let quad = data.selection_quad();
responses.push_front(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into()); responses.push_front(DocumentMessage::AddSelectedLayers(document.graphene_document.intersects_quad_root(quad)).into());
responses.push_front( responses.push_front(
Operation::DeleteLayer { Operation::DeleteLayer {
path: data.drag_box_id.take().unwrap(), path: data.drag_box_id.take().unwrap(),

View file

@ -62,6 +62,7 @@ module.exports = {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-param-reassign": ["error", { props: false }], "no-param-reassign": ["error", { props: false }],
"no-bitwise": "off",
// TypeScript plugin config // TypeScript plugin config
"@typescript-eslint/camelcase": "off", "@typescript-eslint/camelcase": "off",

15422
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -110,18 +110,18 @@
position: absolute; position: absolute;
width: 0; width: 0;
height: 0; height: 0;
top: 2px; top: 3px;
left: 3px; left: 4px;
border-style: solid; border-style: solid;
border-width: 0 3px 6px 3px; border-width: 3px 0 3px 6px;
border-color: transparent transparent var(--color-2-mildblack) transparent; border-color: transparent transparent transparent var(--color-2-mildblack);
} }
&.expanded::after { &.expanded::after {
top: 3px; top: 4px;
left: 4px; left: 3px;
border-width: 3px 0 3px 6px; border-width: 6px 3px 0 3px;
border-color: transparent transparent transparent var(--color-2-mildblack); border-color: var(--color-2-mildblack) transparent transparent transparent;
} }
} }
@ -181,7 +181,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { ResponseType, registerResponseHandler, Response, BlendMode, ExpandFolder, CollapseFolder, UpdateLayer, LayerPanelEntry, LayerType } from "@/utilities/response-handler"; import { ResponseType, registerResponseHandler, Response, BlendMode, DisplayFolderTreeStructure, UpdateLayer, LayerPanelEntry, LayerType } from "@/utilities/response-handler";
import { panicProxy } from "@/utilities/panic-proxy"; import { panicProxy } from "@/utilities/panic-proxy";
import { SeparatorType } from "@/components/widgets/widgets"; import { SeparatorType } from "@/components/widgets/widgets";
@ -238,7 +238,24 @@ const blendModeEntries: SectionsOfMenuListEntries = [
]; ];
export default defineComponent({ export default defineComponent({
props: {}, data() {
return {
blendModeEntries,
blendModeSelectedIndex: 0,
blendModeDropdownDisabled: true,
opacityNumberInputDisabled: true,
// TODO: replace with BigUint64Array as index
layerCache: new Map() as Map<string, LayerPanelEntry>,
layers: [] as Array<LayerPanelEntry>,
layerDepths: [] as Array<number>,
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
opacity: 100,
MenuDirection,
SeparatorType,
LayerType,
};
},
methods: { methods: {
layerIndent(layer: LayerPanelEntry): string { layerIndent(layer: LayerPanelEntry): string {
return `${(layer.path.length - 1) * 16}px`; return `${(layer.path.length - 1) * 16}px`;
@ -325,7 +342,6 @@ export default defineComponent({
output.set(path, i); output.set(path, i);
i += path.length; i += path.length;
if (index < paths.length) { if (index < paths.length) {
// eslint-disable-next-line no-bitwise
output[i] = (1n << 64n) - 1n; output[i] = (1n << 64n) - 1n;
} }
i += 1; i += 1;
@ -374,99 +390,40 @@ export default defineComponent({
}, },
}, },
mounted() { mounted() {
registerResponseHandler(ResponseType.ExpandFolder, (responseData: Response) => { registerResponseHandler(ResponseType.DisplayFolderTreeStructure, (responseData: Response) => {
const expandData = responseData as ExpandFolder; const expandData = responseData as DisplayFolderTreeStructure;
if (expandData) { if (!expandData) return;
const responsePath = expandData.path; console.log(expandData);
const responseLayers = expandData.children as Array<LayerPanelEntry>;
// TODO: @Keavon Refactor this function
if (responseLayers.length === 0) return;
const mergeIntoExisting = (elements: Array<LayerPanelEntry>, layers: Array<LayerPanelEntry>) => { const path = [] as Array<bigint>;
let lastInsertion = layers.findIndex((layer: LayerPanelEntry) => { this.layers = [] as Array<LayerPanelEntry>;
const pathLengthsEqual = elements[0].path.length - 1 === layer.path.length; function recurse(folder: DisplayFolderTreeStructure, layers: Array<LayerPanelEntry>, cache: Map<string, LayerPanelEntry>) {
return pathLengthsEqual && elements[0].path.slice(0, -1).every((layerId, i) => layerId === layer.path[i]); folder.children.forEach((item) => {
// TODO: fix toString
path.push(BigInt(item.layerId.toString()));
const mapping = cache.get(path.toString());
if (mapping) layers.push(mapping);
if (item.children.length > 1) recurse(item, layers, cache);
path.pop();
}); });
elements.forEach((nlayer) => {
const index = layers.findIndex((layer: LayerPanelEntry) => {
const pathLengthsEqual = nlayer.path.length === layer.path.length;
return pathLengthsEqual && nlayer.path.every((layerId, i) => layerId === layer.path[i]);
});
if (index >= 0) {
lastInsertion = index;
layers[index] = nlayer;
} else {
lastInsertion += 1;
layers.splice(lastInsertion, 0, nlayer);
} }
recurse(expandData, this.layers, this.layerCache);
}); });
};
mergeIntoExisting(responseLayers, this.layers);
const newLayers: Array<LayerPanelEntry> = [];
this.layers.forEach((layer) => {
const index = responseLayers.findIndex((nlayer: LayerPanelEntry) => {
const pathLengthsEqual = responsePath.length + 1 === layer.path.length;
return pathLengthsEqual && nlayer.path.every((layerId, i) => layerId === layer.path[i]);
});
if (index >= 0 || layer.path.length !== responsePath.length + 1) {
newLayers.push(layer);
}
});
this.layers = newLayers;
this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers();
}
});
registerResponseHandler(ResponseType.CollapseFolder, (responseData) => {
const collapseData = responseData as CollapseFolder;
if (collapseData) {
const responsePath = collapseData.path;
const newLayers: Array<LayerPanelEntry> = [];
this.layers.forEach((layer) => {
if (responsePath.length >= layer.path.length || !responsePath.every((layerId, i) => layerId === layer.path[i])) {
newLayers.push(layer);
}
});
this.layers = newLayers;
this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers();
}
});
registerResponseHandler(ResponseType.UpdateLayer, (responseData) => { registerResponseHandler(ResponseType.UpdateLayer, (responseData) => {
const updateData = responseData as UpdateLayer; const updateData = responseData as UpdateLayer;
if (updateData) { if (updateData) {
const responsePath = updateData.path; const responsePath = updateData.path;
const responseLayer = updateData.data; const responseLayer = updateData.data;
const index = this.layers.findIndex((layer: LayerPanelEntry) => { const layer = this.layerCache.get(responsePath.toString());
const pathLengthsEqual = responsePath.length === layer.path.length; if (layer) Object.assign(this.layerCache.get(responsePath.toString()), responseLayer);
return pathLengthsEqual && responsePath.every((layerId, i) => layerId === layer.path[i]); else this.layerCache.set(responsePath.toString(), responseLayer);
});
if (index >= 0) this.layers[index] = responseLayer;
this.setBlendModeForSelectedLayers(); this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers(); this.setOpacityForSelectedLayers();
} }
}); });
}, },
data() {
return {
blendModeEntries,
blendModeSelectedIndex: 0,
blendModeDropdownDisabled: true,
opacityNumberInputDisabled: true,
layers: [] as Array<LayerPanelEntry>,
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
opacity: 100,
MenuDirection,
SeparatorType,
LayerType,
};
},
components: { components: {
LayoutRow, LayoutRow,
LayoutCol, LayoutCol,

View file

@ -293,7 +293,6 @@ export default defineComponent({
this.setClosed(); this.setClosed();
} }
// eslint-disable-next-line no-bitwise
const eventIncludesLmb = Boolean(e.buttons & 1); const eventIncludesLmb = Boolean(e.buttons & 1);
// Clean up any messes from lost mouseup events // Clean up any messes from lost mouseup events

View file

@ -4,22 +4,32 @@ import { fullscreenModeChanged } from "@/utilities/fullscreen";
import { onKeyUp, onKeyDown, onMouseMove, onMouseDown, onMouseUp, onMouseScroll, onWindowResize } from "@/utilities/input"; import { onKeyUp, onKeyDown, onMouseMove, onMouseDown, onMouseUp, onMouseScroll, onWindowResize } from "@/utilities/input";
import "@/utilities/errors"; import "@/utilities/errors";
import App from "@/App.vue"; import App from "@/App.vue";
import { panicProxy } from "@/utilities/panic-proxy";
// Bind global browser events const wasm = import("@/../wasm/pkg").then(panicProxy);
window.addEventListener("resize", onWindowResize); // eslint-disable-next-line @typescript-eslint/no-explicit-any
window.addEventListener("DOMContentLoaded", onWindowResize); (window as any).wasmMemory = undefined;
document.addEventListener("contextmenu", (e) => e.preventDefault()); (async () => {
document.addEventListener("fullscreenchange", () => fullscreenModeChanged()); // eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).wasmMemory = (await wasm).wasm_memory;
window.addEventListener("keyup", onKeyUp); // Initialize the Vue application
window.addEventListener("keydown", onKeyDown); createApp(App).mount("#app");
window.addEventListener("mousemove", onMouseMove); // Bind global browser events
window.addEventListener("mousedown", onMouseDown); window.addEventListener("resize", onWindowResize);
window.addEventListener("mouseup", onMouseUp); onWindowResize();
window.addEventListener("wheel", onMouseScroll, { passive: false }); document.addEventListener("contextmenu", (e) => e.preventDefault());
document.addEventListener("fullscreenchange", () => fullscreenModeChanged());
// Initialize the Vue application window.addEventListener("keyup", onKeyUp);
createApp(App).mount("#app"); window.addEventListener("keydown", onKeyDown);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mousedown", onMouseDown);
window.addEventListener("mouseup", onMouseUp);
window.addEventListener("wheel", onMouseScroll, { passive: false });
})();

View file

@ -126,6 +126,5 @@ export async function onWindowResize() {
} }
export function makeModifiersBitfield(e: MouseEvent | KeyboardEvent): number { export function makeModifiersBitfield(e: MouseEvent | KeyboardEvent): number {
// eslint-disable-next-line no-bitwise
return Number(e.ctrlKey) | (Number(e.shiftKey) << 1) | (Number(e.altKey) << 2); return Number(e.ctrlKey) | (Number(e.shiftKey) << 1) | (Number(e.altKey) << 2);
} }

View file

@ -18,8 +18,7 @@ export enum ResponseType {
ExportDocument = "ExportDocument", ExportDocument = "ExportDocument",
SaveDocument = "SaveDocument", SaveDocument = "SaveDocument",
OpenDocumentBrowse = "OpenDocumentBrowse", OpenDocumentBrowse = "OpenDocumentBrowse",
ExpandFolder = "ExpandFolder", DisplayFolderTreeStructure = "DisplayFolderTreeStructure",
CollapseFolder = "CollapseFolder",
UpdateLayer = "UpdateLayer", UpdateLayer = "UpdateLayer",
SetActiveTool = "SetActiveTool", SetActiveTool = "SetActiveTool",
SetActiveDocument = "SetActiveDocument", SetActiveDocument = "SetActiveDocument",
@ -56,10 +55,8 @@ function parseResponse(responseType: string, data: any): Response {
switch (responseType) { switch (responseType) {
case "DocumentChanged": case "DocumentChanged":
return newDocumentChanged(data.DocumentChanged); return newDocumentChanged(data.DocumentChanged);
case "CollapseFolder": case "DisplayFolderTreeStructure":
return newCollapseFolder(data.CollapseFolder); return newDisplayFolderTreeStructure(data.DisplayFolderTreeStructure);
case "ExpandFolder":
return newExpandFolder(data.ExpandFolder);
case "SetActiveTool": case "SetActiveTool":
return newSetActiveTool(data.SetActiveTool); return newSetActiveTool(data.SetActiveTool);
case "SetActiveDocument": case "SetActiveDocument":
@ -97,7 +94,7 @@ function parseResponse(responseType: string, data: any): Response {
} }
} }
export type Response = SetActiveTool | UpdateCanvas | UpdateScrollbars | DocumentChanged | CollapseFolder | ExpandFolder | UpdateWorkingColors | SetCanvasZoom | SetCanvasRotation; export type Response = SetActiveTool | UpdateCanvas | UpdateScrollbars | UpdateLayer | DocumentChanged | DisplayFolderTreeStructure | UpdateWorkingColors | SetCanvasZoom | SetCanvasRotation;
export interface UpdateOpenDocumentsList { export interface UpdateOpenDocumentsList {
open_documents: Array<string>; open_documents: Array<string>;
@ -239,13 +236,61 @@ function newDocumentChanged(_: any): DocumentChanged {
return {}; return {};
} }
export interface CollapseFolder { export interface DisplayFolderTreeStructure {
path: BigUint64Array; layerId: BigInt;
children: DisplayFolderTreeStructure[];
} }
function newCollapseFolder(input: any): CollapseFolder { function newDisplayFolderTreeStructure(input: any): DisplayFolderTreeStructure {
return { const { ptr, len } = input.data_buffer;
path: newPath(input.path), const wasmMemoryBuffer = (window as any).wasmMemory().buffer;
};
// Decode the folder structure encoding
const encoding = new DataView(wasmMemoryBuffer, ptr, len);
// The structure section indicates how to read through the upcoming layer list and assign depths to each layer
const structureSectionLength = Number(encoding.getBigUint64(0, true));
const structureSectionMsbSigned = new DataView(wasmMemoryBuffer, ptr + 8, structureSectionLength * 8);
// The layer IDs section lists each layer ID sequentially in the tree, as it will show up in the panel
const layerIdsSection = new DataView(wasmMemoryBuffer, ptr + 8 + structureSectionLength * 8);
let layersEncountered = 0;
let currentFolder: DisplayFolderTreeStructure = { layerId: BigInt(-1), children: [] };
const currentFolderStack = [currentFolder];
for (let i = 0; i < structureSectionLength; i += 1) {
const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true);
const msbMask = BigInt(1) << BigInt(63);
// Set the MSB to 0 to clear the sign and then read the number as usual
const numberOfLayersAtThisDepth = msbSigned & ~msbMask;
// Store child folders in the current folder (until we are interrupted by an indent)
for (let j = 0; j < numberOfLayersAtThisDepth; j += 1) {
const layerId = layerIdsSection.getBigUint64(layersEncountered * 8, true);
layersEncountered += 1;
const childLayer = { layerId, children: [] };
currentFolder.children.push(childLayer);
}
// Check the sign of the MSB, where a 1 is a negative (outward) indent
const subsequentDirectionOfDepthChange = (msbSigned & msbMask) === BigInt(0);
// debugger;
// Inward
if (subsequentDirectionOfDepthChange) {
currentFolderStack.push(currentFolder);
currentFolder = currentFolder.children[currentFolder.children.length - 1];
}
// Outward
else {
const popped = currentFolderStack.pop();
if (!popped) throw Error("Too many negative indents in the folder structure");
if (popped) currentFolder = popped;
}
}
return currentFolder;
} }
export interface UpdateLayer { export interface UpdateLayer {
@ -259,17 +304,6 @@ function newUpdateLayer(input: any): UpdateLayer {
}; };
} }
export interface ExpandFolder {
path: BigUint64Array;
children: Array<LayerPanelEntry>;
}
function newExpandFolder(input: any): ExpandFolder {
return {
path: newPath(input.path),
children: input.children.map((child: any) => newLayerPanelEntry(child)),
};
}
export interface SetCanvasZoom { export interface SetCanvasZoom {
new_zoom: number; new_zoom: number;
} }

View file

@ -186,7 +186,6 @@ function htmlDecode(input) {
} }
// eslint-disable-next-line no-cond-assign // eslint-disable-next-line no-cond-assign
if ((match = entityCode.match(/^#(\d+)$/))) { if ((match = entityCode.match(/^#(\d+)$/))) {
// eslint-disable-next-line no-bitwise
return String.fromCharCode(~~match[1]); return String.fromCharCode(~~match[1]);
} }
return entity; return entity;

View file

@ -20,6 +20,11 @@ pub fn intentional_panic() {
panic!(); panic!();
} }
#[wasm_bindgen]
pub fn wasm_memory() -> JsValue {
wasm_bindgen::memory()
}
/// Modify the currently selected tool in the document state store /// Modify the currently selected tool in the document state store
#[wasm_bindgen] #[wasm_bindgen]
pub fn select_tool(tool: String) -> Result<(), JsValue> { pub fn select_tool(tool: String) -> Result<(), JsValue> {

View file

@ -117,49 +117,6 @@ impl Document {
.unwrap_or_default() .unwrap_or_default()
} }
fn serialize_structure(folder: &Folder, structure: &mut Vec<u64>, data: &mut Vec<LayerId>) {
let mut space = 0;
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
data.push(*id);
match layer.data {
LayerDataType::Shape(_) => space += 1,
LayerDataType::Folder(ref folder) => {
structure.push(space);
Document::serialize_structure(folder, structure, data);
}
}
}
structure.push(space | 1 << 63);
}
/// Serializes the layer structure into a compressed 1d structure
/// 4,2,1,-2-0,10,12,13,14,15 <- input data
/// l = 4 = structure.len() <- length of the structure section
/// structure = 2,1,-2,-0 <- structure section
/// data = 10,12,13,14,15 <- data section
///
/// the numbers in the structure block encode the indentation,
/// 2 mean read two element from the data section, then place a [
/// -x means read x elements from the date section an then insert a ]
///
/// 2 V 1 V -2 A -0 A
/// 10,12, 13, 14,15
/// 10,12,[ 13,[14,15] ]
///
/// resulting layer panel:
/// 10
/// 12
/// [12,13]
/// [12,13,14]
/// [12,13,15]
pub fn serialize_root(&self) -> Vec<LayerId> {
let (mut structure, mut data) = (vec![0], Vec::new());
Document::serialize_structure(self.root.as_folder().unwrap(), &mut structure, &mut data);
structure[0] = structure.len() as u64 - 1;
structure.extend(data);
structure
}
/// Given a path to a layer, returns a vector of the indices in the layer tree /// Given a path to a layer, returns a vector of the indices in the layer tree
/// These indices can be used to order a list of layers /// These indices can be used to order a list of layers
pub fn indices_for_path(&self, path: &[LayerId]) -> Result<Vec<usize>, DocumentError> { pub fn indices_for_path(&self, path: &[LayerId]) -> Result<Vec<usize>, DocumentError> {
@ -446,10 +403,24 @@ impl Document {
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat())
} }
Operation::DeleteLayer { path } => { Operation::DeleteLayer { path } => {
fn aggregate_deletions(folder: &Folder, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>) {
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
path.push(*id);
responses.push(DocumentResponse::DeletedLayer { path: path.clone() });
if let LayerDataType::Folder(f) = &layer.data {
aggregate_deletions(f, path, responses);
}
path.pop();
}
}
let mut responses = Vec::new();
if let Ok(folder) = self.folder(path) {
aggregate_deletions(folder, &mut path.clone(), &mut responses)
};
self.delete(path)?; self.delete(path)?;
let (folder, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0)); let (folder, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
let mut responses = vec![DocumentChanged, DeletedLayer { path: path.clone() }, FolderChanged { path: folder.to_vec() }]; responses.extend([DocumentChanged, DeletedLayer { path: path.clone() }, FolderChanged { path: folder.to_vec() }]);
responses.extend(update_thumbnails_upstream(folder)); responses.extend(update_thumbnails_upstream(folder));
Some(responses) Some(responses)
} }
@ -458,8 +429,22 @@ impl Document {
let id = folder.add_layer(layer.clone(), None, *insert_index).ok_or(DocumentError::IndexOutOfBounds)?; let id = folder.add_layer(layer.clone(), None, *insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
let full_path = [path.clone(), vec![id]].concat(); let full_path = [path.clone(), vec![id]].concat();
self.mark_as_dirty(&full_path)?; self.mark_as_dirty(&full_path)?;
fn aggregate_insertions(folder: &Folder, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>) {
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
path.push(*id);
responses.push(DocumentResponse::CreatedLayer { path: path.clone() });
if let LayerDataType::Folder(f) = &layer.data {
aggregate_insertions(f, path, responses);
}
path.pop();
}
}
let mut responses = Vec::new();
if let Ok(folder) = self.folder(&full_path) {
aggregate_insertions(folder, &mut full_path.clone(), &mut responses)
};
let mut responses = vec![DocumentChanged, CreatedLayer { path: full_path }, FolderChanged { path: path.clone() }]; responses.extend([DocumentChanged, CreatedLayer { path: full_path }, FolderChanged { path: path.clone() }]);
responses.extend(update_thumbnails_upstream(path)); responses.extend(update_thumbnails_upstream(path));
Some(responses) Some(responses)
} }