From 434970aa16d8f2499380f69b51f4271d820739a0 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 29 Jan 2022 11:04:15 -0800 Subject: [PATCH] Implement layer renaming (#501) * Implement layer renaming * Fix sneaky typo in CSS Co-authored-by: Moritz Vetter <16950410+HansAuger@users.noreply.github.com> Co-authored-by: Moritz Vetter <16950410+HansAuger@users.noreply.github.com> --- editor/src/document/document_message.rs | 4 + .../src/document/document_message_handler.rs | 10 ++ editor/src/document/layer_panel.rs | 3 +- editor/src/viewport_tools/snapping.rs | 2 +- frontend/src/App.vue | 4 + frontend/src/components/panels/LayerTree.vue | 134 ++++++++++++++---- .../components/widgets/inputs/NumberInput.vue | 6 - .../src/components/workspace/Workspace.vue | 4 +- frontend/wasm/src/api.rs | 7 + graphene/src/document.rs | 7 + graphene/src/operation.rs | 5 + 11 files changed, 144 insertions(+), 42 deletions(-) diff --git a/editor/src/document/document_message.rs b/editor/src/document/document_message.rs index 4532129e..7946f147 100644 --- a/editor/src/document/document_message.rs +++ b/editor/src/document/document_message.rs @@ -99,6 +99,10 @@ pub enum DocumentMessage { layer_path: Vec, set_expanded: bool, }, + SetLayerName { + layer_path: Vec, + name: String, + }, SetOpacityForSelectedLayers { opacity: f64, }, diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index b783e31e..478d960e 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -401,6 +401,7 @@ impl DocumentMessageHandler { } } + // TODO: This should probably take a slice not a vec, also why does this even exist when `layer_panel_entry_from_path` also exists? pub fn layer_panel_entry(&mut self, path: Vec) -> Result { let data: LayerMetadata = *self .layer_metadata @@ -927,6 +928,15 @@ impl MessageHandler for Docum responses.push_back(DocumentStructureChanged.into()); responses.push_back(LayerChanged { affected_layer_path: layer_path }.into()) } + SetLayerName { layer_path, name } => { + if let Some(layer) = self.layer_panel_entry_from_path(&layer_path) { + // Only save the history state if the name actually changed to something different + if layer.name != name { + self.backup(responses); + responses.push_back(DocumentOperation::SetLayerName { path: layer_path, name }.into()); + } + } + } SetOpacityForSelectedLayers { opacity } => { self.backup(responses); let opacity = opacity.clamp(0., 1.); diff --git a/editor/src/document/layer_panel.rs b/editor/src/document/layer_panel.rs index 80ff28e2..f637a1b6 100644 --- a/editor/src/document/layer_panel.rs +++ b/editor/src/document/layer_panel.rs @@ -21,8 +21,7 @@ impl LayerMetadata { } pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec) -> LayerPanelEntry { - let layer_type: LayerDataTypeDiscriminant = (&layer.data).into(); - let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type)); + let name = layer.name.clone().unwrap_or_else(|| String::from("")); let arr = layer.data.bounding_box(transform).unwrap_or([DVec2::ZERO, DVec2::ZERO]); let arr = arr.iter().map(|x| (*x).into()).collect::>(); diff --git a/editor/src/viewport_tools/snapping.rs b/editor/src/viewport_tools/snapping.rs index 32f9cfb0..3103f0a8 100644 --- a/editor/src/viewport_tools/snapping.rs +++ b/editor/src/viewport_tools/snapping.rs @@ -21,7 +21,7 @@ impl SnapHandler { overlay_paths: &mut Vec>, responses: &mut VecDeque, viewport_bounds: DVec2, - (positions_and_distances): (impl Iterator, impl Iterator), + positions_and_distances: (impl Iterator, impl Iterator), closest_distance: DVec2, ) { /// Draws an alignment line overlay with the correct transform and fade opacity, reusing lines from the pool if available. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 22b3735d..b6160ce5 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -87,6 +87,10 @@ button { color: var(--color-e-nearwhite); } +::selection { + background: var(--color-accent); +} + svg, img { display: block; diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index 45dbe11b..e922f624 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -29,21 +29,21 @@ - + -
+ -
+
@@ -53,27 +53,36 @@ :class="{ expanded: listing.entry.layer_metadata.expanded }" @click.stop="handleExpandArrowClick(listing.entry.path)" > -
-
+ -
-
- {{ listing.entry.name }} -
+ + + +
-
+
@@ -115,9 +124,8 @@ border-bottom: 1px solid var(--color-4-dimgray); .visibility { - height: 100%; flex: 0 0 auto; - display: flex; + height: 100%; align-items: center; .icon-button { @@ -170,14 +178,12 @@ } .layer { - display: flex; align-items: center; z-index: 1; - min-width: 0; width: 100%; height: 100%; - border-radius: 2px; padding: 0 4px; + border-radius: 2px; margin-right: 8px; &.selected { @@ -192,14 +198,41 @@ .layer-name { flex: 1 1 100%; - display: flex; - min-width: 0; margin: 0 4px; - span { + input { + color: inherit; + background: none; + border: none; + outline: none; + margin: 0; + padding: 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; + border-radius: 2px; + height: 24px; + width: 100%; + + &:disabled { + user-select: none; + // Workaround for `user-select: none` not working on elements + pointer-events: none; + } + + &::placeholder { + color: inherit; + font-style: italic; + } + + &:focus { + background: var(--color-1-nearblack); + padding: 0 4px; + + &::placeholder { + opacity: 0.5; + } + } } } @@ -254,7 +287,7 @@ import NumberInput from "@/components/widgets/inputs/NumberInput.vue"; import IconLabel from "@/components/widgets/labels/IconLabel.vue"; import Separator from "@/components/widgets/separators/Separator.vue"; -type LayerListingInfo = { entry: LayerPanelEntry; bottomLayer: boolean; folderIndex: number }; +type LayerListingInfo = { folderIndex: number; bottomLayer: boolean; editingName: boolean; entry: LayerPanelEntry }; const blendModeEntries: SectionsOfMenuListEntries = [ [{ label: "Normal", value: "Normal" }], @@ -317,7 +350,9 @@ export default defineComponent({ selectionRangeStartLayer: undefined as undefined | LayerPanelEntry, selectionRangeEndLayer: undefined as undefined | LayerPanelEntry, opacity: 100, + draggable: true, draggingData: undefined as undefined | DraggingData, + devMode: process.env.NODE_ENV === "development", }; }, methods: { @@ -336,6 +371,36 @@ export default defineComponent({ async handleExpandArrowClick(path: BigUint64Array) { this.editor.instance.toggle_layer_expansion(path); }, + onEditLayerName(listing: LayerListingInfo) { + if (listing.editingName) return; + + this.draggable = false; + + listing.editingName = true; + const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement; + this.$nextTick(() => { + (tree.querySelector("input:not([disabled])") as HTMLInputElement).select(); + }); + }, + async onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | null) { + // Eliminate duplicate events + if (!listing.editingName) return; + + this.draggable = true; + + const name = (inputElement as HTMLInputElement).value; + listing.editingName = false; + this.editor.instance.set_layer_name(listing.entry.path, name); + }, + onEditLayerNameDeselect(listing: LayerListingInfo) { + this.draggable = true; + + listing.editingName = false; + this.$nextTick(() => { + const selection = window.getSelection(); + if (selection) selection.removeAllRanges(); + }); + }, async setLayerBlendMode(newSelectedIndex: number) { const blendMode = this.blendModeEntries.flat()[newSelectedIndex].value; if (blendMode) this.editor.instance.set_blend_mode_for_selected_layers(blendMode); @@ -446,6 +511,7 @@ export default defineComponent({ this.draggingData = undefined; } }, + // TODO: Move blend mode setting logic to backend based on the layers it knows are selected setBlendModeForSelectedLayers() { const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected); @@ -466,8 +532,8 @@ export default defineComponent({ this.blendModeSelectedIndex = NaN; } }, + // TODO: Move opacity setting logic to backend based on the layers it knows are selected setOpacityForSelectedLayers() { - // todo figure out why this is here const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected); if (selected.length < 1) { @@ -490,15 +556,21 @@ export default defineComponent({ }, mounted() { this.editor.dispatcher.subscribeJsMessage(DisplayDocumentLayerTreeStructure, (displayDocumentLayerTreeStructure) => { + const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName); + const layerPathWithNameBeingEdited = layerWithNameBeingEdited && layerWithNameBeingEdited.entry.path; + const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited && layerPathWithNameBeingEdited.slice(-1)[0]; const path = [] as bigint[]; - this.layers = [] as { folderIndex: number; bottomLayer: boolean; entry: LayerPanelEntry }[]; + this.layers = [] as LayerListingInfo[]; - const recurse = (folder: DisplayDocumentLayerTreeStructure, layers: { folderIndex: number; bottomLayer: boolean; entry: LayerPanelEntry }[], cache: Map): void => { + const recurse = (folder: DisplayDocumentLayerTreeStructure, layers: LayerListingInfo[], cache: Map): void => { folder.children.forEach((item, index) => { // TODO: fix toString - path.push(BigInt(item.layerId.toString())); + const layerId = BigInt(item.layerId.toString()); + path.push(layerId); + const mapping = cache.get(path.toString()); - if (mapping) layers.push({ folderIndex: index, bottomLayer: index === folder.children.length - 1, entry: mapping }); + if (mapping) layers.push({ folderIndex: index, bottomLayer: index === folder.children.length - 1, entry: mapping, editingName: layerIdWithNameBeingEdited === layerId }); + if (item.children.length >= 1) recurse(item, layers, cache); path.pop(); }); diff --git a/frontend/src/components/widgets/inputs/NumberInput.vue b/frontend/src/components/widgets/inputs/NumberInput.vue index c6a40549..7420c8d9 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.vue +++ b/frontend/src/components/widgets/inputs/NumberInput.vue @@ -52,8 +52,6 @@ border: none; background: none; color: var(--color-e-nearwhite); - font-size: inherit; - font-family: inherit; text-align: center; &:not(:focus).has-label { @@ -62,10 +60,6 @@ margin-right: 8px; } - &::selection { - background: var(--color-accent); - } - &:focus { text-align: left; diff --git a/frontend/src/components/workspace/Workspace.vue b/frontend/src/components/workspace/Workspace.vue index 44bfdceb..528f2db0 100644 --- a/frontend/src/components/workspace/Workspace.vue +++ b/frontend/src/components/workspace/Workspace.vue @@ -23,12 +23,12 @@ ref="documentsPanel" /> - + - + diff --git a/frontend/wasm/src/api.rs b/frontend/wasm/src/api.rs index 7ed30d15..93e55e06 100644 --- a/frontend/wasm/src/api.rs +++ b/frontend/wasm/src/api.rs @@ -364,6 +364,7 @@ impl JsEditorHandle { self.dispatch(message); } + /// Modify the layer selection based on the layer which is clicked while holding down the Ctrl and/or Shift modifier keys used for range selection behavior pub fn select_layer(&self, layer_path: Vec, ctrl: bool, shift: bool) { let message = DocumentMessage::SelectLayer { layer_path, ctrl, shift }; self.dispatch(message); @@ -397,6 +398,12 @@ impl JsEditorHandle { self.dispatch(message); } + /// Set the name for the layer + pub fn set_layer_name(&self, layer_path: Vec, name: String) { + let message = DocumentMessage::SetLayerName { layer_path, name }; + self.dispatch(message); + } + /// Set the blend mode for the selected layers pub fn set_blend_mode_for_selected_layers(&self, blend_mode_svg_style_name: String) -> Result<(), JsValue> { if let Some(blend_mode) = translate_blend_mode(blend_mode_svg_style_name.as_str()) { diff --git a/graphene/src/document.rs b/graphene/src/document.rs index c78afa85..14b3d7b6 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -641,6 +641,13 @@ impl Document { layer.visible = *visible; Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) } + Operation::SetLayerName { path, name } => { + self.mark_as_dirty(path)?; + let mut layer = self.layer_mut(path)?; + layer.name = if name.as_str() == "" { None } else { Some(name.clone()) }; + + Some(vec![LayerChanged { path: path.clone() }]) + } Operation::SetLayerBlendMode { path, blend_mode } => { self.mark_as_dirty(path)?; self.layer_mut(path)?.blend_mode = *blend_mode; diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index 5e07c142..087a0430 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -10,6 +10,7 @@ use std::hash::{Hash, Hasher}; #[repr(C)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +// TODO: Rename all instances of `path` to `layer_path` pub enum Operation { AddEllipse { path: Vec, @@ -124,6 +125,10 @@ pub enum Operation { path: Vec, visible: bool, }, + SetLayerName { + path: Vec, + name: String, + }, SetLayerBlendMode { path: Vec, blend_mode: BlendMode,