Skip to content

Commit 4629b7c

Browse files
KeavonTrueDoctor
andcommitted
Add folders to frontend and folder creation to backend (#315)
* Add folders to frontend and folder creation to backend Closes #149 * Add Group keybind * Add logic to handle expanding of folders * Send all paths as (u32, u32) * Add custom serialization for path * Merge two layer_panel files * Refactor frontend layer merging * Fix JS linting * Update upstream thumbnail changes * Add paste into selected folder + fix thumbnail dirtification * Implement CollapseFolder function * Skip folders on a different indentation level during reorder * Only reorder within the same folder * Add folder node icon for folder layers * Add expand/collapse folder button; partly implement new layer tree design * Update terminology in the docs * Add number labels to ruler marks * Replace promise with await in MenuList.vue * Miscellaneous minor code cleanup * Disallow snake_case variable names in frontend * Add support for saving and opening files (#325) * Add support for saving a document This is similar to the "export" functionality, except that we store all metadata needed to open the file again. Currently we store the internal representation of the layer which is probably pretty fragile. * Add support for opening a saved document User can select a file using the browser's file input selector. We parse it as JSON and load it into the internal representation. Concerns: - The file format is fragile - Loading data directly into internal data structures usually creates security vulnerabilities - Error handling: The user is not informed of errors * Serialize Document and skip "cache" fields in Layer Instead of serializing the root layer, we serialize the Document struct directly. Additionally, we mark the "cache" fields in layer as "skip" fields so they don't get serialized. * Opened files use the filename as the tab title * Split "new document" and "open document" handling Open document needs name and content to be provided so having a different interface is cleaner. Also did some refactoring to reuse code. * Show error to user when a file fails to open * Clean up code: better variable naming and structure * Use document name for saved and exported files We pass through the document name in the export and save messages. Additionally, we check if the appropriate file suffixes (.graphite and .svg) need to be added before passing it to the frontend. * Refactor document name generation * Don't assign a default of 1 to Documents that start with something other than DEFAULT_DOCUMENT_NAME * Improve runtime complexity by using binary instead of linear search * Update Layer panel upon document selection * Add File>Open/Ctrl+O; File>Save (As)/Ctrl+(Shift)+S; browse filters extension; split out download()/upload() into files.ts; change unsaved close dialog text Co-authored-by: Dennis Kobert <[email protected]> Co-authored-by: Keavon Chambers <[email protected]> * Refactor ViewportPosition from u32 (UVec2) to f64 (DVec2) (#345) * Refactor ViewportPosition from u32 (UVec2) to f64 (DVec2) * Fix pseudo_hash call * Replace hash function with proper function for uuid generation * Cargo fmt Co-authored-by: Dennis Kobert <[email protected]> * Improve Frontend -> Backend user input system (#348) Includes refactor that sends coordinates of the document viewports to the backend so input is sent relative to the application window Closes #124 Fixes #291 * Improve Frontend -> Backend user input system * Code review changes * More code review changes * Fix TS error * Update the readme * Make scrollbars interactable (#328) * Make scrollbars interactable * Add watcher for position change * Fix case of data * Fix updateHandlePosition capitalization * Clean up class name thing * Scroll bars between 0 and 1 * Allow width to be 100% * Scrollbars reflect backend * Include viewport in scrollbar * Add half viewport padding for scrollbars * Refactor scrollbar using lerp * Send messages to backend * Refactor * Use glam::DVec2 * Remove glam:: * Remove unnecessary abs * Add TrueDoctor's change * Add missing minus * Fix vue issues * Fix viewport size * Remove unnecessary log * Linear dragging * Improve scrollbar behavior (#351) * Change scrollbar behavior * Leave space at the end of the scrollbar * Change mid to center * Use shorter array initialization * Add space around scrollbar * Fix scrollbar spacing * Smooth end of scrollbars * Add page up and down * Page up and down on click in scrollbar track * Add shift pageup to translate horizontally * Implement bounding box for selected layers (#349) * Implement bounding box for selected layers * Add shift modifier for multi selection * Fix collapsing of folders * Add have pixel offset to selection bounding box * Don't panic on Ctrl + A * Rename to camel case * Add todo comment for Keavon * Apply @hypercubes review suggestions * Fix many panics, improve behavior of copy/paste and grouping (but grouping still can panic) Co-authored-by: Dennis Kobert <[email protected]>
1 parent 024ce9d commit 4629b7c

23 files changed

+460
-201
lines changed

editor/src/communication/dispatcher.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ mod test {
126126
let mut editor = create_editor_with_three_layers();
127127

128128
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
129-
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
130-
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
129+
editor.handle_message(DocumentsMessage::Copy).unwrap();
130+
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
131131
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
132132

133133
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
@@ -159,8 +159,8 @@ mod test {
159159
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
160160

161161
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![shape_id]])).unwrap();
162-
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
163-
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
162+
editor.handle_message(DocumentsMessage::Copy).unwrap();
163+
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
164164

