Move layouts definitions to backend and fix Firefox overlay scrollbars (#647)

* Fix two-axis scrollbars in scrollable regions on Firefox

* Move Document Mode dropdown to the backend; and related code cleanup

* Port the Layer Tree options bar layout to the backend

* Port the tool shelf to the backend

* Clean up initialization and wasm wrapper

* Fix crash

* Fix missing document bar

* Remove unused functions in api.rs

* Code review

* Tool initalisation

* Remove some frontend functions

* Initalise -> Init so en-US/GB doesn't have to matter :)

* Remove blend_mode and opacity from LayerPanelEntry

Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers
2022-05-17 13:12:52 -07:00
co-authored by 0hypercube
parent e7d63276ad
commit 29e00e488b
50 changed files with 1034 additions and 978 deletions
@@ -9,7 +9,7 @@
v-model:activeEntry="activeEntry"
@update:activeEntry="(newActiveEntry: typeof MENU_LIST_ENTRY) => activeEntryChanged(newActiveEntry)"
@widthChanged="(newWidth: number) => onWidthChanged(newWidth)"
:menuEntries="menuEntries"
:entries="entries"
:direction="'Bottom'"
:drawIcon="drawIcon"
:scrollableY="true"
@@ -100,23 +100,23 @@ declare global {
export default defineComponent({
emits: ["update:selectedIndex"],
props: {
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
selectedIndex: { type: Number as PropType<number>, required: true },
entries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
selectedIndex: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
drawIcon: { type: Boolean as PropType<boolean>, default: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
activeEntry: this.menuEntries.flat()[this.selectedIndex],
activeEntry: this.selectedIndex !== undefined ? this.entries.flat()[this.selectedIndex] : { label: "-" },
minWidth: 0,
};
},
watch: {
// Called only when `selectedIndex` is changed from outside this component (with v-model)
selectedIndex(newSelectedIndex: number) {
const entries = this.menuEntries.flat();
selectedIndex(newSelectedIndex: number | undefined) {
const entries = this.entries.flat();
if (!Number.isNaN(newSelectedIndex) && newSelectedIndex >= 0 && newSelectedIndex < entries.length) {
if (newSelectedIndex !== undefined && newSelectedIndex >= 0 && newSelectedIndex < entries.length) {
this.activeEntry = entries[newSelectedIndex];
} else {
this.activeEntry = { label: "-" };
@@ -126,7 +126,7 @@ export default defineComponent({
methods: {
// Called only when `activeEntry` is changed from the child MenuList component via user input
activeEntryChanged(newActiveEntry: MenuListEntry) {
this.$emit("update:selectedIndex", this.menuEntries.flat().indexOf(newActiveEntry));
this.$emit("update:selectedIndex", this.entries.flat().indexOf(newActiveEntry));
},
clickDropdownBox() {
if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen();
@@ -4,14 +4,7 @@
<span>{{ activeEntry.label }}</span>
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
</LayoutRow>
<MenuList
v-model:activeEntry="activeEntry"
@widthChanged="(newWidth: number) => onWidthChanged(newWidth)"
:menuEntries="menuEntries"
:direction="'Bottom'"
:scrollableY="true"
ref="menuList"
/>
<MenuList v-model:activeEntry="activeEntry" @widthChanged="(newWidth: number) => onWidthChanged(newWidth)" :entries="entries" :direction="'Bottom'" :scrollableY="true" ref="menuList" />
</LayoutRow>
</template>
@@ -100,9 +93,9 @@ export default defineComponent({
isStyle: { type: Boolean as PropType<boolean>, default: false },
},
data() {
const { menuEntries, activeEntry } = this.updateEntries();
const { entries, activeEntry } = this.updateEntries();
return {
menuEntries,
entries,
activeEntry,
minWidth: 0,
};
@@ -133,12 +126,12 @@ export default defineComponent({
onWidthChanged(newWidth: number) {
this.minWidth = newWidth;
},
updateEntries(): { menuEntries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
updateEntries(): { entries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
const choices = this.isStyle ? getFontStyles(this.fontFamily) : fontNames();
const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;
let selectedEntry: MenuListEntry | undefined;
const entries = choices.map((name) => {
const menuListEntries = choices.map((name) => {
const result: MenuListEntry = {
label: name,
action: (): void => this.selectFont(name),
@@ -149,21 +142,21 @@ export default defineComponent({
return result;
});
const menuEntries: SectionsOfMenuListEntries = [entries];
const entries: SectionsOfMenuListEntries = [menuListEntries];
const activeEntry = selectedEntry || { label: "-" };
return { menuEntries, activeEntry };
return { entries, activeEntry };
},
},
watch: {
fontFamily() {
const { menuEntries, activeEntry } = this.updateEntries();
this.menuEntries = menuEntries;
const { entries, activeEntry } = this.updateEntries();
this.entries = entries;
this.activeEntry = activeEntry;
},
fontStyle() {
const { menuEntries, activeEntry } = this.updateEntries();
this.menuEntries = menuEntries;
const { entries, activeEntry } = this.updateEntries();
this.entries = entries;
this.activeEntry = activeEntry;
},
},
@@ -5,12 +5,12 @@
<IconLabel :icon="'GraphiteLogo'" />
</div>
</div>
<div class="entry-container" v-for="(entry, index) in menuEntries" :key="index">
<div class="entry-container" v-for="(entry, index) in entries" :key="index">
<div @click="() => handleEntryClick(entry)" class="entry" :class="{ open: entry.ref?.isOpen() }" data-hover-menu-spawner>
<IconLabel :icon="entry.icon" v-if="entry.icon" />
<span v-if="entry.label">{{ entry.label }}</span>
</div>
<MenuList :menuEntries="entry.children || []" :direction="'Bottom'" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref: any) => setEntryRefs(entry, ref)" />
<MenuList :entries="entry.children || []" :direction="'Bottom'" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref: any) => setEntryRefs(entry, ref)" />
</div>
</div>
</template>
@@ -58,7 +58,7 @@ import { EditorState } from "@/state/wasm-loader";
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
function makeMenuEntries(editor: EditorState): MenuListEntries {
function makeEntries(editor: EditorState): MenuListEntries {
return [
{
label: "File",
@@ -213,7 +213,7 @@ export default defineComponent({
},
data() {
return {
menuEntries: makeMenuEntries(this.editor),
entries: makeEntries(this.editor),
comingSoon: (): void => this.dialog.comingSoon(),
};
},
@@ -10,8 +10,8 @@
@cancelTextChange="() => onCancelTextChange()"
ref="fieldInput"
>
<button v-if="!Number.isNaN(value)" class="arrow left" @click="() => onIncrement('Decrease')"></button>
<button v-if="!Number.isNaN(value)" class="arrow right" @click="() => onIncrement('Increase')"></button>
<button v-if="value !== undefined" class="arrow left" @click="() => onIncrement('Decrease')"></button>
<button v-if="value !== undefined" class="arrow right" @click="() => onIncrement('Increase')"></button>
</FieldInput>
</template>
@@ -94,7 +94,7 @@ import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
export default defineComponent({
emits: ["update:value"],
props: {
value: { type: Number as PropType<number>, required: true },
value: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
min: { type: Number as PropType<number>, required: false },
max: { type: Number as PropType<number>, required: false },
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
@@ -116,7 +116,7 @@ export default defineComponent({
},
methods: {
onTextFocused() {
if (Number.isNaN(this.value)) this.text = "";
if (this.value === undefined) this.text = "";
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
else this.text = `${this.value}${this.unit}`;
@@ -131,18 +131,20 @@ export default defineComponent({
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding)
onTextChanged() {
// The `inputElement.blur()` call at the bottom of this function causes itself to be run again, so this check skips a second run
if (this.editing) {
const newValue = parseFloat(this.text);
this.updateValue(newValue);
if (!this.editing) return;
this.editing = false;
const parsed = parseFloat(this.text);
const newValue = Number.isNaN(parsed) ? undefined : parsed;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
inputElement.blur();
}
this.updateValue(newValue);
this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
inputElement.blur();
},
onCancelTextChange() {
this.updateValue(NaN);
this.updateValue(undefined);
this.editing = false;
@@ -150,16 +152,16 @@ export default defineComponent({
inputElement.blur();
},
onIncrement(direction: IncrementDirection) {
if (Number.isNaN(this.value)) return;
if (this.value === undefined) return;
const actions = {
Add: (): void => {
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
this.updateValue(this.value + directionAddend);
this.updateValue(this.value !== undefined ? this.value + directionAddend : undefined);
},
Multiply: (): void => {
const directionMultiplier = direction === "Increase" ? this.incrementFactor : 1 / this.incrementFactor;
this.updateValue(this.value * directionMultiplier);
this.updateValue(this.value !== undefined ? this.value * directionMultiplier : undefined);
},
Callback: (): void => {
if (direction === "Increase") this.incrementCallbackIncrease?.();
@@ -170,20 +172,20 @@ export default defineComponent({
const action = actions[this.incrementBehavior];
action();
},
updateValue(newValue: number) {
const invalid = Number.isNaN(newValue);
updateValue(newValue: number | undefined) {
const nowValid = this.value !== undefined && this.isInteger ? Math.round(this.value) : this.value;
let cleaned = newValue !== undefined ? newValue : nowValid;
let sanitized = newValue;
if (invalid) sanitized = this.value;
if (this.isInteger) sanitized = Math.round(sanitized);
if (typeof this.min === "number" && !Number.isNaN(this.min)) sanitized = Math.max(sanitized, this.min);
if (typeof this.max === "number" && !Number.isNaN(this.max)) sanitized = Math.min(sanitized, this.max);
if (typeof this.min === "number" && !Number.isNaN(this.min) && cleaned !== undefined) cleaned = Math.max(cleaned, this.min);
if (typeof this.max === "number" && !Number.isNaN(this.max) && cleaned !== undefined) cleaned = Math.min(cleaned, this.max);
if (!invalid) this.$emit("update:value", sanitized);
if (newValue !== undefined) this.$emit("update:value", cleaned);
this.text = this.displayText(sanitized);
this.text = this.displayText(cleaned);
},
displayText(value: number): string {
displayText(value: number | undefined): string {
if (value === undefined) return "-";
// Find the amount of digits on the left side of the decimal
// 10.25 == 2
// 1.23 == 1
@@ -199,8 +201,8 @@ export default defineComponent({
},
watch: {
// Called only when `value` is changed from outside this component (with v-model)
value(newValue: number) {
if (Number.isNaN(newValue)) {
value(newValue: number | undefined) {
if (newValue === undefined) {
this.text = "-";
return;
}
@@ -1,50 +0,0 @@
<template>
<LayoutRow class="shelf-item-input" :class="{ active: active }">
<IconButton :action="action" :icon="icon" :size="32" />
</LayoutRow>
</template>
<style lang="scss">
.shelf-item-input {
flex: 0 0 auto;
border-radius: 2px;
&:hover {
background: var(--color-6-lowergray);
}
&.active {
background: var(--color-accent);
}
.icon-button {
background: unset;
}
svg {
width: 24px;
height: 24px;
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { IconName } from "@/utilities/icons";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
export default defineComponent({
components: {
IconButton,
LayoutRow,
},
props: {
icon: { type: String as PropType<IconName>, required: true },
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
active: { type: Boolean as PropType<boolean>, default: false },
},
});
</script>