Skip to content

Commit

Permalink
Add folders to frontend and folder creation to backend (#315)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
Keavon and TrueDoctor committed Aug 29, 2021
1 parent e757143 commit 6b274b3
Show file tree
Hide file tree
Showing 23 changed files with 460 additions and 201 deletions.
22 changes: 11 additions & 11 deletions editor/src/communication/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ mod test {
let mut editor = create_editor_with_three_layers();

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

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

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

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

Expand Down Expand Up @@ -192,7 +192,7 @@ mod test {
const LINE_INDEX: usize = 0;
const PEN_INDEX: usize = 1;

editor.handle_message(DocumentMessage::AddFolder(vec![])).unwrap();
editor.handle_message(DocumentMessage::CreateFolder(vec![])).unwrap();

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

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

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

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

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

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

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

Expand Down
97 changes: 65 additions & 32 deletions editor/src/document/document_file.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
pub use super::layer_panel::*;
use crate::{
consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING},
frontend::layer_panel::*,
EditorError,
};
use glam::{DAffine2, DVec2};
Expand Down Expand Up @@ -93,16 +92,17 @@ pub enum DocumentMessage {
DeleteLayer(Vec<LayerId>),
DeleteSelectedLayers,
DuplicateSelectedLayers,
CreateFolder(Vec<LayerId>),
SetBlendModeForSelectedLayers(BlendMode),
SetOpacityForSelectedLayers(f64),
AddFolder(Vec<LayerId>),
RenameLayer(Vec<LayerId>, String),
ToggleLayerVisibility(Vec<LayerId>),
FlipSelectedLayers(FlipAxis),
ToggleLayerExpansion(Vec<LayerId>),
FolderChanged(Vec<LayerId>),
StartTransaction,
RollbackTransaction,
GroupSelectedLayers,
AbortTransaction,
CommitTransaction,
ExportDocument,
Expand Down Expand Up @@ -139,7 +139,7 @@ impl DocumentMessageHandler {
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, children }.into()
FrontendMessage::ExpandFolder { path: path.into(), children }.into()
})
}