165165
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
166166

@@ -192,7 +192,7 @@ mod test {
192192
const LINE_INDEX: usize = 0;
193193
const PEN_INDEX: usize = 1;
194194

195-
editor.handle_message(DocumentMessage::AddFolder(vec![])).unwrap();
195+
editor.handle_message(DocumentMessage::CreateFolder(vec![])).unwrap();
196196

197197
let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().document.clone();
198198
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];
@@ -222,10 +222,10 @@ mod test {
222222

223223
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
224224

225-
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
225+
editor.handle_message(DocumentsMessage::Copy).unwrap();
226226
editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap();
227-
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
228-
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
227+
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
228+
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
229229

230230
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
231231

@@ -283,11 +283,11 @@ mod test {
283283
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
284284

285285
editor.handle_message(DocumentMessage::SetSelectedLayers(vec![vec![rect_id], vec![ellipse_id]])).unwrap();
286-
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
286+
editor.handle_message(DocumentsMessage::Copy).unwrap();
287287
editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap();
288288
editor.draw_rect(0., 800., 12., 200.);
289-
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
290-
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
289+
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
290+
editor.handle_message(DocumentsMessage::PasteIntoFolder { path: vec![], insert_index: -1 }).unwrap();
291291

292292
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
293293

editor/src/document/document_file.rs

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
pub use super::layer_panel::*;
22
use crate::{
33
consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING},
4-
frontend::layer_panel::*,
54
EditorError,
65
};
76
use glam::{DAffine2, DVec2};
@@ -93,16 +92,17 @@ pub enum DocumentMessage {
9392
DeleteLayer(Vec<LayerId>),
9493
DeleteSelectedLayers,
9594
DuplicateSelectedLayers,
95+
CreateFolder(Vec<LayerId>),
9696
SetBlendModeForSelectedLayers(BlendMode),
9797
SetOpacityForSelectedLayers(f64),
98-
AddFolder(Vec<LayerId>),
9998
RenameLayer(Vec<LayerId>, String),
10099
ToggleLayerVisibility(Vec<LayerId>),
101100
FlipSelectedLayers(FlipAxis),
102101
ToggleLayerExpansion(Vec<LayerId>),
103102
FolderChanged(Vec<LayerId>),
104103
StartTransaction,
105104
RollbackTransaction,
105+
GroupSelectedLayers,
106106
AbortTransaction,
107107
CommitTransaction,
108108
ExportDocument,
@@ -139,7 +139,7 @@ impl DocumentMessageHandler {
139139
let _ = self.document.render_root();
140140
self.layer_data(&path).expanded.then(|| {
141141
let children = self.layer_panel(path.as_slice()).expect("The provided Path was not valid");
142-
FrontendMessage::ExpandFolder { path, children }.into()
142+
FrontendMessage::ExpandFolder { path: path.into(), children }.into()
143143
})
144144
}
145145

@@ -154,7 +154,7 @@ impl DocumentMessageHandler {
154154
self.layer_data(path).selected = true;
155155
let data = self.layer_panel_entry(path.to_vec()).ok()?;
156156
// TODO: Add deduplication
157-
(!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec(), data }.into())
157+
(!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec().into(), data }.into())
158158
}
159159

160160
pub fn selected_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
@@ -165,9 +165,9 @@ impl DocumentMessageHandler {
165165
// TODO: Consider moving this to some kind of overlay manager in the future
166166
pub fn selected_layers_vector_points(&self) -> Vec<VectorManipulatorShape> {
167167
let shapes = self.selected_layers().filter_map(|path_to_shape| {
168-
let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape.as_slice()).ok()?;
168+
let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape).ok()?;
169169

170-
let shape = match &self.document.layer(path_to_shape.as_slice()).ok()?.data {
170+
let shape = match &self.document.layer(path_to_shape).ok()?.data {
171171
LayerDataType::Shape(shape) => Some(shape),
172172
LayerDataType::Folder(_) => None,
173173
}?;
@@ -205,8 +205,8 @@ impl DocumentMessageHandler {
205205
self.layer_data.entry(path.to_vec()).or_insert_with(|| LayerData::new(true))
206206
}
207207

208-
pub fn selected_layers(&self) -> impl Iterator<Item = &Vec<LayerId>> {
209-
self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path))
208+
pub fn selected_layers(&self) -> impl Iterator<Item = &[LayerId]> {
209+
self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice()))
210210
}
211211

