Fix/selection toolbox reflow (#5158)
* fix: layout perf issue * feat: skip a whole host of transform issues created by the SelectionOverlay and instead allowing the canvas to render the overlay and then injecting props to the SelecitonToolbox itself * refactor: removed unused files/functionality * refactor: removed unused types * fix: z index issue * fix: PR feedback * fix: PR feedback and more perf improvements * Update test expectations [skip ci] --------- Co-authored-by: github-actions <github-actions@github.com>
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 114 KiB |
@@ -38,9 +38,7 @@
|
|||||||
canvasStore.canvas to be initialized. -->
|
canvasStore.canvas to be initialized. -->
|
||||||
<template v-if="comfyAppReady">
|
<template v-if="comfyAppReady">
|
||||||
<TitleEditor />
|
<TitleEditor />
|
||||||
<SelectionOverlay v-if="selectionToolboxEnabled">
|
<SelectionToolbox v-if="selectionToolboxEnabled" />
|
||||||
<SelectionToolbox />
|
|
||||||
</SelectionOverlay>
|
|
||||||
<DomWidgets />
|
<DomWidgets />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -55,7 +53,6 @@ import DomWidgets from '@/components/graph/DomWidgets.vue'
|
|||||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||||
import MiniMap from '@/components/graph/MiniMap.vue'
|
import MiniMap from '@/components/graph/MiniMap.vue'
|
||||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||||
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
|
|
||||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||||
import TitleEditor from '@/components/graph/TitleEditor.vue'
|
import TitleEditor from '@/components/graph/TitleEditor.vue'
|
||||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
<!-- This component is used to bound the selected items on the canvas. -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-show="visible"
|
|
||||||
class="selection-overlay-container pointer-events-none z-40"
|
|
||||||
:class="{
|
|
||||||
'show-border': showBorder
|
|
||||||
}"
|
|
||||||
:style="style"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { whenever } from '@vueuse/core'
|
|
||||||
import { provide, readonly, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
|
||||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
|
||||||
import { createBounds } from '@/lib/litegraph/src/litegraph'
|
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
|
||||||
import { SelectionOverlayInjectionKey } from '@/types/selectionOverlayTypes'
|
|
||||||
|
|
||||||
const canvasStore = useCanvasStore()
|
|
||||||
const { style, updatePosition } = useAbsolutePosition()
|
|
||||||
const { getSelectableItems } = useSelectedLiteGraphItems()
|
|
||||||
|
|
||||||
const visible = ref(false)
|
|
||||||
const showBorder = ref(false)
|
|
||||||
// Increment counter to notify child components of position/visibility change
|
|
||||||
// This does not include viewport changes.
|
|
||||||
const overlayUpdateCount = ref(0)
|
|
||||||
provide(SelectionOverlayInjectionKey, {
|
|
||||||
visible: readonly(visible),
|
|
||||||
updateCount: readonly(overlayUpdateCount)
|
|
||||||
})
|
|
||||||
|
|
||||||
const positionSelectionOverlay = () => {
|
|
||||||
const selectableItems = getSelectableItems()
|
|
||||||
showBorder.value = selectableItems.size > 1
|
|
||||||
|
|
||||||
if (!selectableItems.size) {
|
|
||||||
visible.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
visible.value = true
|
|
||||||
const bounds = createBounds(selectableItems)
|
|
||||||
if (bounds) {
|
|
||||||
updatePosition({
|
|
||||||
pos: [bounds[0], bounds[1]],
|
|
||||||
size: [bounds[2], bounds[3]]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
whenever(
|
|
||||||
() => canvasStore.getCanvas().state.selectionChanged,
|
|
||||||
() => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
positionSelectionOverlay()
|
|
||||||
overlayUpdateCount.value++
|
|
||||||
canvasStore.getCanvas().state.selectionChanged = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
canvasStore.getCanvas().ds.onChanged = positionSelectionOverlay
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => canvasStore.canvas?.state?.draggingItems,
|
|
||||||
(draggingItems) => {
|
|
||||||
// Litegraph draggingItems state can end early before the bounding boxes of
|
|
||||||
// the selected items are updated. Delay to make sure we put the overlay in
|
|
||||||
// the correct position.
|
|
||||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/2656
|
|
||||||
if (draggingItems === false) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
visible.value = true
|
|
||||||
positionSelectionOverlay()
|
|
||||||
overlayUpdateCount.value++
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Selection change update to visible state is delayed by a frame. Here
|
|
||||||
// we also delay a frame so that the order of events is correct when
|
|
||||||
// the initial selection and dragging happens at the same time.
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
visible.value = false
|
|
||||||
overlayUpdateCount.value++
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.selection-overlay-container > * {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-border {
|
|
||||||
@apply border-dashed rounded-md border-2 border-[var(--border-color)];
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,34 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<Panel
|
<Transition name="slide-up">
|
||||||
class="selection-toolbox absolute left-1/2 rounded-lg"
|
<!-- Wrapping panel in div to get correct ref because panel ref is not of raw dom el -->
|
||||||
:class="{ 'animate-slide-up': shouldAnimate }"
|
<div
|
||||||
:pt="{
|
v-show="visible"
|
||||||
header: 'hidden',
|
ref="toolboxRef"
|
||||||
content: 'p-0 flex flex-row'
|
style="
|
||||||
}"
|
transform: translate(calc(var(--tb-x) - 50%), calc(var(--tb-y) - 120%));
|
||||||
@wheel="canvasInteractions.handleWheel"
|
"
|
||||||
>
|
class="selection-toolbox fixed left-0 top-0 z-40"
|
||||||
<ExecuteButton />
|
>
|
||||||
<ColorPickerButton />
|
<Panel
|
||||||
<BypassButton />
|
class="rounded-lg"
|
||||||
<PinButton />
|
:pt="{
|
||||||
<Load3DViewerButton />
|
header: 'hidden',
|
||||||
<MaskEditorButton />
|
content: 'p-0 flex flex-row'
|
||||||
<ConvertToSubgraphButton />
|
}"
|
||||||
<DeleteButton />
|
@wheel="canvasInteractions.handleWheel"
|
||||||
<RefreshSelectionButton />
|
>
|
||||||
<ExtensionCommandButton
|
<ExecuteButton />
|
||||||
v-for="command in extensionToolboxCommands"
|
<ColorPickerButton />
|
||||||
:key="command.id"
|
<BypassButton />
|
||||||
:command="command"
|
<PinButton />
|
||||||
/>
|
<Load3DViewerButton />
|
||||||
<HelpButton />
|
<MaskEditorButton />
|
||||||
</Panel>
|
<ConvertToSubgraphButton />
|
||||||
|
<DeleteButton />
|
||||||
|
<RefreshSelectionButton />
|
||||||
|
<ExtensionCommandButton
|
||||||
|
v-for="command in extensionToolboxCommands"
|
||||||
|
:key="command.id"
|
||||||
|
:command="command"
|
||||||
|
/>
|
||||||
|
<HelpButton />
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Panel from 'primevue/panel'
|
import Panel from 'primevue/panel'
|
||||||
import { computed, inject } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
||||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||||
@@ -41,23 +52,19 @@ import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewer
|
|||||||
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
||||||
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
||||||
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
|
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
|
||||||
import { useRetriggerableAnimation } from '@/composables/element/useRetriggerableAnimation'
|
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
|
||||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||||
import { useExtensionService } from '@/services/extensionService'
|
import { useExtensionService } from '@/services/extensionService'
|
||||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
import { SelectionOverlayInjectionKey } from '@/types/selectionOverlayTypes'
|
|
||||||
|
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const extensionService = useExtensionService()
|
const extensionService = useExtensionService()
|
||||||
const canvasInteractions = useCanvasInteractions()
|
const canvasInteractions = useCanvasInteractions()
|
||||||
|
|
||||||
const selectionOverlayState = inject(SelectionOverlayInjectionKey)
|
const toolboxRef = ref<HTMLElement | undefined>()
|
||||||
const { shouldAnimate } = useRetriggerableAnimation(
|
const { visible } = useSelectionToolboxPosition(toolboxRef)
|
||||||
selectionOverlayState?.updateCount,
|
|
||||||
{ animateOnMount: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||||
const commandIds = new Set<string>(
|
const commandIds = new Set<string>(
|
||||||
@@ -77,23 +84,22 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.selection-toolbox {
|
.slide-up-enter-active {
|
||||||
transform: translateX(-50%) translateY(-120%);
|
opacity: 1;
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slide up animation using CSS animation */
|
.slide-up-leave-active {
|
||||||
@keyframes slideUp {
|
transition: none;
|
||||||
from {
|
|
||||||
transform: translateX(-50%) translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(-50%) translateY(-120%);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-up {
|
.slide-up-enter-from {
|
||||||
animation: slideUp 0.3s ease-out;
|
transform: translateY(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-leave-to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
113
src/composables/canvas/useSelectionToolboxPosition.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||||
|
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||||
|
import { createBounds } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the position of the selection toolbox independently.
|
||||||
|
* Uses CSS custom properties for performant transform updates.
|
||||||
|
*/
|
||||||
|
export function useSelectionToolboxPosition(
|
||||||
|
toolboxRef: Ref<HTMLElement | undefined>
|
||||||
|
) {
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
const lgCanvas = canvasStore.getCanvas()
|
||||||
|
const { getSelectableItems } = useSelectedLiteGraphItems()
|
||||||
|
|
||||||
|
// World position of selection center
|
||||||
|
const worldPosition = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update position based on selection
|
||||||
|
*/
|
||||||
|
const updateSelectionBounds = () => {
|
||||||
|
const selectableItems = getSelectableItems()
|
||||||
|
|
||||||
|
if (!selectableItems.size) {
|
||||||
|
visible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
visible.value = true
|
||||||
|
const bounds = createBounds(selectableItems)
|
||||||
|
|
||||||
|
if (!bounds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [xBase, y, width] = bounds
|
||||||
|
|
||||||
|
worldPosition.value = {
|
||||||
|
x: xBase + width / 2,
|
||||||
|
y: y
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTransform()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTransform = () => {
|
||||||
|
if (!visible.value) return
|
||||||
|
|
||||||
|
const { scale, offset } = lgCanvas.ds
|
||||||
|
const canvasRect = lgCanvas.canvas.getBoundingClientRect()
|
||||||
|
|
||||||
|
const screenX =
|
||||||
|
(worldPosition.value.x + offset[0]) * scale + canvasRect.left
|
||||||
|
const screenY = (worldPosition.value.y + offset[1]) * scale + canvasRect.top
|
||||||
|
|
||||||
|
// Update CSS custom properties directly for best performance
|
||||||
|
if (toolboxRef.value) {
|
||||||
|
toolboxRef.value.style.setProperty('--tb-x', `${screenX}px`)
|
||||||
|
toolboxRef.value.style.setProperty('--tb-y', `${screenY}px`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync with canvas transform
|
||||||
|
const { startSync, stopSync } = useCanvasTransformSync(updateTransform, {
|
||||||
|
autoStart: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for selection changes
|
||||||
|
watch(
|
||||||
|
() => canvasStore.getCanvas().state.selectionChanged,
|
||||||
|
(changed) => {
|
||||||
|
if (changed) {
|
||||||
|
updateSelectionBounds()
|
||||||
|
canvasStore.getCanvas().state.selectionChanged = false
|
||||||
|
|
||||||
|
// Start transform sync if we have selection
|
||||||
|
if (visible.value) {
|
||||||
|
startSync()
|
||||||
|
} else {
|
||||||
|
stopSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watch for dragging state
|
||||||
|
watch(
|
||||||
|
() => canvasStore.canvas?.state?.draggingItems,
|
||||||
|
(dragging) => {
|
||||||
|
if (dragging) {
|
||||||
|
// Hide during node dragging
|
||||||
|
visible.value = false
|
||||||
|
} else {
|
||||||
|
// Update after dragging ends
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
updateSelectionBounds()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import './previewAny'
|
|||||||
import './rerouteNode'
|
import './rerouteNode'
|
||||||
import './saveImageExtraOutput'
|
import './saveImageExtraOutput'
|
||||||
import './saveMesh'
|
import './saveMesh'
|
||||||
|
import './selectionBorder'
|
||||||
import './simpleTouchSupport'
|
import './simpleTouchSupport'
|
||||||
import './slotDefaults'
|
import './slotDefaults'
|
||||||
import './uploadAudio'
|
import './uploadAudio'
|
||||||
|
|||||||
70
src/extensions/core/selectionBorder.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { type LGraphCanvas, createBounds } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a dashed border around selected items that maintains constant pixel size
|
||||||
|
* regardless of zoom level, similar to the DOM selection overlay.
|
||||||
|
*/
|
||||||
|
function drawSelectionBorder(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
canvas: LGraphCanvas
|
||||||
|
) {
|
||||||
|
const selectedItems = canvas.selectedItems
|
||||||
|
|
||||||
|
// Only draw if multiple items selected
|
||||||
|
if (selectedItems.size <= 1) return
|
||||||
|
|
||||||
|
// Use the same bounds calculation as the toolbox
|
||||||
|
const bounds = createBounds(selectedItems, 10)
|
||||||
|
if (!bounds) return
|
||||||
|
|
||||||
|
const [x, y, width, height] = bounds
|
||||||
|
|
||||||
|
// Save context state
|
||||||
|
ctx.save()
|
||||||
|
|
||||||
|
// Set up dashed line style that doesn't scale with zoom
|
||||||
|
const borderWidth = 2 / canvas.ds.scale // Constant 2px regardless of zoom
|
||||||
|
ctx.lineWidth = borderWidth
|
||||||
|
ctx.strokeStyle =
|
||||||
|
getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue('--border-color')
|
||||||
|
.trim() || '#ffffff66'
|
||||||
|
|
||||||
|
// Create dash pattern that maintains visual size
|
||||||
|
const dashSize = 5 / canvas.ds.scale
|
||||||
|
ctx.setLineDash([dashSize, dashSize])
|
||||||
|
|
||||||
|
// Draw the border using the bounds directly
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.roundRect(x, y, width, height, 8 / canvas.ds.scale)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Restore context
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension that adds a dashed selection border for multiple selected nodes
|
||||||
|
*/
|
||||||
|
const ext = {
|
||||||
|
name: 'Comfy.SelectionBorder',
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// Hook into the canvas drawing
|
||||||
|
const originalDrawForeground = app.canvas.onDrawForeground
|
||||||
|
|
||||||
|
app.canvas.onDrawForeground = function (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
visibleArea: any
|
||||||
|
) {
|
||||||
|
// Call original if it exists
|
||||||
|
originalDrawForeground?.call(this, ctx, visibleArea)
|
||||||
|
|
||||||
|
// Draw our selection border
|
||||||
|
drawSelectionBorder(ctx, app.canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension(ext)
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { InjectionKey, Ref } from 'vue'
|
|
||||||
|
|
||||||
export interface SelectionOverlayState {
|
|
||||||
visible: Readonly<Ref<boolean>>
|
|
||||||
updateCount: Readonly<Ref<number>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SelectionOverlayInjectionKey: InjectionKey<SelectionOverlayState> =
|
|
||||||
Symbol('selectionOverlayState')
|
|
||||||