Expand All @@ -154,7 +154,7 @@ impl DocumentMessageHandler {
self.layer_data(path).selected = true;
let data = self.layer_panel_entry(path.to_vec()).ok()?;
// TODO: Add deduplication
(!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec(), 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]> {
Expand All @@ -165,9 +165,9 @@ impl DocumentMessageHandler {
// TODO: Consider moving this to some kind of overlay manager in the future
pub fn selected_layers_vector_points(&self) -> Vec<VectorManipulatorShape> {
let shapes = self.selected_layers().filter_map(|path_to_shape| {
let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape.as_slice()).ok()?;
let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape).ok()?;

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

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

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

Expand Down Expand Up @@ -362,7 +362,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
match message {
Movement(message) => self.movement_handler.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.document, ipp), responses),
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
AddFolder(path) => responses.push_back(DocumentOperation::AddFolder { path }.into()),
StartTransaction => self.backup(),
RollbackTransaction => {
self.rollback().unwrap_or_else(|e| log::warn!("{}", e));
Expand Down Expand Up @@ -409,6 +408,32 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
.into(),
)
}
CreateFolder(mut path) => {
let id = generate_uuid();
path.push(id);
self.layerdata_mut(&path).expanded = true;
responses.push_back(DocumentOperation::CreateFolder { path }.into())
}
GroupSelectedLayers => {
let common_prefix = self.document.common_prefix(self.selected_layers());
let (_id, common_prefix) = common_prefix.split_last().unwrap_or((&0, &[]));

let mut new_folder_path = common_prefix.to_vec();
new_folder_path.push(generate_uuid());

responses.push_back(DocumentsMessage::Copy.into());
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
responses.push_back(DocumentOperation::CreateFolder { path: new_folder_path.clone() }.into());
responses.push_back(DocumentMessage::ToggleLayerExpansion(new_folder_path.clone()).into());
responses.push_back(
DocumentsMessage::PasteIntoFolder {
path: new_folder_path.clone(),
insert_index: -1,
}
.into(),
);
responses.push_back(DocumentMessage::SetSelectedLayers(vec![new_folder_path]).into());
}
SetBlendModeForSelectedLayers(blend_mode) => {
self.backup();
for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
Expand All @@ -419,7 +444,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
self.backup();
let opacity = opacity.clamp(0., 1.);

for path in self.selected_layers().cloned() {
for path in self.selected_layers().map(|path| path.to_vec()) {
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
}
}
Expand All @@ -428,7 +453,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}
ToggleLayerExpansion(path) => {
self.layer_data(&path).expanded ^= true;
responses.push_back(FolderChanged(path).into());
match self.layer_data(&path).expanded {
true => responses.push_back(FolderChanged(path.clone()).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 => {
// TODO: Hoist this duplicated code into wider system
Expand All @@ -437,7 +466,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
DeleteSelectedLayers => {
self.backup();
responses.push_front(ToolMessage::SelectedLayersChanged.into());
for path in self.selected_layers().cloned() {
for path in self.selected_layers().map(|path| path.to_vec()) {
responses.push_front(DocumentOperation::DeleteLayer { path }.into());
}
}
Expand Down Expand Up @@ -469,14 +498,12 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
let all_layer_paths = self
.layer_data
.keys()
.filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay)
.filter(|path| !path.is_empty() && !self.document.layer(path).map(|layer| layer.overlay).unwrap_or(false))
.cloned()
.collect::<Vec<_>>();
responses.push_front(SetSelectedLayers(all_layer_paths).into());
}
DeselectAllLayers => {
responses.push_front(SetSelectedLayers(vec![]).into());
}
DeselectAllLayers => responses.push_front(SetSelectedLayers(vec![]).into()),
DocumentHistoryBackward => self.undo().unwrap_or_else(|e| log::warn!("{}", e)),
DocumentHistoryForward => self.redo().unwrap_or_else(|e| log::warn!("{}", e)),
Undo => {
Expand Down Expand Up @@ -505,18 +532,19 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
self.layer_data.remove(&path);
Some(ToolMessage::SelectedLayersChanged.into())
}
DocumentResponse::LayerChanged { path } => (!self.document.layer(&path).unwrap().overlay).then(|| {
FrontendMessage::UpdateLayer {
path: path.clone(),
data: self.layer_panel_entry(path).unwrap(),
}
.into()
DocumentResponse::LayerChanged { path } => self.layer_panel_entry(path.clone()).ok().and_then(|entry| {
let overlay = self.document.layer(&path).unwrap().overlay;
(!overlay).then(|| FrontendMessage::UpdateLayer { path: path.into(), data: entry }.into())
}),
DocumentResponse::CreatedLayer { path } => (!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into()),
DocumentResponse::CreatedLayer { path } => {
self.layer_data.insert(path.clone(), LayerData::new(false));
(!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into())
}
DocumentResponse::DocumentChanged => Some(RenderDocument.into()),
})
.flatten(),
);
log::debug!("LayerPanel: {:?}", self.layer_data.keys());
}
Err(e) => log::error!("DocumentError: {:?}", e),
Ok(_) => (),
Expand Down Expand Up @@ -551,7 +579,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand

NudgeSelectedLayers(x, y) => {
self.backup();
for path in self.selected_layers().cloned() {
for path in self.selected_layers().map(|path| path.to_vec()) {
let operation = DocumentOperation::TransformLayerInViewport {
path,
transform: DAffine2::from_translation((x, y).into()).to_cols_array(),
Expand All @@ -561,9 +589,9 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_back(ToolMessage::SelectedLayersChanged.into());
}
MoveSelectedLayersTo { path, insert_index } => {
responses.push_back(DocumentsMessage::CopySelectedLayers.into());
responses.push_back(DocumentsMessage::Copy.into());
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into());
responses.push_back(DocumentsMessage::PasteIntoFolder { path, insert_index }.into());
}
ReorderSelectedLayers(relative_position) => {
self.backup();
Expand All @@ -574,7 +602,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
1 => selected_layers.last(),
_ => unreachable!(),
} {
if let Some(pos) = all_layer_paths.iter().position(|path| path == pivot) {
let all_layer_paths: Vec<_> = all_layer_paths
.iter()
.filter(|layer| layer.starts_with(&pivot[0..pivot.len() - 1]) && pivot.len() == layer.len())
.collect();
if let Some(pos) = all_layer_paths.iter().position(|path| *path == pivot) {
let max = all_layer_paths.len() as i64 - 1;
let insert_pos = (pos as i64 + relative_position as i64).clamp(0, max) as usize;
let insert = all_layer_paths.get(insert_pos);
Expand Down Expand Up @@ -602,13 +634,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
FlipAxis::X => 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.as_slice())) {
if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
let center = (max + min) / 2.;
let bbox_trans = DAffine2::from_translation(-center);
for path in self.selected_layers() {
responses.push_back(
DocumentOperation::TransformLayerInScope {
path: path.clone(),
path: path.to_vec(),
transform: DAffine2::from_scale(scale).to_cols_array(),
scope: bbox_trans.to_cols_array(),
}
Expand All @@ -627,7 +659,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
AlignAxis::Y => DVec2::Y,
};
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.as_slice())) {
if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x)) {
let aggregated = match aggregate {
AlignAggregate::Min => combined_box[0],
AlignAggregate::Max => combined_box[1],
Expand All @@ -643,7 +675,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
let translation = (aggregated - center) * axis;
responses.push_back(
DocumentOperation::TransformLayerInViewport {
path: path.clone(),
path: path.to_vec(),
transform: DAffine2::from_translation(translation).to_cols_array(),
}
.into(),
Expand Down Expand Up @@ -673,6 +705,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
DuplicateSelectedLayers,
NudgeSelectedLayers,
ReorderSelectedLayers,
GroupSelectedLayers,
);
common.extend(select);
}
Expand Down
Loading

0 comments on commit 6b274b3

Please sign in to comment.