Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2566c1f
Keyboard menu navigation
Keavon May 6, 2022
5fa20a9
Fix dropdown keyboard navigation
0HyperCube May 16, 2022
f14591b
Merge branch 'master' into widget-keyboard-nav
0HyperCube May 16, 2022
2e2bc54
Merge branch 'master' into widget-keyboard-nav
0HyperCube May 19, 2022
2cca3e5
Fix merge error
0HyperCube May 19, 2022
4621410
Merge branch 'master' into widget-keyboard-nav
0HyperCube May 22, 2022
8e0e144
Some code review
0HyperCube May 22, 2022
1106407
Interactive dropdowns
0HyperCube May 22, 2022
9970454
Query by data attr not class name
0HyperCube May 23, 2022
641740a
Add locking behaviour
0HyperCube May 23, 2022
ef5b8be
Add font prieviews
0HyperCube May 19, 2022
7c32f93
Merge branch 'master' into font-prieviews
0HyperCube May 24, 2022
796e351
Merge remote-tracking branch 'origin/master' into font-prieviews
0HyperCube Jun 5, 2022
246617b
Remove blank line in css
0HyperCube Jun 7, 2022
be580f2
Merge branch 'master' into font-prieviews
0HyperCube Jun 7, 2022
7c7f225
Use default for interactive in struct
0HyperCube Jun 9, 2022
763ca73
Use menulist for fontinput
0HyperCube Jun 9, 2022
99e8bd4
Polish
0HyperCube Jun 9, 2022
712417b
Rename state -> manager
0HyperCube Jun 9, 2022
d299f45
Merge branch 'master' into font-prieviews
0HyperCube Jun 9, 2022
8f93cb0
Code review
0HyperCube Jun 9, 2022
beaf0e4
Cleanup fontinput
0HyperCube Jun 10, 2022
2f7fcd7
More cleanup
0HyperCube Jun 10, 2022
6a12a6a
Merge branch 'master' into font-prieviews
0HyperCube Jun 10, 2022
2995812
Make fonts.ts an empty state
0HyperCube Jun 10, 2022
2e2f45d
Fix regression
0HyperCube Jun 10, 2022
794e65b
Merge branch 'master' into font-prieviews
Keavon Jun 10, 2022
13e645b
Merge branch 'master' into font-prieviews
Keavon Jun 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions editor/src/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ impl DocumentMessageHandler {
]],
selected_index: Some(self.document_mode as u32),
draw_icon: true,
interactive: false, // TODO: set to true when dialogs are not spawned
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
Expand Down
103 changes: 69 additions & 34 deletions frontend/src/components/floating-menus/MenuList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,57 @@
:type="'Dropdown'"
:windowEdgeMargin="0"
:escapeCloses="false"
v-bind="{ direction, scrollableY, minWidth }"
v-bind="{ direction, scrollableY: scrollableY && virtualScrollingEntryHeight === 0, minWidth }"
ref="floatingMenu"
data-hover-menu-keep-open
>
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
<!-- If we put the scrollableY on the layoutcol for non-font dropdowns then for some reason it always creates a tiny scrollbar.
However when we are using the virtual scrolling then we need the layoutcol to be scrolling so we can bind the events without using $refs. -->
<LayoutCol ref="scroller" :scrollableY="scrollableY && virtualScrollingEntryHeight !== 0" @scroll="onScroll" :style="{ minWidth: virtualScrollingEntryHeight ? `${minWidth}px` : `inherit` }">
<LayoutRow v-if="virtualScrollingEntryHeight" class="scroll-spacer" :style="{ height: `${virtualScrollingStartIndex * virtualScrollingEntryHeight}px` }"></LayoutRow>
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
<LayoutRow
v-for="(entry, entryIndex) in virtualScrollingEntryHeight ? section.slice(virtualScrollingStartIndex, virtualScrollingEndIndex) : section"
:key="entryIndex + (virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0)"
class="row"
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label }"
:style="{ height: virtualScrollingEntryHeight || '20px' }"
@click="() => onEntryClick(entry)"
@pointerenter="() => onEntryPointerEnter(entry)"
@pointerleave="() => onEntryPointerLeave(entry)"
>
<CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" :disableTabIndex="true" class="entry-checkbox" />
<IconLabel v-else-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
<div v-else-if="drawIcon" class="no-icon"></div>