212212
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
@@ -326,7 +326,7 @@ impl DocumentMessageHandler {
326326
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
327327
let data: LayerData = *layer_data(&mut self.layer_data, &path);
328328
let layer = self.document.layer(&path)?;
329-
let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path).unwrap(), layer, path);
329+
let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path)?, layer, path);
330330
Ok(entry)
331331
}
332332

@@ -362,7 +362,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
362362
match message {
363363
Movement(message) => self.movement_handler.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.document, ipp), responses),
364364
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
365-
AddFolder(path) => responses.push_back(DocumentOperation::AddFolder { path }.into()),
366365
StartTransaction => self.backup(),
367366
RollbackTransaction => {
368367
self.rollback().unwrap_or_else(|e| log::warn!("{}", e));
@@ -409,6 +408,32 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
409408
.into(),
410409
)
411410
}
411+
CreateFolder(mut path) => {
412+
let id = generate_uuid();
413+
path.push(id);
414+
self.layerdata_mut(&path).expanded = true;
415+
responses.push_back(DocumentOperation::CreateFolder { path }.into())
416+
}
417+
GroupSelectedLayers => {
418+
let common_prefix = self.document.common_prefix(self.selected_layers());
419+
let (_id, common_prefix) = common_prefix.split_last().unwrap_or((&0, &[]));
420+
421+
let mut new_folder_path = common_prefix.to_vec();
422+
new_folder_path.push(generate_uuid());
423+
424+
responses.push_back(DocumentsMessage::Copy.into());
425+
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
426+
responses.push_back(DocumentOperation::CreateFolder { path: new_folder_path.clone() }.into());
427+
responses.push_back(DocumentMessage::ToggleLayerExpansion(new_folder_path.clone()).into());
428+
responses.push_back(
429+
DocumentsMessage::PasteIntoFolder {
430+
path: new_folder_path.clone(),
431+
insert_index: -1,
432+
}
433+
.into(),
434+
);
435+
responses.push_back(DocumentMessage::SetSelectedLayers(vec![new_folder_path]).into());
436+
}
412437
SetBlendModeForSelectedLayers(blend_mode) => {
413438
self.backup();
414439
for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
@@ -419,7 +444,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
419444
self.backup();
420445
let opacity = opacity.clamp(0., 1.);
421446

422-
for path in self.selected_layers().cloned() {
447+
for path in self.selected_layers().map(|path| path.to_vec()) {
423448
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
424449
}
425450
}
@@ -428,7 +453,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
428453
}
429454
ToggleLayerExpansion(path) => {
430455
self.layer_data(&path).expanded ^= true;
431-
responses.push_back(FolderChanged(path).into());
456+
match self.layer_data(&path).expanded {
457+
true => responses.push_back(FolderChanged(path.clone()).into()),
458+
false => responses.push_back(FrontendMessage::CollapseFolder { path: path.clone().into() }.into()),
459+
}
460+
responses.extend(self.layer_panel_entry(path.clone()).ok().map(|data| FrontendMessage::UpdateLayer { path: path.into(), data }.into()));
432461
}
433462
SelectionChanged => {
434463
// TODO: Hoist this duplicated code into wider system
@@ -437,7 +466,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
437466
DeleteSelectedLayers => {
438467
self.backup();
439468
responses.push_front(ToolMessage::SelectedLayersChanged.into());
440-
for path in self.selected_layers().cloned() {
469+
for path in self.selected_layers().map(|path| path.to_vec()) {
441470
responses.push_front(DocumentOperation::DeleteLayer { path }.into());
442471
}
443472
}
@@ -469,14 +498,12 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
469498
let all_layer_paths = self
470499
.layer_data
471500
.keys()
472-
.filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay)
501+
.filter(|path| !path.is_empty() && !self.document.layer(path).map(|layer| layer.overlay).unwrap_or(false))
473502
.cloned()
474503
.collect::<Vec<_>>();
475504
responses.push_front(SetSelectedLayers(all_layer_paths).into());
476505
}
477-
DeselectAllLayers => {
478-
responses.push_front(SetSelectedLayers(vec![]).into());
479-
}
506+
DeselectAllLayers => responses.push_front(SetSelectedLayers(vec![]).into()),
480507
DocumentHistoryBackward => self.undo().unwrap_or_else(|e| log::warn!("{}", e)),
481508
DocumentHistoryForward => self.redo().unwrap_or_else(|e| log::warn!("{}", e)),
482509
Undo => {
@@ -505,18 +532,19 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
505532
self.layer_data.remove(&path);
506533
Some(ToolMessage::SelectedLayersChanged.into())
507534
}
508-
DocumentResponse::LayerChanged { path } => (!self.document.layer(&path).unwrap().overlay).then(|| {
509-
FrontendMessage::UpdateLayer {
510-
path: path.clone(),
511-
data: self.layer_panel_entry(path).unwrap(),
512-
}
513-
.into()
535+
DocumentResponse::LayerChanged { path } => self.layer_panel_entry(path.clone()).ok().and_then(|entry| {
536+
let overlay = self.document.layer(&path).unwrap().overlay;
537+
(!overlay).then(|| FrontendMessage::UpdateLayer { path: path.into(), data: entry }.into())
514538
}),
515-
DocumentResponse::CreatedLayer { path } => (!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into()),
539+
DocumentResponse::CreatedLayer { path } => {
540+
self.layer_data.insert(path.clone(), LayerData::new(false));
541+
(!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into())
542+
}
516543
DocumentResponse::DocumentChanged => Some(RenderDocument.into()),
517544
})
518545
.flatten(),
519546
);
547+
log::debug!("LayerPanel: {:?}", self.layer_data.keys());
520548
}
521549
Err(e) => log::error!("DocumentError: {:?}", e),
522550
Ok(_) => (),
@@ -551,7 +579,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
551579

