parent
3d646d2bc3
commit
d1bf68320e
|
|
@ -28,6 +28,7 @@ module.exports = {
|
|||
"eol-last": ["error", "always"],
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-param-reassign": ["error", { props: false }],
|
||||
"max-len": ["error", { code: 200, tabWidth: 4 }],
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M7,0L2,5v11h12V0H7z M13,15H3V6h5V1h5V15z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 123 B |
|
|
@ -27,6 +27,7 @@
|
|||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color() and https://caniuse.com/css-color-function
|
||||
// E6 = 90% alpha
|
||||
--floating-menu-opacity-color-2-mildblack: #222222e6;
|
||||
--floating-menu-shadow: rgba(0, 0, 0, 50%);
|
||||
}
|
||||
|
||||
html,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="separator" :class="[direction.toLowerCase(), type.toLowerCase()]">
|
||||
<div v-if="type === SeparatorType.Section"></div>
|
||||
<div v-if="[SeparatorType.Section, SeparatorType.List].includes(type)"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -15,9 +15,9 @@
|
|||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&.section {
|
||||
&.section,
|
||||
&.list {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
|
||||
div {
|
||||
height: 1px;
|
||||
|
|
@ -26,6 +26,14 @@
|
|||
background: var(--color-7-middlegray);
|
||||
}
|
||||
}
|
||||
|
||||
&.section {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
&.list {
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
|
|
@ -37,9 +45,9 @@
|
|||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&.section {
|
||||
&.section,
|
||||
&.list {
|
||||
height: 100%;
|
||||
margin: 0 8px;
|
||||
|
||||
div {
|
||||
height: calc(100% - 8px);
|
||||
|
|
@ -48,6 +56,14 @@
|
|||
background: var(--color-7-middlegray);
|
||||
}
|
||||
}
|
||||
|
||||
&.section {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
&.list {
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -64,6 +80,7 @@ export enum SeparatorType {
|
|||
"Related" = "Related",
|
||||
"Unrelated" = "Unrelated",
|
||||
"Section" = "Section",
|
||||
"List" = "List",
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
<div class="working-colors">
|
||||
<SwatchPairInput />
|
||||
<div class="swap-and-reset">
|
||||
<IconButton :icon="'SwapButton'" :size="16" />
|
||||
<IconButton :icon="'ResetColorsButton'" :size="16" />
|
||||
<IconButton @click="swapColors" :icon="'SwapButton'" :size="16" />
|
||||
<IconButton @click="resetColors" :icon="'ResetColorsButton'" :size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -21,7 +21,19 @@ import { defineComponent } from "vue";
|
|||
import SwatchPairInput from "./inputs/SwatchPairInput.vue";
|
||||
import IconButton from "./buttons/IconButton.vue";
|
||||
|
||||
const wasm = import("../../../wasm/pkg");
|
||||
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
async swapColors() {
|
||||
const { swap_colors } = await wasm;
|
||||
swap_colors();
|
||||
},
|
||||
async resetColors() {
|
||||
const { reset_colors } = await wasm;
|
||||
reset_colors();
|
||||
},
|
||||
},
|
||||
components: {
|
||||
SwatchPairInput,
|
||||
IconButton,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="popover-button">
|
||||
<IconButton :icon="icon" :size="16" @click="clickButton" />
|
||||
<IconButton :icon="icon" :size="16" @click="clickButton" data-hover-menu-spawner />
|
||||
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Bottom" ref="floatingMenu">
|
||||
<slot></slot>
|
||||
</FloatingMenu>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open">
|
||||
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open" ref="floatingMenu">
|
||||
<div class="tail" v-if="type === MenuType.Popover"></div>
|
||||
<div class="floating-menu-container" ref="floatingMenuContainer">
|
||||
<div class="floating-menu-content" ref="floatingMenuContent">
|
||||
|
|
@ -18,7 +18,11 @@
|
|||
// Floating menus begin at a z-index of 1000
|
||||
z-index: 1000;
|
||||
--floating-menu-content-offset: 0;
|
||||
--floating-menu-content-border-radius: 0 0 4px 4px;
|
||||
--floating-menu-content-border-radius: 4px;
|
||||
|
||||
&.bottom {
|
||||
--floating-menu-content-border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.tail {
|
||||
width: 0;
|
||||
|
|
@ -35,7 +39,7 @@
|
|||
|
||||
.floating-menu-content {
|
||||
background: var(--floating-menu-opacity-color-2-mildblack);
|
||||
box-shadow: var(--color-0-black) 0 0 4px;
|
||||
box-shadow: var(--floating-menu-shadow) 0 2px 4px;
|
||||
border-radius: var(--floating-menu-content-border-radius);
|
||||
color: var(--color-e-nearwhite);
|
||||
font-size: inherit;
|
||||
|
|
@ -48,6 +52,61 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.dropdown {
|
||||
&.top {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.left {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.topleft {
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
&.topright {
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
&.topleft {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
|
||||
&.topright {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.top.dropdown .floating-menu-container,
|
||||
&.bottom.dropdown .floating-menu-container {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
&.popover {
|
||||
--floating-menu-content-offset: 10px;
|
||||
--floating-menu-content-border-radius: 4px;
|
||||
|
|
@ -116,6 +175,10 @@ export enum MenuDirection {
|
|||
Bottom = "Bottom",
|
||||
Left = "Left",
|
||||
Right = "Right",
|
||||
TopLeft = "TopLeft",
|
||||
TopRight = "TopRight",
|
||||
BottomLeft = "BottomLeft",
|
||||
BottomRight = "BottomRight",
|
||||
}
|
||||
|
||||
export enum MenuType {
|
||||
|
|
@ -128,6 +191,7 @@ export default defineComponent({
|
|||
props: {
|
||||
direction: { type: String, default: MenuDirection.Bottom },
|
||||
type: { type: String, required: true },
|
||||
windowEdgeMargin: { type: Number, default: 8 },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -147,18 +211,18 @@ export default defineComponent({
|
|||
const floatingMenuBounds = floatingMenuContent.getBoundingClientRect();
|
||||
|
||||
if (this.direction === MenuDirection.Left || this.direction === MenuDirection.Right) {
|
||||
const topOffset = floatingMenuBounds.top - workspaceBounds.top - 8;
|
||||
const topOffset = floatingMenuBounds.top - workspaceBounds.top - this.windowEdgeMargin;
|
||||
if (topOffset < 0) floatingMenuContainer.style.transform = `translate(0, ${-topOffset}px)`;
|
||||
|
||||
const bottomOffset = workspaceBounds.bottom - floatingMenuBounds.bottom - 8;
|
||||
const bottomOffset = workspaceBounds.bottom - floatingMenuBounds.bottom - this.windowEdgeMargin;
|
||||
if (bottomOffset < 0) floatingMenuContainer.style.transform = `translate(0, ${bottomOffset}px)`;
|
||||
}
|
||||
|
||||
if (this.direction === MenuDirection.Top || this.direction === MenuDirection.Bottom) {
|
||||
const leftOffset = floatingMenuBounds.left - workspaceBounds.left - 8;
|
||||
const leftOffset = floatingMenuBounds.left - workspaceBounds.left - this.windowEdgeMargin;
|
||||
if (leftOffset < 0) floatingMenuContainer.style.transform = `translate(${-leftOffset}px, 0)`;
|
||||
|
||||
const rightOffset = workspaceBounds.right - floatingMenuBounds.right - 8;
|
||||
const rightOffset = workspaceBounds.right - floatingMenuBounds.right - this.windowEdgeMargin;
|
||||
if (rightOffset < 0) floatingMenuContainer.style.transform = `translate(${rightOffset}px, 0)`;
|
||||
}
|
||||
}
|
||||
|
|
@ -170,11 +234,28 @@ export default defineComponent({
|
|||
setClosed() {
|
||||
this.open = false;
|
||||
},
|
||||
isOpen(): boolean {
|
||||
return this.open;
|
||||
},
|
||||
mouseMoveHandler(e: MouseEvent) {
|
||||
const MOUSE_STRAY_DISTANCE = 100;
|
||||
const target = e.target as HTMLElement;
|
||||
const mouseOverFloatingMenuKeepOpen = target && (target.closest("[data-hover-menu-keep-open]") as HTMLElement);
|
||||
const mouseOverFloatingMenuSpawner = target && (target.closest("[data-hover-menu-spawner]") as HTMLElement);
|
||||
// TODO: Simplify the following expression when optional chaining is supported by the build system
|
||||
const mouseOverOwnFloatingMenuSpawner =
|
||||
mouseOverFloatingMenuSpawner && mouseOverFloatingMenuSpawner.parentElement && mouseOverFloatingMenuSpawner.parentElement.contains(this.$refs.floatingMenu as HTMLElement);
|
||||
|
||||
// Swap this open floating menu with the one created by the floating menu spawner being hovered over
|
||||
if (mouseOverFloatingMenuSpawner && !mouseOverOwnFloatingMenuSpawner) {
|
||||
this.setClosed();
|
||||
mouseOverFloatingMenuSpawner.click();
|
||||
}
|
||||
|
||||
// Close the floating menu if the mouse has strayed far enough from its bounds
|
||||
if (this.isMouseEventOutsideFloatingMenu(e, MOUSE_STRAY_DISTANCE)) {
|
||||
if (this.isMouseEventOutsideFloatingMenu(e, MOUSE_STRAY_DISTANCE) && !mouseOverOwnFloatingMenuSpawner && !mouseOverFloatingMenuKeepOpen) {
|
||||
// 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
|
||||
this.setClosed();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
<template>
|
||||
<FloatingMenu :class="'menu-list'" :direction="direction" :type="MenuType.Dropdown" ref="floatingMenu" :windowEdgeMargin="0" data-hover-menu-keep-open>
|
||||
<template v-for="(section, sectionIndex) in menuEntries" :key="sectionIndex">
|
||||
<Separator :type="SeparatorType.List" :direction="SeparatorDirection.Vertical" v-if="sectionIndex > 0" />
|
||||
<div
|
||||
v-for="(entry, entryIndex) in section"
|
||||
:key="entryIndex"
|
||||
class="row"
|
||||
:class="{ open: isMenuEntryOpen(entry) }"
|
||||
@click="handleEntryClick(entry)"
|
||||
@mouseenter="handleEntryMouseEnter(entry)"
|
||||
@mouseleave="handleEntryMouseLeave(entry)"
|
||||
:data-hover-menu-spawner-extend="entry.children && []"
|
||||
>
|
||||
<Icon :icon="entry.icon" v-if="entry.icon" />
|
||||
<div class="no-icon" v-else />
|
||||
<span class="label">{{ entry.label }}</span>
|
||||
<UserInputLabel v-if="entry.shortcut && entry.shortcut.length" :inputKeys="[entry.shortcut]" />
|
||||
<div class="submenu-arrow" v-if="entry.children && entry.children.length"></div>
|
||||
<div class="no-submenu-arrow" v-else></div>
|
||||
<MenuList v-if="entry.children" :menuEntries="entry.children" :direction="MenuDirection.TopRight" :ref="(ref) => setEntryRefs(entry, ref)" />
|
||||
</div>
|
||||
</template>
|
||||
</FloatingMenu>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-list {
|
||||
.floating-menu-container .floating-menu-content {
|
||||
min-width: 240px;
|
||||
padding: 4px 0;
|
||||
|
||||
.row {
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
|
||||
& > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
.no-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.no-icon,
|
||||
.label {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.user-input-label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.submenu-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 3px 0 3px 6px;
|
||||
border-color: transparent transparent transparent var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
.no-submenu-arrow {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.submenu-arrow,
|
||||
.no-submenu-arrow {
|
||||
margin-left: 4px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
svg {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
import FloatingMenu, { MenuDirection, MenuType } from "./FloatingMenu.vue";
|
||||
import Separator, { SeparatorDirection, SeparatorType } from "../Separator.vue";
|
||||
import Icon from "../labels/Icon.vue";
|
||||
import UserInputLabel from "../labels/UserInputLabel.vue";
|
||||
|
||||
export type MenuListEntries = Array<MenuListEntry>;
|
||||
|
||||
export interface MenuListEntry {
|
||||
label?: string;
|
||||
icon?: string;
|
||||
// TODO: Add `checkbox` (which overrides any `icon`)
|
||||
shortcut?: Array<string>;
|
||||
action?: Function;
|
||||
children?: Array<Array<MenuListEntry>>;
|
||||
ref?: typeof FloatingMenu | typeof MenuList;
|
||||
}
|
||||
const MenuList = defineComponent({
|
||||
props: {
|
||||
direction: { type: String as PropType<MenuDirection>, value: MenuDirection.Bottom },
|
||||
menuEntries: { type: Array as PropType<MenuListEntries>, required: true },
|
||||
},
|
||||
methods: {
|
||||
setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu) {
|
||||
if (ref) menuEntry.ref = ref;
|
||||
},
|
||||
handleEntryClick(menuEntry: MenuListEntry) {
|
||||
if (menuEntry.action) menuEntry.action();
|
||||
else alert("This action is not yet implemented");
|
||||
},
|
||||
handleEntryMouseEnter(menuEntry: MenuListEntry) {
|
||||
if (!menuEntry.children || !menuEntry.children.length) return;
|
||||
|
||||
if (menuEntry.ref) {
|
||||
menuEntry.ref.setOpen();
|
||||
} else throw new Error("The menu bar floating menu has no associated ref");
|
||||
},
|
||||
handleEntryMouseLeave(menuEntry: MenuListEntry) {
|
||||
if (!menuEntry.children || !menuEntry.children.length) return;
|
||||
|
||||
if (menuEntry.ref) {
|
||||
menuEntry.ref.setClosed();
|
||||
} else throw new Error("The menu bar floating menu has no associated ref");
|
||||
},
|
||||
isMenuEntryOpen(menuEntry: MenuListEntry): boolean {
|
||||
if (!menuEntry.children || !menuEntry.children.length) return false;
|
||||
|
||||
if (menuEntry.ref) {
|
||||
return menuEntry.ref.isOpen();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
setOpen() {
|
||||
(this.$refs.floatingMenu as typeof FloatingMenu).setOpen();
|
||||
},
|
||||
setClosed() {
|
||||
(this.$refs.floatingMenu as typeof FloatingMenu).setClosed();
|
||||
},
|
||||
isOpen(): boolean {
|
||||
const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu;
|
||||
return Boolean(floatingMenu && floatingMenu.isOpen());
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
SeparatorDirection,
|
||||
SeparatorType,
|
||||
MenuDirection,
|
||||
MenuType,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
FloatingMenu,
|
||||
Separator,
|
||||
Icon,
|
||||
UserInputLabel,
|
||||
},
|
||||
});
|
||||
export default MenuList;
|
||||
</script>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<template>
|
||||
<div class="menu-bar-input">
|
||||
<div class="entry"><Icon :icon="'GraphiteLogo'" /></div>
|
||||
<div class="entry"><span>File</span></div>
|
||||
<div class="entry"><span>Edit</span></div>
|
||||
<div class="entry"><span>Document</span></div>
|
||||
<div class="entry"><span>View</span></div>
|
||||
<div class="entry"><span>Help</span></div>
|
||||
<div class="entry-container" v-for="entry in menuEntries" :key="entry">
|
||||
<div @click="handleEntryClick(entry)" class="entry" :class="{ open: entry.ref && entry.ref.isOpen() }" data-hover-menu-spawner>
|
||||
<Icon :icon="entry.icon" v-if="entry.icon" />
|
||||
<span v-if="entry.label">{{ entry.label }}</span>
|
||||
</div>
|
||||
<MenuList :menuEntries="entry.children" :direction="MenuDirection.Bottom" :ref="(ref) => setEntryRefs(entry, ref)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -13,25 +14,31 @@
|
|||
.menu-bar-input {
|
||||
display: flex;
|
||||
|
||||
.entry {
|
||||
.entry-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
padding: 0 8px;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-6-lowergray);
|
||||
.entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
padding: 0 8px;
|
||||
|
||||
svg {
|
||||
fill: var(--color-f-white);
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--color-f-white);
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
svg {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,13 +49,108 @@
|
|||
import { defineComponent } from "vue";
|
||||
import Icon from "../labels/Icon.vue";
|
||||
import { ApplicationPlatform } from "../../window/MainWindow.vue";
|
||||
import MenuList, { MenuListEntry, MenuListEntries } from "../floating-menus/MenuList.vue";
|
||||
import { MenuDirection } from "../floating-menus/FloatingMenu.vue";
|
||||
|
||||
const wasm = import("../../../../wasm/pkg");
|
||||
|
||||
const menuEntries: MenuListEntries = [
|
||||
{
|
||||
icon: "GraphiteLogo",
|
||||
ref: undefined,
|
||||
children: [[{ label: "Visit project GitHub…", action: () => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") }]],
|
||||
},
|
||||
{
|
||||
label: "File",
|
||||
ref: undefined,
|
||||
children: [
|
||||
[
|
||||
{ label: "New", icon: "FileNew", shortcut: ["Ctrl", "N"] },
|
||||
{ label: "Open…", shortcut: ["Ctrl", "O"] },
|
||||
{
|
||||
label: "Open Recent",
|
||||
shortcut: ["Ctrl", "⇧", "O"],
|
||||
children: [
|
||||
[{ label: "Reopen Last Closed", shortcut: ["Ctrl", "⇧", "T"] }, { label: "Clear Recently Opened" }],
|
||||
[
|
||||
{ label: "Some Recent File.gdd" },
|
||||
{ label: "Another Recent File.gdd" },
|
||||
{ label: "An Older File.gdd" },
|
||||
{ label: "Some Other Older File.gdd" },
|
||||
{ label: "Yet Another Older File.gdd" },
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{ label: "Close", shortcut: ["Ctrl", "W"] },
|
||||
{ label: "Close All", shortcut: ["Ctrl", "Alt", "W"] },
|
||||
],
|
||||
[
|
||||
{ label: "Save", shortcut: ["Ctrl", "S"] },
|
||||
{ label: "Save As…", shortcut: ["Ctrl", "⇧", "S"] },
|
||||
{ label: "Save All", shortcut: ["Ctrl", "Alt", "S"] },
|
||||
{ label: "Auto-Save", shortcut: undefined },
|
||||
],
|
||||
[
|
||||
{ label: "Import…", shortcut: ["Ctrl", "I"] },
|
||||
{ label: "Export…", shortcut: ["Ctrl", "E"] },
|
||||
],
|
||||
[{ label: "Quit", shortcut: ["Ctrl", "Q"] }],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
ref: undefined,
|
||||
children: [
|
||||
[
|
||||
{ label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => (await wasm).undo()},
|
||||
{ label: "Redo", shortcut: ["Ctrl", "⇧", "Z"] },
|
||||
],
|
||||
[
|
||||
{ label: "Cut", shortcut: ["Ctrl", "X"] },
|
||||
{ label: "Copy", shortcut: ["Ctrl", "C"] },
|
||||
{ label: "Paste", shortcut: ["Ctrl", "V"] },
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Document",
|
||||
ref: undefined,
|
||||
children: [[{ label: "Menu not yet populated" }]],
|
||||
},
|
||||
{
|
||||
label: "View",
|
||||
ref: undefined,
|
||||
children: [[{ label: "Menu not yet populated" }]],
|
||||
},
|
||||
{
|
||||
label: "Help",
|
||||
ref: undefined,
|
||||
children: [[{ label: "Menu not yet populated" }]],
|
||||
},
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
components: { Icon },
|
||||
methods: {
|
||||
setEntryRefs(menuEntry: MenuListEntry, ref: typeof MenuList) {
|
||||
if (ref) menuEntry.ref = ref;
|
||||
},
|
||||
handleEntryClick(menuEntry: MenuListEntry) {
|
||||
if (menuEntry.ref) menuEntry.ref.setOpen();
|
||||
else throw new Error("The menu bar floating menu has no associated ref");
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ApplicationPlatform,
|
||||
menuEntries,
|
||||
MenuDirection,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
MenuList,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div class="swatch-pair">
|
||||
<div class="secondary swatch">
|
||||
<button @click="clickSecondarySwatch" ref="secondaryButton"></button>
|
||||
<button @click="clickSecondarySwatch" ref="secondaryButton" data-hover-menu-spawner></button>
|
||||
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Right" horizontal ref="secondarySwatchFloatingMenu">
|
||||
<ColorPicker v-model:color="secondaryColor" />
|
||||
</FloatingMenu>
|
||||
</div>
|
||||
<div class="primary swatch">
|
||||
<button @click="clickPrimarySwatch" ref="primaryButton"></button>
|
||||
<button @click="clickPrimarySwatch" ref="primaryButton" data-hover-menu-spawner></button>
|
||||
<FloatingMenu :type="MenuType.Popover" :direction="MenuDirection.Right" horizontal ref="primarySwatchFloatingMenu">
|
||||
<ColorPicker v-model:color="primaryColor" />
|
||||
</FloatingMenu>
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ import ViewModePixels from "../../../../assets/16px-solid/view-mode-pixels.svg";
|
|||
import EyeVisible from "../../../../assets/16px-solid/visibility-eye-visible.svg";
|
||||
import EyeHidden from "../../../../assets/16px-solid/visibility-eye-hidden.svg";
|
||||
import GraphiteLogo from "../../../../assets/16px-solid/graphite-logo.svg";
|
||||
import FileNew from "../../../../assets/16px-solid/file-new.svg";
|
||||
|
||||
import SwapButton from "../../../../assets/12px-solid/swap.svg";
|
||||
import ResetColorsButton from "../../../../assets/12px-solid/reset-colors.svg";
|
||||
|
|
@ -142,6 +143,7 @@ const icons = {
|
|||
EyeVisible: { component: EyeVisible, size: 16 },
|
||||
EyeHidden: { component: EyeHidden, size: 16 },
|
||||
GraphiteLogo: { component: GraphiteLogo, size: 16 },
|
||||
FileNew: { component: FileNew, size: 16 },
|
||||
SwapButton: { component: SwapButton, size: 12 },
|
||||
ResetColorsButton: { component: ResetColorsButton, size: 12 },
|
||||
DropdownArrow: { component: DropdownArrow, size: 12 },
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
<template>
|
||||
<div class="user-input-label">
|
||||
<span class="input-key" v-for="inputKey in inputKeys" :key="inputKey" :class="keyCapWidth(inputKey)">
|
||||
{{ inputKey }}
|
||||
</span>
|
||||
<template v-for="(keyGroup, keyGroupIndex) in inputKeys" :key="keyGroupIndex">
|
||||
<span class="group-gap" v-if="keyGroupIndex > 0"></span>
|
||||
<span class="input-key" v-for="inputKey in keyGroup" :key="inputKey" :class="keyCapWidth(inputKey)">
|
||||
{{ inputKey }}
|
||||
</span>
|
||||
</template>
|
||||
<span class="input-mouse" v-if="inputMouse">
|
||||
<Icon :icon="mouseInputInteractionToIcon(inputMouse)" />
|
||||
</span>
|
||||
<span class="hint-text">
|
||||
<span class="hint-text" v-if="hasSlotContent">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -20,17 +23,22 @@
|
|||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
.group-gap {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.input-key,
|
||||
.input-mouse {
|
||||
margin-right: 4px;
|
||||
& + .input-key,
|
||||
& + .input-mouse {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-key {
|
||||
font-family: "Inconsolata", monospace;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: var(--color-2-mildblack);
|
||||
background: var(--color-e-nearwhite);
|
||||
color: var(--color-e-nearwhite);
|
||||
border: 1px;
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
|
|
@ -73,6 +81,10 @@
|
|||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -99,6 +111,11 @@ export default defineComponent({
|
|||
inputKeys: { type: Array, default: () => [] },
|
||||
inputMouse: { type: String },
|
||||
},
|
||||
computed: {
|
||||
hasSlotContent(): boolean {
|
||||
return Boolean(this.$slots.default);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
keyCapWidth(keyText: string) {
|
||||
return `width-${keyText.length * 8 + 8}`;
|
||||
|
|
|
|||
|
|
@ -2,26 +2,26 @@
|
|||
<div class="status-bar">
|
||||
<UserInputLabel :inputMouse="'LMBDrag'">Drag Selected</UserInputLabel>
|
||||
<Separator :type="SeparatorType.Section" />
|
||||
<UserInputLabel :inputKeys="['G']">Grab Selected</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="['R']">Rotate Selected</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="['S']">Scale Selected</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['G']]">Grab Selected</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['R']]">Rotate Selected</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['S']]">Scale Selected</UserInputLabel>
|
||||
<Separator :type="SeparatorType.Section" />
|
||||
<UserInputLabel :inputMouse="'LMB'">Select Object</UserInputLabel>
|
||||
<span class="plus">+</span>
|
||||
<UserInputLabel :inputKeys="['Ctrl']">Innermost</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['Ctrl']]">Innermost</UserInputLabel>
|
||||
<span class="plus">+</span>
|
||||
<UserInputLabel :inputKeys="['⇧']">Grow/Shrink Selection</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['⇧']]">Grow/Shrink Selection</UserInputLabel>
|
||||
<Separator :type="SeparatorType.Section" />
|
||||
<UserInputLabel :inputMouse="'LMBDrag'">Select Area</UserInputLabel>
|
||||
<span class="plus">+</span>
|
||||
<UserInputLabel :inputKeys="['⇧']">Grow/Shrink Selection</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['⇧']]">Grow/Shrink Selection</UserInputLabel>
|
||||
<Separator :type="SeparatorType.Section" />
|
||||
<UserInputLabel :inputKeys="['↑', '→', '↓', '←']">Nudge Selected</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['↑'], ['→'], ['↓'], ['←']]">Nudge Selected</UserInputLabel>
|
||||
<span class="plus">+</span>
|
||||
<UserInputLabel :inputKeys="['⇧']">Big Increment Nudge</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['⇧']]">Big Increment Nudge</UserInputLabel>
|
||||
<Separator :type="SeparatorType.Section" />
|
||||
<UserInputLabel :inputKeys="['Alt']" :inputMouse="'LMBDrag'">Move Duplicate</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="['Ctrl', 'D']">Duplicate</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['Alt']]" :inputMouse="'LMBDrag'">Move Duplicate</UserInputLabel>
|
||||
<UserInputLabel :inputKeys="[['Ctrl', 'D']]">Duplicate</UserInputLabel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,12 @@ pub fn reset_colors() -> Result<(), JsValue> {
|
|||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ToolMessage::ResetColors)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// Undo history one step
|
||||
#[wasm_bindgen]
|
||||
pub fn undo() -> Result<(), JsValue> {
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Undo)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// Select a layer from the layer list
|
||||
#[wasm_bindgen]
|
||||
pub fn select_layer(path: Vec<LayerId>) -> Result<(), JsValue> {
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ impl MessageHandler<ToolMessage, (&SvgDocument, &InputPreprocessor)> for ToolMes
|
|||
}
|
||||
ResetColors => {
|
||||
let doc_data = &mut self.tool_state.document_tool_data;
|
||||
doc_data.primary_color = Color::WHITE;
|
||||
doc_data.secondary_color = Color::BLACK;
|
||||
doc_data.primary_color = Color::BLACK;
|
||||
doc_data.secondary_color = Color::WHITE;
|
||||
}
|
||||
message => {
|
||||
let tool_type = match message {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ Contributions welcome! If you think of something Graphite would be great for, su
|
|||
- Creating a chart from a CSV
|
||||
- Rendering an always-up-to-date chart powered by real-time updates from a database
|
||||
- Data-driven infographics like an org chart that can be updated with text instead of manual design work
|
||||
- Request the weather from an API and render live visualizations which gets displayed on a monitor in your house or a museum (export to a Windows screen saver?)
|
||||
|
||||
## Digital painting
|
||||
- Creating a digital acrylic or oil painting using various brushes
|
||||
|
|
|
|||
Loading…
Reference in New Issue