<link v-if="entry.font" rel="stylesheet" :href="entry.font?.toString()" />

<span class="entry-label" :style="{ fontFamily: `${!entry.font ? 'inherit' : entry.value}` }">{{ entry.label }}</span>

<IconLabel v-if="entry.shortcutRequiresLock && !fullscreen.state.keyboardLocked" :icon="'Info'" :title="keyboardLockInfoMessage" />
<UserInputLabel v-else-if="entry.shortcut?.length" :inputKeys="[entry.shortcut]" />

<div class="submenu-arrow" v-if="entry.children?.length"></div>
<div class="no-submenu-arrow" v-else></div>

<MenuList
v-if="entry.children"
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
:open="entry.ref?.open || false"
:direction="'TopRight'"
:entries="entry.children"
v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
:ref="(ref: typeof FloatingMenu) => ref && (entry.ref = ref)"
/>
</LayoutRow>
</template>
<LayoutRow
v-for="(entry, entryIndex) in section"
:key="entryIndex"
class="row"
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label }"
@click="() => onEntryClick(entry)"
@pointerenter="() => onEntryPointerEnter(entry)"
@pointerleave="() => onEntryPointerLeave(entry)"
>
<CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" :disableTabIndex="true" class="entry-checkbox" />
<IconLabel v-else-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
<div v-else-if="drawIcon" class="no-icon"></div>

<span class="entry-label">{{ entry.label }}</span>

<IconLabel v-if="entry.shortcutRequiresLock && !fullscreen.state.keyboardLocked" :icon="'Info'" :title="keyboardLockInfoMessage" />
<UserInputLabel v-else-if="entry.shortcut?.length" :inputKeys="[entry.shortcut]" />

<div class="submenu-arrow" v-if="entry.children?.length"></div>
<div class="no-submenu-arrow" v-else></div>

<MenuList
v-if="entry.children"
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
:open="entry.ref?.open || false"
:direction="'TopRight'"
:entries="entry.children"
v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
:ref="(ref: typeof FloatingMenu) => ref && (entry.ref = ref)"
/>
</LayoutRow>
</template>
v-if="virtualScrollingEntryHeight"
class="scroll-spacer"
:style="{ height: `${virtualScrollingTotalHeight - virtualScrollingEndIndex * virtualScrollingEntryHeight}px` }"
></LayoutRow>
</LayoutCol>
</FloatingMenu>
</template>

