Move scrollable behavior into LayoutRow/LayoutCol

This commit is contained in:
Keavon Chambers 2022-01-22 23:02:59 -08:00
parent 3bf5023ef8
commit 45d75bd13f
11 changed files with 153 additions and 147 deletions

View File

@ -1,5 +1,6 @@
<template> <template>
<MainWindow /> <MainWindow />
<div class="unsupported-modal-backdrop" v-if="showUnsupportedModal"> <div class="unsupported-modal-backdrop" v-if="showUnsupportedModal">
<div class="unsupported-modal"> <div class="unsupported-modal">
<h2>Your browser currently doesn't support Graphite</h2> <h2>Your browser currently doesn't support Graphite</h2>
@ -91,72 +92,74 @@ img {
display: block; display: block;
} }
.scrollable, .layout-row,
.scrollable-x, .layout-col {
.scrollable-y { .scrollable-x,
// Standard .scrollable-y {
scrollbar-width: thin; // Standard
scrollbar-width: 6px; scrollbar-width: thin;
scrollbar-gutter: 6px; scrollbar-width: 6px;
scrollbar-color: var(--color-5-dullgray) transparent; scrollbar-gutter: 6px;
scrollbar-color: var(--color-5-dullgray) transparent;
&:not(:hover) { &:not(:hover) {
scrollbar-width: none; scrollbar-width: none;
} }
// WebKit // WebKit
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: calc(2px + 6px + 2px); width: calc(2px + 6px + 2px);
height: calc(2px + 6px + 2px); height: calc(2px + 6px + 2px);
} }
&:not(:hover)::-webkit-scrollbar { &:not(:hover)::-webkit-scrollbar {
width: 0; width: 0;
height: 0; height: 0;
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
box-shadow: inset 0 0 0 1px var(--color-5-dullgray); box-shadow: inset 0 0 0 1px var(--color-5-dullgray);
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 10px; border-radius: 10px;
&:hover { &:hover {
box-shadow: inset 0 0 0 1px var(--color-6-lowergray); box-shadow: inset 0 0 0 1px var(--color-6-lowergray);
}
}
&::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: var(--color-5-dullgray);
border: 2px solid transparent;
border-radius: 10px;
margin: 2px;
&:hover {
background-color: var(--color-6-lowergray);
}
} }
} }
&::-webkit-scrollbar-thumb { .scrollable-x.scrollable-y {
background-clip: padding-box; // Standard
background-color: var(--color-5-dullgray); overflow: auto;
border: 2px solid transparent; // WebKit
border-radius: 10px; overflow: overlay;
margin: 2px;
&:hover {
background-color: var(--color-6-lowergray);
}
} }
}
.scrollable { .scrollable-x:not(.scrollable-y) {
// Standard // Standard
overflow: auto; overflow-x: auto;
// WebKit // WebKit
overflow: overlay; overflow-x: overlay;
} }
.scrollable-x { .scrollable-y:not(.scrollable-x) {
// Standard // Standard
overflow-x: auto; overflow-y: auto;
// WebKit // WebKit
overflow-x: overlay; overflow-y: overlay;
} }
.scrollable-y {
// Standard
overflow-y: auto;
// WebKit
overflow-y: overlay;
} }
// For placeholder messages (remove eventually) // For placeholder messages (remove eventually)

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="['layout-col']"> <div class="layout-col" :class="{ 'scrollable-x': scrollableX, 'scrollable-y': scrollableY }">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@ -19,7 +19,12 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent, PropType } from "vue";
export default defineComponent({}); export default defineComponent({
props: {
scrollableX: { type: Boolean as PropType<boolean>, default: false },
scrollableY: { type: Boolean as PropType<boolean>, default: false },
},
});
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="['layout-row']"> <div class="layout-row" :class="{ 'scrollable-x': scrollableX, 'scrollable-y': scrollableY }">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@ -19,7 +19,12 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent, PropType } from "vue";
export default defineComponent({}); export default defineComponent({
props: {
scrollableX: { type: Boolean as PropType<boolean>, default: false },
scrollableY: { type: Boolean as PropType<boolean>, default: false },
},
});
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<LayoutCol :class="'document'"> <LayoutCol class="document">
<LayoutRow :class="'options-bar scrollable-x'"> <LayoutRow class="options-bar" :scrollableX="true">
<div class="left side"> <div class="left side">
<DropdownInput :menuEntries="documentModeEntries" v-model:selectedIndex="documentModeSelectionIndex" :drawIcon="true" /> <DropdownInput :menuEntries="documentModeEntries" v-model:selectedIndex="documentModeSelectionIndex" :drawIcon="true" />
@ -66,9 +66,9 @@
/> />
</div> </div>
</LayoutRow> </LayoutRow>
<LayoutRow :class="'shelf-and-viewport'"> <LayoutRow class="shelf-and-viewport">
<LayoutCol :class="'shelf'"> <LayoutCol class="shelf">
<div class="tools scrollable-y"> <LayoutCol class="tools" :scrollableY="true">
<ShelfItemInput icon="LayoutSelectTool" title="Select Tool (V)" :active="activeTool === 'Select'" :action="() => selectTool('Select')" /> <ShelfItemInput icon="LayoutSelectTool" title="Select Tool (V)" :active="activeTool === 'Select'" :action="() => selectTool('Select')" />
<ShelfItemInput icon="LayoutCropTool" title="Crop Tool" :active="activeTool === 'Crop'" :action="() => (dialog.comingSoon(289), false) && selectTool('Crop')" /> <ShelfItemInput icon="LayoutCropTool" title="Crop Tool" :active="activeTool === 'Crop'" :action="() => (dialog.comingSoon(289), false) && selectTool('Crop')" />
<ShelfItemInput icon="LayoutNavigateTool" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" :action="() => selectTool('Navigate')" /> <ShelfItemInput icon="LayoutNavigateTool" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" :action="() => selectTool('Navigate')" />
@ -104,50 +104,52 @@
<ShelfItemInput icon="VectorRectangleTool" title="Rectangle Tool (M)" :active="activeTool === 'Rectangle'" :action="() => selectTool('Rectangle')" /> <ShelfItemInput icon="VectorRectangleTool" title="Rectangle Tool (M)" :active="activeTool === 'Rectangle'" :action="() => selectTool('Rectangle')" />
<ShelfItemInput icon="VectorEllipseTool" title="Ellipse Tool (E)" :active="activeTool === 'Ellipse'" :action="() => selectTool('Ellipse')" /> <ShelfItemInput icon="VectorEllipseTool" title="Ellipse Tool (E)" :active="activeTool === 'Ellipse'" :action="() => selectTool('Ellipse')" />
<ShelfItemInput icon="VectorShapeTool" title="Shape Tool (Y)" :active="activeTool === 'Shape'" :action="() => selectTool('Shape')" /> <ShelfItemInput icon="VectorShapeTool" title="Shape Tool (Y)" :active="activeTool === 'Shape'" :action="() => selectTool('Shape')" />
</div> </LayoutCol>
<div class="spacer"></div> <div class="spacer"></div>
<div class="working-colors">
<LayoutCol class="working-colors">
<SwatchPairInput /> <SwatchPairInput />
<div class="swap-and-reset"> <div class="swap-and-reset">
<IconButton :action="swapWorkingColors" :icon="'Swap'" title="Swap (Shift+X)" :size="16" /> <IconButton :action="swapWorkingColors" :icon="'Swap'" title="Swap (Shift+X)" :size="16" />
<IconButton :action="resetWorkingColors" :icon="'ResetColors'" title="Reset (Ctrl+Shift+X)" :size="16" /> <IconButton :action="resetWorkingColors" :icon="'ResetColors'" title="Reset (Ctrl+Shift+X)" :size="16" />
</div> </div>
</div> </LayoutCol>
</LayoutCol> </LayoutCol>
<LayoutCol :class="'viewport'"> <LayoutCol class="viewport">
<LayoutRow :class="'bar-area'"> <LayoutRow class="bar-area">
<CanvasRuler :origin="rulerOrigin.x" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Horizontal'" :class="'top-ruler'" /> <CanvasRuler :origin="rulerOrigin.x" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Horizontal'" class="top-ruler" />
</LayoutRow> </LayoutRow>
<LayoutRow :class="'canvas-area'"> <LayoutRow class="canvas-area">
<LayoutCol :class="'bar-area'"> <LayoutCol class="bar-area">
<CanvasRuler :origin="rulerOrigin.y" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Vertical'" /> <CanvasRuler :origin="rulerOrigin.y" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Vertical'" />
</LayoutCol> </LayoutCol>
<LayoutCol :class="'canvas-area'"> <LayoutCol class="canvas-area">
<div class="canvas" ref="canvas" :style="{ cursor: canvasCursor }" @pointerdown="(e: PointerEvent) => canvasPointerDown(e)"> <div class="canvas" ref="canvas" :style="{ cursor: canvasCursor }" @pointerdown="(e: PointerEvent) => canvasPointerDown(e)">
<svg class="artboards" v-html="artboardSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg> <svg class="artboards" v-html="artboardSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
<svg class="artwork" v-html="artworkSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg> <svg class="artwork" v-html="artworkSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
<svg class="overlays" v-html="overlaysSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg> <svg class="overlays" v-html="overlaysSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
</div> </div>
</LayoutCol> </LayoutCol>
<LayoutCol :class="'bar-area'"> <LayoutCol class="bar-area">
<PersistentScrollbar <PersistentScrollbar
:direction="'Vertical'" :direction="'Vertical'"
:handlePosition="scrollbarPos.y" :handlePosition="scrollbarPos.y"
@update:handlePosition="(newValue: number) => translateCanvasY(newValue)" @update:handlePosition="(newValue: number) => translateCanvasY(newValue)"
v-model:handleLength="scrollbarSize.y" v-model:handleLength="scrollbarSize.y"
@pressTrack="(delta: number) => pageY(delta)" @pressTrack="(delta: number) => pageY(delta)"
:class="'right-scrollbar'" class="right-scrollbar"
/> />
</LayoutCol> </LayoutCol>
</LayoutRow> </LayoutRow>
<LayoutRow :class="'bar-area'"> <LayoutRow class="bar-area">
<PersistentScrollbar <PersistentScrollbar
:direction="'Horizontal'" :direction="'Horizontal'"
:handlePosition="scrollbarPos.x" :handlePosition="scrollbarPos.x"
@update:handlePosition="(newValue: number) => translateCanvasX(newValue)" @update:handlePosition="(newValue: number) => translateCanvasX(newValue)"
v-model:handleLength="scrollbarSize.x" v-model:handleLength="scrollbarSize.x"
@pressTrack="(delta: number) => pageX(delta)" @pressTrack="(delta: number) => pageX(delta)"
:class="'bottom-scrollbar'" class="bottom-scrollbar"
/> />
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>
@ -191,9 +193,13 @@
min-height: 8px; min-height: 8px;
} }
.working-colors .swap-and-reset { .working-colors {
flex: 0 0 auto; flex: 0 0 auto;
display: flex;
.swap-and-reset {
flex: 0 0 auto;
display: flex;
}
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<LayoutCol :class="'layer-tree-panel'"> <LayoutCol class="layer-tree-panel">
<LayoutRow :class="'options-bar'"> <LayoutRow class="options-bar">
<DropdownInput <DropdownInput
v-model:selectedIndex="blendModeSelectedIndex" v-model:selectedIndex="blendModeSelectedIndex"
@update:selectedIndex="(newSelectedIndex: number) => setLayerBlendMode(newSelectedIndex)" @update:selectedIndex="(newSelectedIndex: number) => setLayerBlendMode(newSelectedIndex)"
@ -28,8 +28,8 @@
<p>The contents of this popover menu are coming soon</p> <p>The contents of this popover menu are coming soon</p>
</PopoverButton> </PopoverButton>
</LayoutRow> </LayoutRow>
<LayoutRow :class="'layer-tree scrollable-y'"> <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="updateInsertLine($event)" @dragend="drop()">
<div class="layer-row" v-for="({ entry: layer }, index) in layers" :key="String(layer.path.slice(-1))"> <div class="layer-row" v-for="({ entry: layer }, index) in layers" :key="String(layer.path.slice(-1))">
<div class="visibility"> <div class="visibility">
<IconButton <IconButton

View File

@ -2,14 +2,14 @@
<div class="dialog-modal"> <div class="dialog-modal">
<FloatingMenu :type="'Dialog'" :direction="'Center'"> <FloatingMenu :type="'Dialog'" :direction="'Center'">
<LayoutRow> <LayoutRow>
<LayoutCol :class="'icon-column'"> <LayoutCol class="icon-column">
<!-- `dialog.state.icon` class exists to provide special sizing in CSS to specific icons --> <!-- `dialog.state.icon` class exists to provide special sizing in CSS to specific icons -->
<IconLabel :icon="dialog.state.icon" :class="dialog.state.icon.toLowerCase()" /> <IconLabel :icon="dialog.state.icon" :class="dialog.state.icon.toLowerCase()" />
</LayoutCol> </LayoutCol>
<LayoutCol :class="'main-column'"> <LayoutCol class="main-column">
<TextLabel :bold="true" :class="'heading'">{{ dialog.state.heading }}</TextLabel> <TextLabel :bold="true" class="heading">{{ dialog.state.heading }}</TextLabel>
<TextLabel :class="'details'">{{ dialog.state.details }}</TextLabel> <TextLabel class="details">{{ dialog.state.details }}</TextLabel>
<LayoutRow :class="'buttons-row'" v-if="dialog.state.buttons.length > 0"> <LayoutRow class="buttons-row" v-if="dialog.state.buttons.length > 0">
<TextButton v-for="(button, index) in dialog.state.buttons" :key="index" :title="button.tooltip" :action="() => button.callback && button.callback()" v-bind="button.props" /> <TextButton v-for="(button, index) in dialog.state.buttons" :key="index" :title="button.tooltip" :action="() => button.callback && button.callback()" v-bind="button.props" />
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>

View File

@ -2,9 +2,9 @@
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === 'Dialog'" ref="floatingMenu"> <div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === 'Dialog'" ref="floatingMenu">
<div class="tail" v-if="type === 'Popover'"></div> <div class="tail" v-if="type === 'Popover'"></div>
<div class="floating-menu-container" ref="floatingMenuContainer"> <div class="floating-menu-container" ref="floatingMenuContainer">
<div class="floating-menu-content" :class="{ 'scrollable-y': scrollable }" ref="floatingMenuContent" :style="floatingMenuContentStyle"> <LayoutCol class="floating-menu-content" :scrollableY="scrollableY" ref="floatingMenuContent" :style="floatingMenuContentStyle">
<slot></slot> <slot></slot>
</div> </LayoutCol>
</div> </div>
</div> </div>
</template> </template>
@ -179,6 +179,8 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, StyleValue } from "vue"; import { defineComponent, PropType, StyleValue } from "vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center"; export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
export type MenuType = "Popover" | "Dropdown" | "Dialog"; export type MenuType = "Popover" | "Dropdown" | "Dialog";
@ -190,14 +192,13 @@ export default defineComponent({
type: { type: String as PropType<MenuType>, required: true }, type: { type: String as PropType<MenuType>, required: true },
windowEdgeMargin: { type: Number as PropType<number>, default: 6 }, windowEdgeMargin: { type: Number as PropType<number>, default: 6 },
minWidth: { type: Number as PropType<number>, default: 0 }, minWidth: { type: Number as PropType<number>, default: 0 },
scrollable: { type: Boolean as PropType<boolean>, default: false }, scrollableY: { type: Boolean as PropType<boolean>, default: false },
}, },
data() { data() {
const containerResizeObserver = new ResizeObserver((entries) => { const containerResizeObserver = new ResizeObserver((entries) => {
const content = entries[0].target.querySelector(".floating-menu-content") as HTMLElement; const content = entries[0].target.querySelector(".floating-menu-content") as HTMLElement;
content.style.minWidth = `${entries[0].contentRect.width}px`; content.style.minWidth = `${entries[0].contentRect.width}px`;
}); });
return { return {
open: false, open: false,
pointerStillDown: false, pointerStillDown: false,
@ -206,9 +207,11 @@ export default defineComponent({
}, },
updated() { updated() {
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement; const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement; const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
const floatingMenuContent = floatingMenuContentComponent && (floatingMenuContentComponent.$el as HTMLElement);
const workspace = document.querySelector(".workspace-row"); const workspace = document.querySelector(".workspace-row");
if (!floatingMenuContainer || !floatingMenuContent || !workspace) return;
if (!floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !workspace) return;
const workspaceBounds = workspace.getBoundingClientRect(); const workspaceBounds = workspace.getBoundingClientRect();
const floatingMenuBounds = floatingMenuContent.getBoundingClientRect(); const floatingMenuBounds = floatingMenuContent.getBoundingClientRect();
@ -224,13 +227,11 @@ export default defineComponent({
floatingMenuContent.style.left = `${this.windowEdgeMargin}px`; floatingMenuContent.style.left = `${this.windowEdgeMargin}px`;
if (workspaceBounds.left + floatingMenuContainer.getBoundingClientRect().left === 12) zeroedBorderDirection2 = "Left"; if (workspaceBounds.left + floatingMenuContainer.getBoundingClientRect().left === 12) zeroedBorderDirection2 = "Left";
} }
if (floatingMenuBounds.right + this.windowEdgeMargin >= workspaceBounds.right) { if (floatingMenuBounds.right + this.windowEdgeMargin >= workspaceBounds.right) {
floatingMenuContent.style.right = `${this.windowEdgeMargin}px`; floatingMenuContent.style.right = `${this.windowEdgeMargin}px`;
if (workspaceBounds.right - floatingMenuContainer.getBoundingClientRect().right === 12) zeroedBorderDirection2 = "Right"; if (workspaceBounds.right - floatingMenuContainer.getBoundingClientRect().right === 12) zeroedBorderDirection2 = "Right";
} }
} }
if (this.direction === "Left" || this.direction === "Right") { if (this.direction === "Left" || this.direction === "Right") {
zeroedBorderDirection2 = this.direction === "Left" ? "Right" : "Left"; zeroedBorderDirection2 = this.direction === "Left" ? "Right" : "Left";
@ -238,7 +239,6 @@ export default defineComponent({
floatingMenuContent.style.top = `${this.windowEdgeMargin}px`; floatingMenuContent.style.top = `${this.windowEdgeMargin}px`;
if (workspaceBounds.top + floatingMenuContainer.getBoundingClientRect().top === 12) zeroedBorderDirection1 = "Top"; if (workspaceBounds.top + floatingMenuContainer.getBoundingClientRect().top === 12) zeroedBorderDirection1 = "Top";
} }
if (floatingMenuBounds.bottom + this.windowEdgeMargin >= workspaceBounds.bottom) { if (floatingMenuBounds.bottom + this.windowEdgeMargin >= workspaceBounds.bottom) {
floatingMenuContent.style.bottom = `${this.windowEdgeMargin}px`; floatingMenuContent.style.bottom = `${this.windowEdgeMargin}px`;
if (workspaceBounds.bottom - floatingMenuContainer.getBoundingClientRect().bottom === 12) zeroedBorderDirection1 = "Bottom"; if (workspaceBounds.bottom - floatingMenuContainer.getBoundingClientRect().bottom === 12) zeroedBorderDirection1 = "Bottom";
@ -277,23 +277,21 @@ export default defineComponent({
}, },
getWidth(callback: (width: number) => void) { getWidth(callback: (width: number) => void) {
this.$nextTick(() => { this.$nextTick(() => {
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement; const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement;
const width = floatingMenuContent.clientWidth; const width = floatingMenuContent.clientWidth;
callback(width); callback(width);
}); });
}, },
disableMinWidth(callback: (minWidth: string) => void) { disableMinWidth(callback: (minWidth: string) => void) {
this.$nextTick(() => { this.$nextTick(() => {
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement; const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement;
const initialMinWidth = floatingMenuContent.style.minWidth; const initialMinWidth = floatingMenuContent.style.minWidth;
floatingMenuContent.style.minWidth = "0"; floatingMenuContent.style.minWidth = "0";
callback(initialMinWidth); callback(initialMinWidth);
}); });
}, },
enableMinWidth(minWidth: string) { enableMinWidth(minWidth: string) {
const floatingMenuContent = this.$refs.floatingMenuContent as HTMLElement; const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement;
floatingMenuContent.style.minWidth = minWidth; floatingMenuContent.style.minWidth = minWidth;
}, },
pointerMoveHandler(e: PointerEvent) { pointerMoveHandler(e: PointerEvent) {
@ -303,22 +301,18 @@ export default defineComponent({
// TODO: Simplify the following expression when optional chaining is supported by the build system // TODO: Simplify the following expression when optional chaining is supported by the build system
const pointerOverOwnFloatingMenuSpawner = const pointerOverOwnFloatingMenuSpawner =
pointerOverFloatingMenuSpawner && pointerOverFloatingMenuSpawner.parentElement && pointerOverFloatingMenuSpawner.parentElement.contains(this.$refs.floatingMenu as HTMLElement); pointerOverFloatingMenuSpawner && pointerOverFloatingMenuSpawner.parentElement && pointerOverFloatingMenuSpawner.parentElement.contains(this.$refs.floatingMenu as HTMLElement);
// Swap this open floating menu with the one created by the floating menu spawner being hovered over // Swap this open floating menu with the one created by the floating menu spawner being hovered over
if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) { if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) {
this.setClosed(); this.setClosed();
pointerOverFloatingMenuSpawner.click(); pointerOverFloatingMenuSpawner.click();
} }
// Close the floating menu if the pointer has strayed far enough from its bounds // Close the floating menu if the pointer has strayed far enough from its bounds
if (this.isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE) && !pointerOverOwnFloatingMenuSpawner && !pointerOverFloatingMenuKeepOpen) { if (this.isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE) && !pointerOverOwnFloatingMenuSpawner && !pointerOverFloatingMenuKeepOpen) {
// TODO: Extend this rectangle bounds check to all `data-hover-menu-keep-open` element bounds up the DOM tree since currently // TODO: Extend this rectangle bounds check to all `data-hover-menu-keep-open` element bounds up the DOM tree since currently
// submenus disappear with zero stray distance if the cursor is further than the stray distance from only the top-level menu // submenus disappear with zero stray distance if the cursor is further than the stray distance from only the top-level menu
this.setClosed(); this.setClosed();
} }
const eventIncludesLmb = Boolean(e.buttons & 1); const eventIncludesLmb = Boolean(e.buttons & 1);
// Clean up any messes from lost pointerup events // Clean up any messes from lost pointerup events
if (!this.open && !eventIncludesLmb) { if (!this.open && !eventIncludesLmb) {
this.pointerStillDown = false; this.pointerStillDown = false;
@ -329,7 +323,6 @@ export default defineComponent({
// Close the floating menu if the pointer clicked outside the floating menu (but within stray distance) // Close the floating menu if the pointer clicked outside the floating menu (but within stray distance)
if (this.isPointerEventOutsideFloatingMenu(e)) { if (this.isPointerEventOutsideFloatingMenu(e)) {
this.setClosed(); this.setClosed();
// Track if the left pointer button is now down so its later click event can be canceled // Track if the left pointer button is now down so its later click event can be canceled
const eventIsForLmb = e.button === 0; const eventIsForLmb = e.button === 0;
if (eventIsForLmb) this.pointerStillDown = true; if (eventIsForLmb) this.pointerStillDown = true;
@ -337,12 +330,10 @@ export default defineComponent({
}, },
pointerUpHandler(e: PointerEvent) { pointerUpHandler(e: PointerEvent) {
const eventIsForLmb = e.button === 0; const eventIsForLmb = e.button === 0;
if (this.pointerStillDown && eventIsForLmb) { if (this.pointerStillDown && eventIsForLmb) {
// Clean up self // Clean up self
this.pointerStillDown = false; this.pointerStillDown = false;
window.removeEventListener("pointerup", this.pointerUpHandler); window.removeEventListener("pointerup", this.pointerUpHandler);
// Prevent the click event from firing, which would normally occur right after this pointerup event // Prevent the click event from firing, which would normally occur right after this pointerup event
window.addEventListener("click", this.clickHandlerCapture, true); window.addEventListener("click", this.clickHandlerCapture, true);
} }
@ -350,7 +341,6 @@ export default defineComponent({
clickHandlerCapture(e: MouseEvent) { clickHandlerCapture(e: MouseEvent) {
// Stop the click event from reopening this floating menu if the click event targets the floating menu's button // Stop the click event from reopening this floating menu if the click event targets the floating menu's button
e.stopPropagation(); e.stopPropagation();
// Clean up self // Clean up self
window.removeEventListener("click", this.clickHandlerCapture, true); window.removeEventListener("click", this.clickHandlerCapture, true);
}, },
@ -374,13 +364,10 @@ export default defineComponent({
if (newState && !oldState) { if (newState && !oldState) {
// Close floating menu if pointer strays far enough away // Close floating menu if pointer strays far enough away
window.addEventListener("pointermove", this.pointerMoveHandler); window.addEventListener("pointermove", this.pointerMoveHandler);
// Close floating menu if pointer is outside (but within stray distance) // Close floating menu if pointer is outside (but within stray distance)
window.addEventListener("pointerdown", this.pointerDownHandler); window.addEventListener("pointerdown", this.pointerDownHandler);
// Cancel the subsequent click event to prevent the floating menu from reopening if the floating menu's button is the click event target // Cancel the subsequent click event to prevent the floating menu from reopening if the floating menu's button is the click event target
window.addEventListener("pointerup", this.pointerUpHandler); window.addEventListener("pointerup", this.pointerUpHandler);
// Floating menu min-width resize observer // Floating menu min-width resize observer
this.$nextTick(() => { this.$nextTick(() => {
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement; const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
@ -390,12 +377,10 @@ export default defineComponent({
} }
}); });
} }
// Switching from open to closed // Switching from open to closed
if (!newState && oldState) { if (!newState && oldState) {
window.removeEventListener("pointermove", this.pointerMoveHandler); window.removeEventListener("pointermove", this.pointerMoveHandler);
window.removeEventListener("pointerdown", this.pointerDownHandler); window.removeEventListener("pointerdown", this.pointerDownHandler);
this.containerResizeObserver.disconnect(); this.containerResizeObserver.disconnect();
} }
}, },
@ -407,5 +392,6 @@ export default defineComponent({
}; };
}, },
}, },
components: { LayoutCol },
}); });
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<FloatingMenu :class="'menu-list'" :direction="direction" :type="'Dropdown'" ref="floatingMenu" :windowEdgeMargin="0" :scrollable="scrollable" data-hover-menu-keep-open> <FloatingMenu class="menu-list" :direction="direction" :type="'Dropdown'" ref="floatingMenu" :windowEdgeMargin="0" :scrollableY="scrollableY" data-hover-menu-keep-open>
<template v-for="(section, sectionIndex) in menuEntries" :key="sectionIndex"> <template v-for="(section, sectionIndex) in menuEntries" :key="sectionIndex">
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" /> <Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
<div <div
@ -12,8 +12,8 @@
@pointerleave="handleEntryPointerLeave(entry)" @pointerleave="handleEntryPointerLeave(entry)"
:data-hover-menu-spawner-extend="entry.children && []" :data-hover-menu-spawner-extend="entry.children && []"
> >
<CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" :class="'entry-checkbox'" /> <CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" class="entry-checkbox" />
<IconLabel v-else-if="entry.icon && drawIcon" :icon="entry.icon" :class="'entry-icon'" /> <IconLabel v-else-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
<div v-else-if="drawIcon" class="no-icon"></div> <div v-else-if="drawIcon" class="no-icon"></div>
<span class="entry-label">{{ entry.label }}</span> <span class="entry-label">{{ entry.label }}</span>
@ -28,7 +28,7 @@
v-if="entry.children" v-if="entry.children"
:direction="'TopRight'" :direction="'TopRight'"
:menuEntries="entry.children" :menuEntries="entry.children"
v-bind="{ defaultAction, minWidth, drawIcon, scrollable }" v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
:ref="(ref: any) => setEntryRefs(entry, ref)" :ref="(ref: any) => setEntryRefs(entry, ref)"
/> />
</div> </div>
@ -168,7 +168,7 @@ const MenuList = defineComponent({
defaultAction: { type: Function as PropType<() => void>, required: false }, defaultAction: { type: Function as PropType<() => void>, required: false },
minWidth: { type: Number as PropType<number>, default: 0 }, minWidth: { type: Number as PropType<number>, default: 0 },
drawIcon: { type: Boolean as PropType<boolean>, default: false }, drawIcon: { type: Boolean as PropType<boolean>, default: false },
scrollable: { type: Boolean as PropType<boolean>, default: false }, scrollableY: { type: Boolean as PropType<boolean>, default: false },
}, },
methods: { methods: {
setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu) { setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu) {

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="dropdown-input"> <div class="dropdown-input">
<div class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => clickDropdownBox()" data-hover-menu-spawner> <div class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => clickDropdownBox()" data-hover-menu-spawner>
<IconLabel :class="'dropdown-icon'" :icon="activeEntry.icon" v-if="activeEntry.icon" /> <IconLabel class="dropdown-icon" :icon="activeEntry.icon" v-if="activeEntry.icon" />
<span>{{ activeEntry.label }}</span> <span>{{ activeEntry.label }}</span>
<IconLabel :class="'dropdown-arrow'" :icon="'DropdownArrow'" /> <IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
</div> </div>
<MenuList <MenuList
v-model:activeEntry="activeEntry" v-model:activeEntry="activeEntry"
@ -12,7 +12,7 @@
:menuEntries="menuEntries" :menuEntries="menuEntries"
:direction="'Bottom'" :direction="'Bottom'"
:drawIcon="drawIcon" :drawIcon="drawIcon"
:scrollable="true" :scrollableY="true"
ref="menuList" ref="menuList"
/> />
</div> </div>

View File

@ -1,12 +1,12 @@
<template> <template>
<LayoutCol class="main-window"> <LayoutCol class="main-window">
<LayoutRow :class="'title-bar-row'"> <LayoutRow class="title-bar-row">
<TitleBar :platform="platform" :maximized="maximized" /> <TitleBar :platform="platform" :maximized="maximized" />
</LayoutRow> </LayoutRow>
<LayoutRow :class="'workspace-row'"> <LayoutRow class="workspace-row">
<Workspace /> <Workspace />
</LayoutRow> </LayoutRow>
<LayoutRow :class="'status-bar-row'"> <LayoutRow class="status-bar-row">
<StatusBar /> <StatusBar />
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="panel"> <LayoutCol class="panel">
<div class="tab-bar" :class="{ 'min-widths': tabMinWidths }"> <LayoutRow class="tab-bar" :class="{ 'min-widths': tabMinWidths }">
<div class="tab-group scrollable-x"> <LayoutRow class="tab-group" :scrollableX="true">
<div <div
class="tab" class="tab"
:class="{ active: tabIndex === tabActiveIndex }" :class="{ active: tabIndex === tabActiveIndex }"
@ -13,41 +13,35 @@
<span>{{ tabLabel }}</span> <span>{{ tabLabel }}</span>
<IconButton :action="(e) => (e && e.stopPropagation(), closeAction && closeAction(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" /> <IconButton :action="(e) => (e && e.stopPropagation(), closeAction && closeAction(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
</div> </div>
</div> </LayoutRow>
<PopoverButton :icon="'VerticalEllipsis'"> <PopoverButton :icon="'VerticalEllipsis'">
<h3>Panel Options</h3> <h3>Panel Options</h3>
<p>The contents of this popover menu are coming soon</p> <p>The contents of this popover menu are coming soon</p>
</PopoverButton> </PopoverButton>
</div> </LayoutRow>
<div class="panel-body"> <LayoutCol class="panel-body">
<component :is="panelType" /> <component :is="panelType" />
</div> </LayoutCol>
</div> </LayoutCol>
</template> </template>
<style lang="scss"> <style lang="scss">
.panel { .panel {
background: var(--color-1-nearblack); background: var(--color-1-nearblack);
border-radius: 8px; border-radius: 8px;
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden; overflow: hidden;
.tab-bar { .tab-bar {
height: 28px; height: 28px;
display: flex; min-height: auto;
flex-direction: row;
&.min-widths .tab-group .tab { &.min-widths .tab-group .tab {
min-width: 124px; min-width: 120px;
max-width: 360px; max-width: 360px;
} }
.tab-group { .tab-group {
flex: 1 1 100%; flex: 1 1 100%;
display: flex;
flex-direction: row;
position: relative; position: relative;
// This always hangs out at the end of the last tab, providing 16px (15px plus the 1px reserved for the separator line) to the right of the tabs. // This always hangs out at the end of the last tab, providing 16px (15px plus the 1px reserved for the separator line) to the right of the tabs.
@ -154,6 +148,8 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import Document from "@/components/panels/Document.vue"; import Document from "@/components/panels/Document.vue";
import LayerTree from "@/components/panels/LayerTree.vue"; import LayerTree from "@/components/panels/LayerTree.vue";
import Minimap from "@/components/panels/Minimap.vue"; import Minimap from "@/components/panels/Minimap.vue";
@ -161,7 +157,7 @@ import Properties from "@/components/panels/Properties.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue"; import IconButton from "@/components/widgets/buttons/IconButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue"; import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
const components = { const panelComponents = {
Document, Document,
Properties, Properties,
LayerTree, LayerTree,
@ -169,18 +165,23 @@ const components = {
IconButton, IconButton,
PopoverButton, PopoverButton,
}; };
type PanelTypes = keyof typeof panelComponents;
export default defineComponent({ export default defineComponent({
inject: ["documents"], inject: ["documents"],
components,
props: { props: {
tabMinWidths: { type: Boolean as PropType<boolean>, default: false }, tabMinWidths: { type: Boolean as PropType<boolean>, default: false },
tabCloseButtons: { type: Boolean as PropType<boolean>, default: false }, tabCloseButtons: { type: Boolean as PropType<boolean>, default: false },
tabLabels: { type: Array as PropType<string[]>, required: true }, tabLabels: { type: Array as PropType<string[]>, required: true },
tabActiveIndex: { type: Number as PropType<number>, required: true }, tabActiveIndex: { type: Number as PropType<number>, required: true },
panelType: { type: String as PropType<keyof typeof components>, required: true }, panelType: { type: String as PropType<PanelTypes>, required: true },
clickAction: { type: Function as PropType<(index: number) => void>, required: false }, clickAction: { type: Function as PropType<(index: number) => void>, required: false },
closeAction: { type: Function as PropType<(index: number) => void>, required: false }, closeAction: { type: Function as PropType<(index: number) => void>, required: false },
}, },
components: {
LayoutCol,
LayoutRow,
...panelComponents,
},
}); });
</script> </script>