552580
NudgeSelectedLayers(x, y) => {
553581
self.backup();
554-
for path in self.selected_layers().cloned() {
582+
for path in self.selected_layers().map(|path| path.to_vec()) {
555583
let operation = DocumentOperation::TransformLayerInViewport {
556584
path,
557585
transform: DAffine2::from_translation((x, y).into()).to_cols_array(),
@@ -561,9 +589,9 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
561589
responses.push_back(ToolMessage::SelectedLayersChanged.into());
562590
}
563591
MoveSelectedLayersTo { path, insert_index } => {
564-
responses.push_back(DocumentsMessage::CopySelectedLayers.into());
592+
responses.push_back(DocumentsMessage::Copy.into());
565593
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
566-
responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into());
594+
responses.push_back(DocumentsMessage::PasteIntoFolder { path, insert_index }.into());
567595
}
568596
ReorderSelectedLayers(relative_position) => {
569597
self.backup();
@@ -574,7 +602,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
574602
1 => selected_layers.last(),
575603
_ => unreachable!(),
576604
} {
577-
if let Some(pos) = all_layer_paths.iter().position(|path| path == pivot) {
605+
let all_layer_paths: Vec<_> = all_layer_paths
606+
.iter()
607+
.filter(|layer| layer.starts_with(&pivot[0..pivot.len() - 1]) && pivot.len() == layer.len())
608+
.collect();
609+
if let Some(pos) = all_layer_paths.iter().position(|path| *path == pivot) {
578610
let max = all_layer_paths.len() as i64 - 1;
579611
let insert_pos = (pos as i64 + relative_position as i64).clamp(0, max) as usize;
580612
let insert = all_layer_paths.get(insert_pos);
@@ -602,13 +634,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
602634
FlipAxis::X => DVec2::new(-1., 1.),
603635
FlipAxis::Y => DVec2::new(1., -1.),
604636
};
605-
if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x.as_slice())) {
637+
if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
606638
let center = (max + min) / 2.;
607639
let bbox_trans = DAffine2::from_translation(-center);
608640
for path in self.selected_layers() {
609641
responses.push_back(
610642
DocumentOperation::TransformLayerInScope {
611-
path: path.clone(),
643+
path: path.to_vec(),
612644
transform: DAffine2::from_scale(scale).to_cols_array(),
613645
scope: bbox_trans.to_cols_array(),
614646
}
@@ -627,7 +659,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
627659
AlignAxis::Y => DVec2::Y,
628660
};
629661
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5);
630-
if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x.as_slice())) {
662+
if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
631663
let aggregated = match aggregate {
632664
AlignAggregate::Min => combined_box[0],
633665
AlignAggregate::Max => combined_box[1],
@@ -643,7 +675,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
643675
let translation = (aggregated - center) * axis;
644676
responses.push_back(
645677
DocumentOperation::TransformLayerInViewport {
646-
path: path.clone(),
678+
path: path.to_vec(),
647679
transform: DAffine2::from_translation(translation).to_cols_array(),
648680
}
649681
.into(),
@@ -673,6 +705,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
673705
DuplicateSelectedLayers,
674706
NudgeSelectedLayers,
675707
ReorderSelectedLayers,
708+
GroupSelectedLayers,
676709
);
677710
common.extend(select);
678711
}

0 commit comments

Comments
 (0)