Expand All @@ -52,6 +65,10 @@
.floating-menu-container .floating-menu-content {
padding: 4px 0;

.scroll-spacer {
flex: 0 0 auto;
}

.row {
height: 20px;
align-items: center;
Expand Down Expand Up @@ -145,6 +162,7 @@ import { defineComponent, PropType } from "vue";
import { IconName } from "@/utility-functions/icons";

import FloatingMenu, { MenuDirection } from "@/components/floating-menus/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
Expand All @@ -158,6 +176,7 @@ interface MenuListEntryData<Value = string> {
value?: Value;
label?: string;
icon?: IconName;
font?: URL;
checkbox?: boolean;
shortcut?: string[];
shortcutRequiresLock?: boolean;
Expand All @@ -182,13 +201,15 @@ const MenuList = defineComponent({
drawIcon: { type: Boolean as PropType<boolean>, default: false },
interactive: { type: Boolean as PropType<boolean>, default: false },
scrollableY: { type: Boolean as PropType<boolean>, default: false },
virtualScrollingEntryHeight: { type: Number as PropType<number>, default: 0 },
defaultAction: { type: Function as PropType<() => void>, required: false },
},
data() {
return {
isOpen: this.open,
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
highlighted: this.activeEntry as MenuListEntry | undefined,
virtualScrollingEntriesStart: 0,
};
},
watch: {
Expand Down Expand Up @@ -326,6 +347,10 @@ const MenuList = defineComponent({
// Interactive menus should keep the active entry the same as the highlighted one
if (this.interactive && newHighlight?.value !== this.activeEntry?.value) this.$emit("update:activeEntry", newHighlight);
},
onScroll(e: Event) {
if (!this.virtualScrollingEntryHeight) return;
this.virtualScrollingEntriesStart = (e.target as HTMLElement)?.scrollTop || 0;
},
},
computed: {
entriesWithoutRefs(): MenuListEntryData[][] {
Expand All @@ -336,6 +361,15 @@ const MenuList = defineComponent({
})
);
},
virtualScrollingTotalHeight() {
return this.entries[0].length * this.virtualScrollingEntryHeight;
},
virtualScrollingStartIndex() {
return Math.floor(this.virtualScrollingEntriesStart / this.virtualScrollingEntryHeight);
},
virtualScrollingEndIndex() {
return Math.min(this.entries[0].length, this.virtualScrollingStartIndex + 1 + 400 / this.virtualScrollingEntryHeight);
},
},
components: {
FloatingMenu,
Expand All @@ -344,6 +378,7 @@ const MenuList = defineComponent({
CheckboxInput,
UserInputLabel,
LayoutRow,
LayoutCol,
},
});
export default MenuList;
Expand Down
121 changes: 64 additions & 57 deletions frontend/src/components/widgets/inputs/FontInput.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
<template>
<LayoutRow class="font-input">
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => !disabled && (open = true)" data-hover-menu-spawner>
<span>{{ activeEntry?.label || "" }}</span>
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" tabindex="0" @click="toggleOpen" @keydown="keydown" data-hover-menu-spawner>
<span>{{ activeEntry?.value || "" }}</span>
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
</LayoutRow>
<MenuList
ref="menulist"
v-model:activeEntry="activeEntry"
v-model:open="open"
@naturalWidth="(newNaturalWidth: number) => (minWidth = newNaturalWidth)"
:entries="entries"
:direction="'Bottom'"
:entries="[entries]"
:minWidth="isStyle ? 0 : minWidth"
:virtualScrollingEntryHeight="isStyle ? 0 : 20"
:scrollableY="true"
/>
@naturalWidth="(newNaturalWidth: number) => (isStyle && (minWidth = newNaturalWidth))"
></MenuList>
</LayoutRow>
</template>

Expand All @@ -26,21 +28,12 @@
height: 24px;
border-radius: 2px;

.dropdown-icon {
margin: 4px;
flex: 0 0 auto;
}

span {
margin: 0;
margin-left: 8px;
flex: 1 1 100%;
}

.dropdown-icon + span {
margin-left: 0;
}

.dropdown-arrow {
margin: 6px 2px;
flex: 0 0 auto;
Expand All @@ -53,10 +46,6 @@
span {
color: var(--color-f-white);
}

svg {
fill: var(--color-f-white);
}
}

&.open {
Expand All @@ -69,23 +58,23 @@
span {
color: var(--color-8-uppergray);
}

svg {
fill: var(--color-8-uppergray);
}
}
}

.menu-list .floating-menu-container .floating-menu-content {
max-height: 400px;
padding: 4px 0;
}
}
</style>

<script lang="ts">
import { defineComponent, PropType } from "vue";
import { defineComponent, nextTick, PropType } from "vue";

import MenuList, { MenuListEntry, SectionsOfMenuListEntries } from "@/components/floating-menus/MenuList.vue";
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
import MenuList, { MenuListEntry } from "@/components/floating-menus/MenuList.vue";

import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";

Expand All @@ -101,17 +90,42 @@ export default defineComponent({
data() {
return {
open: false,
minWidth: 0,
entries: [] as SectionsOfMenuListEntries,
activeEntry: undefined as undefined | MenuListEntry,
entries: [] as MenuListEntry[],
activeEntry: undefined as MenuListEntry | undefined,
highlighted: undefined as MenuListEntry | undefined,
entriesStart: 0,
minWidth: this.isStyle ? 0 : 300,
};
},
async mounted() {
const { entries, activeEntry } = await this.updateEntries();
this.entries = entries;
this.activeEntry = activeEntry;
this.entries = await this.getEntries();
this.activeEntry = this.getActiveEntry(this.entries);
this.highlighted = this.activeEntry;
},
methods: {
floatingMenu() {
return this.$refs.floatingMenu as typeof FloatingMenu;
},
scroller() {
return ((this.$refs.menulist as typeof MenuList).$refs.scroller as typeof LayoutCol)?.$el as HTMLElement;
},
async setOpen() {
this.open = true;
// Scroll to the active entry (the scroller div does not yet exist so we must wait for vue to render)
await nextTick();
if (this.activeEntry) {
const index = this.entries.indexOf(this.activeEntry);
this.scroller()?.scrollTo(0, Math.max(0, index * 20 - 190));
}
},
toggleOpen() {
if (this.disabled) return;
this.open = !this.open;
if (this.open) this.setOpen();
},
keydown(e: KeyboardEvent) {
(this.$refs.menulist as typeof MenuList).keydown(e, false);
},
async selectFont(newName: string): Promise<void> {
let fontFamily;
let fontStyle;
Expand All @@ -125,50 +139,43 @@ export default defineComponent({
this.$emit("update:fontFamily", newName);

fontFamily = newName;
fontStyle = (await this.fonts.getFontStyles(newName))[0];
fontStyle = "Normal (400)";
}

const fontFileUrl = await this.fonts.getFontFileUrl(fontFamily, fontStyle);
this.$emit("changeFont", { fontFamily, fontStyle, fontFileUrl });
},
async updateEntries(): Promise<{ entries: SectionsOfMenuListEntries; activeEntry: MenuListEntry }> {
const choices = this.isStyle ? await this.fonts.getFontStyles(this.fontFamily) : this.fonts.state.fontNames;
async getEntries(): Promise<MenuListEntry[]> {
const x = this.isStyle ? this.fonts.getFontStyles(this.fontFamily) : this.fonts.fontNames();
return (await x).map((entry: { name: string; url: URL | undefined }) => ({
label: entry.name,
value: entry.name,
font: entry.url,
action: () => this.selectFont(entry.name),
}));
},
getActiveEntry(entries: MenuListEntry[]): MenuListEntry {
const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;

let selectedEntry: MenuListEntry | undefined;
const menuListEntries = choices.map((name) => {
const result: MenuListEntry = {
label: name,
action: async (): Promise<void> => this.selectFont(name),
};

if (name === selectedChoice) selectedEntry = result;

return result;
});

const entries: SectionsOfMenuListEntries = [menuListEntries];
const activeEntry = selectedEntry || { label: "-" };

return { entries, activeEntry };
return entries.find((entry) => entry.value === selectedChoice) as MenuListEntry;
},
},
watch: {
async fontFamily() {
const { entries, activeEntry } = await this.updateEntries();
this.entries = entries;
this.activeEntry = activeEntry;
this.entries = await this.getEntries();
this.activeEntry = this.getActiveEntry(this.entries);
this.highlighted = this.activeEntry;
},
async fontStyle() {
const { entries, activeEntry } = await this.updateEntries();
this.entries = entries;
this.activeEntry = activeEntry;
this.entries = await this.getEntries();
this.activeEntry = this.getActiveEntry(this.entries);
this.highlighted = this.activeEntry;
},
},
components: {
LayoutRow,
IconLabel,
MenuList,
LayoutRow,
},
});
</script>
Loading