mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 20:38:13 +08:00
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>
This commit is contained in:
co-authored by
Moritz Vetter
parent
7910395191
commit
dae79caf15
@@ -99,6 +99,10 @@ pub enum DocumentMessage {
|
|||||||
layer_path: Vec<LayerId>,
|
layer_path: Vec<LayerId>,
|
||||||
set_expanded: bool,
|
set_expanded: bool,
|
||||||
},
|
},
|
||||||
|
SetLayerName {
|
||||||
|
layer_path: Vec<LayerId>,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
SetOpacityForSelectedLayers {
|
SetOpacityForSelectedLayers {
|
||||||
opacity: f64,
|
opacity: f64,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
||||||
let data: LayerMetadata = *self
|
let data: LayerMetadata = *self
|
||||||
.layer_metadata
|
.layer_metadata
|
||||||
@@ -927,6 +928,15 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
|||||||
responses.push_back(DocumentStructureChanged.into());
|
responses.push_back(DocumentStructureChanged.into());
|
||||||
responses.push_back(LayerChanged { affected_layer_path: layer_path }.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 } => {
|
SetOpacityForSelectedLayers { opacity } => {
|
||||||
self.backup(responses);
|
self.backup(responses);
|
||||||
let opacity = opacity.clamp(0., 1.);
|
let opacity = opacity.clamp(0., 1.);
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ impl LayerMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>) -> LayerPanelEntry {
|
pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>) -> LayerPanelEntry {
|
||||||
let layer_type: LayerDataTypeDiscriminant = (&layer.data).into();
|
let name = layer.name.clone().unwrap_or_else(|| String::from(""));
|
||||||
let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type));
|
|
||||||
let arr = layer.data.bounding_box(transform).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
let arr = layer.data.bounding_box(transform).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
||||||
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
|
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ impl SnapHandler {
|
|||||||
overlay_paths: &mut Vec<Vec<LayerId>>,
|
overlay_paths: &mut Vec<Vec<LayerId>>,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
viewport_bounds: DVec2,
|
viewport_bounds: DVec2,
|
||||||
(positions_and_distances): (impl Iterator<Item = (f64, f64)>, impl Iterator<Item = (f64, f64)>),
|
positions_and_distances: (impl Iterator<Item = (f64, f64)>, impl Iterator<Item = (f64, f64)>),
|
||||||
closest_distance: DVec2,
|
closest_distance: DVec2,
|
||||||
) {
|
) {
|
||||||
/// Draws an alignment line overlay with the correct transform and fade opacity, reusing lines from the pool if available.
|
/// Draws an alignment line overlay with the correct transform and fade opacity, reusing lines from the pool if available.
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ button {
|
|||||||
color: var(--color-e-nearwhite);
|
color: var(--color-e-nearwhite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
svg,
|
svg,
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -29,21 +29,21 @@
|
|||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="layer-tree" :scrollableY="true">
|
<LayoutRow class="layer-tree" :scrollableY="true">
|
||||||
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="updateInsertLine($event)" @dragend="drop()">
|
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e) => draggable && updateInsertLine(e)" @dragend="draggable && drop()">
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
class="layer-row"
|
class="layer-row"
|
||||||
v-for="(listing, index) in layers"
|
v-for="(listing, index) in layers"
|
||||||
:key="String(listing.entry.path.slice(-1))"
|
:key="String(listing.entry.path.slice(-1))"
|
||||||
:class="{ 'insert-folder': draggingData && draggingData.highlightFolder && draggingData.insertFolder === listing.entry.path }"
|
:class="{ 'insert-folder': draggingData && draggingData.highlightFolder && draggingData.insertFolder === listing.entry.path }"
|
||||||
>
|
>
|
||||||
<div class="visibility">
|
<LayoutRow class="visibility">
|
||||||
<IconButton
|
<IconButton
|
||||||
:action="(e) => (toggleLayerVisibility(listing.entry.path), e && e.stopPropagation())"
|
:action="(e) => (toggleLayerVisibility(listing.entry.path), e && e.stopPropagation())"
|
||||||
:size="24"
|
:size="24"
|
||||||
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
|
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
|
||||||
:title="listing.entry.visible ? 'Visible' : 'Hidden'"
|
:title="listing.entry.visible ? 'Visible' : 'Hidden'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</LayoutRow>
|
||||||
|
|
||||||
<div class="indent" :style="{ marginLeft: layerIndent(listing.entry) }"></div>
|
<div class="indent" :style="{ marginLeft: layerIndent(listing.entry) }"></div>
|
||||||
|
|
||||||
@@ -53,27 +53,36 @@
|
|||||||
:class="{ expanded: listing.entry.layer_metadata.expanded }"
|
:class="{ expanded: listing.entry.layer_metadata.expanded }"
|
||||||
@click.stop="handleExpandArrowClick(listing.entry.path)"
|
@click.stop="handleExpandArrowClick(listing.entry.path)"
|
||||||
></button>
|
></button>
|
||||||
<div
|
<LayoutRow
|
||||||
class="layer"
|
class="layer"
|
||||||
:class="{ selected: listing.entry.layer_metadata.selected }"
|
:class="{ selected: listing.entry.layer_metadata.selected }"
|
||||||
@click.shift.exact.stop="selectLayer(listing.entry, false, true)"
|
@click.shift.exact.stop="!listing.editingName && selectLayer(listing.entry, false, true)"
|
||||||
@click.shift.ctrl.exact.stop="selectLayer(listing.entry, true, true)"
|
@click.shift.ctrl.exact.stop="!listing.editingName && selectLayer(listing.entry, true, true)"
|
||||||
@click.ctrl.exact.stop="selectLayer(listing.entry, true, false)"
|
@click.ctrl.exact.stop="!listing.editingName && selectLayer(listing.entry, true, false)"
|
||||||
@click.exact.stop="selectLayer(listing.entry, false, false)"
|
@click.exact.stop="!listing.editingName && selectLayer(listing.entry, false, false)"
|
||||||
:data-index="index"
|
:data-index="index"
|
||||||
draggable="true"
|
:draggable="draggable"
|
||||||
@dragstart="dragStart($event, listing.entry)"
|
@dragstart="(e) => draggable && dragStart(e, listing.entry)"
|
||||||
:title="String(listing.entry.path)"
|
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`"
|
||||||
>
|
>
|
||||||
<div class="layer-type-icon">
|
<LayoutRow class="layer-type-icon">
|
||||||
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeTypeFolder'" title="Folder" />
|
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeTypeFolder'" title="Folder" />
|
||||||
<IconLabel v-else :icon="'NodeTypePath'" title="Path" />
|
<IconLabel v-else :icon="'NodeTypePath'" title="Path" />
|
||||||
</div>
|
</LayoutRow>
|
||||||
<div class="layer-name">
|
<LayoutRow class="layer-name" @dblclick="onEditLayerName(listing)">
|
||||||
<span>{{ listing.entry.name }}</span>
|
<input
|
||||||
</div>
|
type="text"
|
||||||
|
:value="listing.entry.name"
|
||||||
|
:placeholder="listing.entry.layer_type"
|
||||||
|
:disabled="!listing.editingName"
|
||||||
|
@change="(e) => onEditLayerNameChange(listing, e.target)"
|
||||||
|
@blur="onEditLayerNameDeselect(listing)"
|
||||||
|
@keydown.enter="(e) => onEditLayerNameChange(listing, e.target)"
|
||||||
|
@keydown.escape="onEditLayerNameDeselect(listing)"
|
||||||
|
/>
|
||||||
|
</LayoutRow>
|
||||||
<div class="thumbnail" v-html="listing.entry.thumbnail"></div>
|
<div class="thumbnail" v-html="listing.entry.thumbnail"></div>
|
||||||
</div>
|
</LayoutRow>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
<div class="insert-mark" v-if="draggingData && !draggingData.highlightFolder" :style="{ left: markIndent(draggingData.insertFolder), top: markTopOffset(draggingData.markerHeight) }"></div>
|
<div class="insert-mark" v-if="draggingData && !draggingData.highlightFolder" :style="{ left: markIndent(draggingData.insertFolder), top: markTopOffset(draggingData.markerHeight) }"></div>
|
||||||
@@ -115,9 +124,8 @@
|
|||||||
border-bottom: 1px solid var(--color-4-dimgray);
|
border-bottom: 1px solid var(--color-4-dimgray);
|
||||||
|
|
||||||
.visibility {
|
.visibility {
|
||||||
height: 100%;
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
@@ -170,14 +178,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layer {
|
.layer {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 2px;
|
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
|
border-radius: 2px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
@@ -192,14 +198,41 @@
|
|||||||
|
|
||||||
.layer-name {
|
.layer-name {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
|
|
||||||
span {
|
input {
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 24px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
user-select: none;
|
||||||
|
// Workaround for `user-select: none` not working on <input> 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 IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
import Separator from "@/components/widgets/separators/Separator.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<BlendMode> = [
|
const blendModeEntries: SectionsOfMenuListEntries<BlendMode> = [
|
||||||
[{ label: "Normal", value: "Normal" }],
|
[{ label: "Normal", value: "Normal" }],
|
||||||
@@ -317,7 +350,9 @@ export default defineComponent({
|
|||||||
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
||||||
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
|
draggable: true,
|
||||||
draggingData: undefined as undefined | DraggingData,
|
draggingData: undefined as undefined | DraggingData,
|
||||||
|
devMode: process.env.NODE_ENV === "development",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -336,6 +371,36 @@ export default defineComponent({
|
|||||||
async handleExpandArrowClick(path: BigUint64Array) {
|
async handleExpandArrowClick(path: BigUint64Array) {
|
||||||
this.editor.instance.toggle_layer_expansion(path);
|
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) {
|
async setLayerBlendMode(newSelectedIndex: number) {
|
||||||
const blendMode = this.blendModeEntries.flat()[newSelectedIndex].value;
|
const blendMode = this.blendModeEntries.flat()[newSelectedIndex].value;
|
||||||
if (blendMode) this.editor.instance.set_blend_mode_for_selected_layers(blendMode);
|
if (blendMode) this.editor.instance.set_blend_mode_for_selected_layers(blendMode);
|
||||||
@@ -446,6 +511,7 @@ export default defineComponent({
|
|||||||
this.draggingData = undefined;
|
this.draggingData = undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// TODO: Move blend mode setting logic to backend based on the layers it knows are selected
|
||||||
setBlendModeForSelectedLayers() {
|
setBlendModeForSelectedLayers() {
|
||||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
||||||
|
|
||||||
@@ -466,8 +532,8 @@ export default defineComponent({
|
|||||||
this.blendModeSelectedIndex = NaN;
|
this.blendModeSelectedIndex = NaN;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// TODO: Move opacity setting logic to backend based on the layers it knows are selected
|
||||||
setOpacityForSelectedLayers() {
|
setOpacityForSelectedLayers() {
|
||||||
// todo figure out why this is here
|
|
||||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
||||||
|
|
||||||
if (selected.length < 1) {
|
if (selected.length < 1) {
|
||||||
@@ -490,15 +556,21 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.editor.dispatcher.subscribeJsMessage(DisplayDocumentLayerTreeStructure, (displayDocumentLayerTreeStructure) => {
|
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[];
|
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<string, LayerPanelEntry>): void => {
|
const recurse = (folder: DisplayDocumentLayerTreeStructure, layers: LayerListingInfo[], cache: Map<string, LayerPanelEntry>): void => {
|
||||||
folder.children.forEach((item, index) => {
|
folder.children.forEach((item, index) => {
|
||||||
// TODO: fix toString
|
// TODO: fix toString
|
||||||
path.push(BigInt(item.layerId.toString()));
|
const layerId = BigInt(item.layerId.toString());
|
||||||
|
path.push(layerId);
|
||||||
|
|
||||||
const mapping = cache.get(path.toString());
|
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);
|
if (item.children.length >= 1) recurse(item, layers, cache);
|
||||||
path.pop();
|
path.pop();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,8 +52,6 @@
|
|||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--color-e-nearwhite);
|
color: var(--color-e-nearwhite);
|
||||||
font-size: inherit;
|
|
||||||
font-family: inherit;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:not(:focus).has-label {
|
&:not(:focus).has-label {
|
||||||
@@ -62,10 +60,6 @@
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::selection {
|
|
||||||
background: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,12 @@
|
|||||||
ref="documentsPanel"
|
ref="documentsPanel"
|
||||||
/>
|
/>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="resizePanel($event)"></LayoutCol>
|
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="(e) => resizePanel(e)"></LayoutCol>
|
||||||
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.17">
|
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.17">
|
||||||
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 402">
|
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 402">
|
||||||
<Panel :panelType="'Properties'" :tabLabels="['Properties']" :tabActiveIndex="0" />
|
<Panel :panelType="'Properties'" :tabLabels="['Properties']" :tabActiveIndex="0" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="resizePanel($event)"></LayoutRow>
|
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e) => resizePanel(e)"></LayoutRow>
|
||||||
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 590">
|
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 590">
|
||||||
<Panel :panelType="'LayerTree'" :tabLabels="['Layer Tree']" :tabActiveIndex="0" />
|
<Panel :panelType="'LayerTree'" :tabLabels="['Layer Tree']" :tabActiveIndex="0" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Modify the layer selection based on the layer which is clicked while holding down the <kbd>Ctrl</kbd> and/or <kbd>Shift</kbd> modifier keys used for range selection behavior
|
||||||
pub fn select_layer(&self, layer_path: Vec<LayerId>, ctrl: bool, shift: bool) {
|
pub fn select_layer(&self, layer_path: Vec<LayerId>, ctrl: bool, shift: bool) {
|
||||||
let message = DocumentMessage::SelectLayer { layer_path, ctrl, shift };
|
let message = DocumentMessage::SelectLayer { layer_path, ctrl, shift };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
@@ -397,6 +398,12 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the name for the layer
|
||||||
|
pub fn set_layer_name(&self, layer_path: Vec<LayerId>, name: String) {
|
||||||
|
let message = DocumentMessage::SetLayerName { layer_path, name };
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the blend mode for the selected layers
|
/// 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> {
|
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()) {
|
if let Some(blend_mode) = translate_blend_mode(blend_mode_svg_style_name.as_str()) {
|
||||||
|
|||||||
@@ -641,6 +641,13 @@ impl Document {
|
|||||||
layer.visible = *visible;
|
layer.visible = *visible;
|
||||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
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 } => {
|
Operation::SetLayerBlendMode { path, blend_mode } => {
|
||||||
self.mark_as_dirty(path)?;
|
self.mark_as_dirty(path)?;
|
||||||
self.layer_mut(path)?.blend_mode = *blend_mode;
|
self.layer_mut(path)?.blend_mode = *blend_mode;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use std::hash::{Hash, Hasher};
|
|||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
|
// TODO: Rename all instances of `path` to `layer_path`
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
AddEllipse {
|
AddEllipse {
|
||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
@@ -124,6 +125,10 @@ pub enum Operation {
|
|||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
},
|
},
|
||||||
|
SetLayerName {
|
||||||
|
path: Vec<LayerId>,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
SetLayerBlendMode {
|
SetLayerBlendMode {
|
||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
blend_mode: BlendMode,
|
blend_mode: BlendMode,
|
||||||
|
|||||||
Reference in New Issue
Block a user