Make font selection show a live preview on hover; move its code to the backend (#3487)

* Remove FontInput.svelte

* Move font picking to the backend

* Fix Text tool font choice style turning to "-" on font that doesn't support previous style
This commit is contained in:
Keavon Chambers
2025-12-19 22:17:28 -08:00
committed by GitHub
parent 6f087eb981
commit 2d6d054359
31 changed files with 766 additions and 702 deletions
@@ -30,6 +30,7 @@
export let parentsValuePath: string[] = [];
export let entries: MenuListEntry[][];
export let entriesHash: bigint;
export let activeEntry: MenuListEntry | undefined = undefined;
export let open: boolean;
export let direction: MenuDirection = "Bottom";
@@ -37,26 +38,29 @@
export let drawIcon = false;
export let interactive = false;
export let scrollableY = false;
export let virtualScrollingEntryHeight = 0;
export let virtualScrolling = false;
// Keep the child references outside of the entries array so as to avoid infinite recursion.
let childReferences: MenuList[][] = [];
let search = "";
let reactiveEntries = entries;
let highlighted = activeEntry as MenuListEntry | undefined;
let virtualScrollingEntriesStart = 0;
// Called only when `open` is changed from outside this component
// `watchOpen` is called only when `open` is changed from outside this component
$: watchOpen(open);
$: watchEntries(entries);
$: watchEntriesHash(entriesHash);
$: watchRemeasureWidth(filteredEntries, drawIcon);
$: watchHighlightedWithSearch(filteredEntries, open);
$: filteredEntries = entries.map((section) => section.filter((entry) => inSearch(search, entry)));
$: virtualScrollingTotalHeight = filteredEntries.length === 0 ? 0 : filteredEntries[0].length * virtualScrollingEntryHeight;
$: virtualScrollingStartIndex = Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
$: virtualScrollingEndIndex = filteredEntries.length === 0 ? 0 : Math.min(filteredEntries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
$: virtualScrollingEntryHeight = virtualScrolling ? 20 : 0;
$: filteredEntries = reactiveEntries.map((section) => section.filter((entry) => inSearch(search, entry)));
$: startIndex = virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0;
// Virtual scrolling calculations
$: virtualScrollingTotalHeight = filteredEntries.length === 0 ? 0 : filteredEntries[0].length * virtualScrollingEntryHeight;
$: virtualScrollingStartIndex = filteredEntries.length === 0 ? 0 : Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
$: virtualScrollingEndIndex = filteredEntries.length === 0 ? 0 : Math.min(filteredEntries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
// TODO: Move keyboard input handling entirely to the unified system in `input.ts`.
// TODO: The current approach is hacky and blocks the allowances for shortcuts like the key to open the browser's dev tools.
@@ -139,6 +143,10 @@
});
}
function watchEntriesHash(_entriesHash: bigint) {
reactiveEntries = entries;
}
function watchRemeasureWidth(_: MenuListEntry[][], __: boolean) {
self?.measureAndEmitNaturalWidth();
}
@@ -149,23 +157,31 @@
}
function getChildReference(menuListEntry: MenuListEntry): MenuList | undefined {
const index = filteredEntries.flat().indexOf(menuListEntry);
return childReferences.flat().filter((x) => x)[index];
const index = filteredEntries.flat().findIndex((entry) => entry.value === menuListEntry.value);
if (index !== -1) {
return childReferences.flat().filter((x) => x)[index];
} else {
// eslint-disable-next-line no-console
console.error("MenuListEntry not found in filteredEntries:", menuListEntry);
return undefined;
}
}
function onEntryClick(menuListEntry: MenuListEntry) {
// Notify the parent about the clicked entry as the new active entry
dispatch("activeEntry", menuListEntry);
dispatch("selectedEntryValuePath", [...parentsValuePath, menuListEntry.value]);
// Close the containing menu
let childReference = getChildReference(menuListEntry);
if (childReference) {
childReference.open = false;
entries = entries;
reactiveEntries = reactiveEntries;
}
dispatch("open", false);
open = false;
reactiveEntries = reactiveEntries;
// Notify the parent about the clicked entry as the new active entry
dispatch("activeEntry", menuListEntry);
dispatch("selectedEntryValuePath", [...parentsValuePath, menuListEntry.value]);
}
function onEntryPointerEnter(menuListEntry: MenuListEntry) {
@@ -177,8 +193,10 @@
let childReference = getChildReference(menuListEntry);
if (childReference) {
childReference.open = true;
entries = entries;
} else dispatch("open", true);
reactiveEntries = reactiveEntries;
} else {
dispatch("open", true);
}
}
function onEntryPointerLeave(menuListEntry: MenuListEntry) {
@@ -190,8 +208,10 @@
let childReference = getChildReference(menuListEntry);
if (childReference) {
childReference.open = false;
entries = entries;
} else dispatch("open", false);
reactiveEntries = reactiveEntries;
} else {
dispatch("open", false);
}
}
function isEntryOpen(menuListEntry: MenuListEntry): boolean {
@@ -365,7 +385,7 @@
let container = scroller?.div?.();
if (!container || !highlighted) return;
let containerBoundingRect = container.getBoundingClientRect();
let highlightedIndex = filteredEntries.flat().findIndex((entry) => entry === highlighted);
let highlightedIndex = filteredEntries.flat().findIndex((entry) => entry.value === highlighted?.value);
let selectedBoundingRect = new DOMRect();
if (virtualScrollingEntryHeight) {
@@ -386,10 +406,6 @@
container.scrollBy(0, selectedBoundingRect.y - (containerBoundingRect.y + containerBoundingRect.height) + selectedBoundingRect.height);
}
}
export function scrollViewTo(distanceDown: number) {
scroller?.div?.()?.scrollTo(0, distanceDown);
}
</script>
<FloatingMenu
@@ -419,8 +435,8 @@
{#if virtualScrollingEntryHeight}
<LayoutRow class="scroll-spacer" styles={{ height: `${virtualScrollingStartIndex * virtualScrollingEntryHeight}px` }} />
{/if}
{#each entries as section, sectionIndex (sectionIndex)}
{#if includeSeparator(entries, section, sectionIndex, search)}
{#each reactiveEntries as section, sectionIndex (sectionIndex)}
{#if includeSeparator(reactiveEntries, section, sectionIndex, search)}
<Separator type="Section" direction="Vertical" />
{/if}
{#each currentEntries(section, virtualScrollingEntryHeight, virtualScrollingStartIndex, virtualScrollingEndIndex, search) as entry, entryIndex (entryIndex + startIndex)}
@@ -442,10 +458,10 @@
{/if}
{#if entry.font}
<link rel="stylesheet" href={entry.font?.toString()} />
<link rel="stylesheet" href={entry.font} />
{/if}
<TextLabel class="entry-label" styles={{ "font-family": `${!entry.font ? "inherit" : entry.value}` }}>{entry.label}</TextLabel>
<TextLabel class="entry-label" styles={entry.font ? { "font-family": entry.value } : {}}>{entry.label}</TextLabel>
{#if entry.tooltipShortcut?.shortcut.length}
<ShortcutLabel shortcut={entry.tooltipShortcut} />
@@ -470,6 +486,7 @@
open={getChildReference(entry)?.open || false}
direction="TopRight"
entries={entry.children}
entriesHash={entry.childrenHash || 0n}
{minWidth}
{drawIcon}
{scrollableY}