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
+2 -3
View File
@@ -11,7 +11,7 @@
import { createAppWindowState } from "@graphite/state-providers/app-window";
import { createDialogState } from "@graphite/state-providers/dialog";
import { createDocumentState } from "@graphite/state-providers/document";
import { createFontsState } from "@graphite/state-providers/fonts";
import { createFontsManager } from "/src/io-managers/fonts";
import { createFullscreenState } from "@graphite/state-providers/fullscreen";
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
import { createPortfolioState } from "@graphite/state-providers/portfolio";
@@ -31,8 +31,6 @@
setContext("tooltip", tooltip);
let document = createDocumentState(editor);
setContext("document", document);
let fonts = createFontsState(editor);
setContext("fonts", fonts);
let fullscreen = createFullscreenState(editor);
setContext("fullscreen", fullscreen);
let nodeGraph = createNodeGraphState(editor);
@@ -48,6 +46,7 @@
createLocalizationManager(editor);
createPanicManager(editor, dialog);
createPersistenceManager(editor, portfolio);
createFontsManager(editor);
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);
onMount(() => {
@@ -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}
+17 -3
View File
@@ -6,6 +6,7 @@
type MouseCursorIcon,
type XY,
DisplayEditableTextbox,
DisplayEditableTextboxUpdateFontData,
DisplayEditableTextboxTransform,
DisplayRemoveEditableTextbox,
TriggerTextCommit,
@@ -360,10 +361,14 @@
if (!textInput) return;
editor.handle.updateBounds(textInputCleanup(textInput.innerText));
};
textInputMatrix = displayEditableTextbox.transform;
const newFont = new FontFace("text-font", `url(${displayEditableTextbox.url})`);
window.document.fonts.add(newFont);
textInput.style.fontFamily = "text-font";
const data = new Uint8Array(displayEditableTextbox.fontData);
if (data.length > 0) {
window.document.fonts.add(new FontFace("text-font", data));
textInput.style.fontFamily = "text-font";
}
// Necessary to select contenteditable: https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element/6150060#6150060
@@ -471,6 +476,15 @@
displayEditableTextbox(data);
});
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxUpdateFontData, async (data) => {
await tick();
const fontData = new Uint8Array(data.fontData);
if (fontData.length > 0 && textInput) {
window.document.fonts.add(new FontFace("text-font", fontData));
textInput.style.fontFamily = "text-font";
}
});
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxTransform, async (data) => {
textInputMatrix = data.transform;
});
+1 -1
View File
@@ -627,7 +627,7 @@
data-tooltip-description={(listing.entry.expanded
? "Hide the layers nested within. (To affect all open descendants, perform the shortcut shown.)"
: "Show the layers nested within. (To affect all closed descendants, perform the shortcut shown.)") +
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nNote: a selected layer is currently contained within.\n" : "")}
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nA selected layer is currently contained within.\n" : "")}
data-tooltip-shortcut={altClickShortcut?.shortcut ? JSON.stringify(altClickShortcut.shortcut) : undefined}
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
tabindex="0"
@@ -17,7 +17,6 @@
import ColorInput from "@graphite/components/widgets/inputs/ColorInput.svelte";
import CurveInput from "@graphite/components/widgets/inputs/CurveInput.svelte";
import DropdownInput from "@graphite/components/widgets/inputs/DropdownInput.svelte";
import FontInput from "@graphite/components/widgets/inputs/FontInput.svelte";
import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte";
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
import ReferencePointInput from "@graphite/components/widgets/inputs/ReferencePointInput.svelte";
@@ -61,16 +60,16 @@
return widgets;
}
function widgetValueCommit(index: number, value: unknown) {
editor.handle.widgetValueCommit(layoutTarget, widgets[index].widgetId, value);
function widgetValueCommit(widgetIndex: number, value: unknown) {
editor.handle.widgetValueCommit(layoutTarget, widgets[widgetIndex].widgetId, value);
}
function widgetValueUpdate(index: number, value: unknown) {
editor.handle.widgetValueUpdate(layoutTarget, widgets[index].widgetId, value);
function widgetValueUpdate(widgetIndex: number, value: unknown, resendWidget: boolean) {
editor.handle.widgetValueUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
}
function widgetValueCommitAndUpdate(index: number, value: unknown) {
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[index].widgetId, value);
function widgetValueCommitAndUpdate(widgetIndex: number, value: unknown, resendWidget: boolean) {
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
}
// TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283
@@ -85,43 +84,47 @@
<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components -->
<div class={`widget-span ${className} ${extraClasses}`.trim()} class:narrow class:row={direction === "row"} class:column={direction === "column"}>
{#each widgets as component, index}
{#each widgets as component, widgetIndex}
{@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")}
{#if checkboxInput}
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
{/if}
{@const colorInput = narrowWidgetProps(component.props, "ColorInput")}
{#if colorInput}
<ColorInput {...exclude(colorInput)} on:value={({ detail }) => widgetValueUpdate(index, detail)} on:startHistoryTransaction={() => widgetValueCommit(index, colorInput.value)} />
<ColorInput
{...exclude(colorInput)}
on:value={({ detail }) => widgetValueUpdate(widgetIndex, detail, false)}
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, colorInput.value)}
/>
{/if}
<!-- TODO: Curves Input is currently unused -->
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
{#if curvesInput}
<CurveInput {...exclude(curvesInput)} on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(index, value), { debounceTime: 120 }).debounceUpdateValue(detail)} />
<CurveInput
{...exclude(curvesInput)}
on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(widgetIndex, value, false), { debounceTime: 120 }).debounceUpdateValue(detail)}
/>
{/if}
{@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")}
{#if dropdownInput}
<DropdownInput
{...exclude(dropdownInput)}
on:hoverInEntry={({ detail }) => {
return widgetValueUpdate(index, detail);
return widgetValueUpdate(widgetIndex, detail, false);
}}
on:hoverOutEntry={({ detail }) => {
return widgetValueUpdate(index, detail);
return widgetValueUpdate(widgetIndex, detail, false);
}}
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)}
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)}
/>
{/if}
{@const fontInput = narrowWidgetProps(component.props, "FontInput")}
{#if fontInput}
<FontInput {...exclude(fontInput)} on:changeFont={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
{/if}
{@const parameterExposeButton = narrowWidgetProps(component.props, "ParameterExposeButton")}
{#if parameterExposeButton}
<ParameterExposeButton {...exclude(parameterExposeButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
<ParameterExposeButton {...exclude(parameterExposeButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
{/if}
{@const iconButton = narrowWidgetProps(component.props, "IconButton")}
{#if iconButton}
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
{/if}
{@const iconLabel = narrowWidgetProps(component.props, "IconLabel")}
{#if iconLabel}
@@ -138,25 +141,25 @@
{/if}
{@const imageButton = narrowWidgetProps(component.props, "ImageButton")}
{#if imageButton}
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
{/if}
{@const nodeCatalog = narrowWidgetProps(component.props, "NodeCatalog")}
{#if nodeCatalog}
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(index, e.detail)} />
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(widgetIndex, e.detail, false)} />
{/if}
{@const numberInput = narrowWidgetProps(component.props, "NumberInput")}
{#if numberInput}
<NumberInput
{...exclude(numberInput)}
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(index, value)).debounceUpdateValue(detail)}
on:startHistoryTransaction={() => widgetValueCommit(index, numberInput.value)}
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(index, "Increment")}
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(index, "Decrement")}
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(widgetIndex, value, true)).debounceUpdateValue(detail)}
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, numberInput.value)}
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(widgetIndex, "Increment", false)}
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(widgetIndex, "Decrement", false)}
/>
{/if}
{@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")}
{#if referencePointInput}
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
{/if}
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
{#if popoverButton}
@@ -166,7 +169,7 @@
{/if}
{@const radioInput = narrowWidgetProps(component.props, "RadioInput")}
{#if radioInput}
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
{/if}
{@const separator = narrowWidgetProps(component.props, "Separator")}
{#if separator}
@@ -178,19 +181,23 @@
{/if}
{@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")}
{#if textAreaInput}
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)} />
{/if}
{@const textButton = narrowWidgetProps(component.props, "TextButton")}
{#if textButton}
<TextButton {...exclude(textButton)} action={() => widgetValueCommitAndUpdate(index, [])} on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
<TextButton
{...exclude(textButton)}
action={() => widgetValueCommitAndUpdate(widgetIndex, [], true)}
on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)}
/>
{/if}
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
{#if breadcrumbTrailButtons}
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(index, breadcrumbIndex)} />
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(widgetIndex, breadcrumbIndex, true)} />
{/if}
{@const textInput = narrowWidgetProps(component.props, "TextInput")}
{#if textInput}
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
{/if}
{@const textLabel = narrowWidgetProps(component.props, "TextLabel")}
{#if textLabel}
@@ -27,6 +27,7 @@
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
export let menuListChildren: MenuListEntry[][] | undefined = undefined;
export let menuListChildrenHash: bigint | undefined = undefined;
// Callbacks
// TODO: Replace this with an event binding (and on other components that do this)
@@ -90,6 +91,7 @@
on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)}
open={self?.open || false}
entries={menuListChildren || []}
entriesHash={menuListChildrenHash || 0n}
direction="Bottom"
minWidth={240}
drawIcon={true}
@@ -12,15 +12,16 @@
const dispatch = createEventDispatcher<{ selectedIndex: number; hoverInEntry: number; hoverOutEntry: number }>();
let menuList: MenuList | undefined;
let self: LayoutRow | undefined;
export let entries: MenuListEntry[][];
export let entriesHash: bigint | undefined = undefined;
export let selectedIndex: number | undefined = undefined; // When not provided, a dash is displayed
export let drawIcon = false;
export let interactive = true;
export let disabled = false;
export let narrow = false;
export let virtualScrolling = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
@@ -53,19 +54,32 @@
activeEntry = makeActiveEntry();
}
// Called when the `activeEntry` two-way binding on this component's MenuList component is changed, or by the `selectedIndex()` watcher above (but we want to skip that case)
// Called when the `activeEntry` two-way binding on this component's MenuList component is changed, or by the `watchSelectedIndex()` watcher above (but we want to skip that case)
function watchActiveEntry(activeEntry: MenuListEntry) {
if (activeEntrySkipWatcher) {
activeEntrySkipWatcher = false;
} else if (activeEntry !== DASH_ENTRY) {
// We need to set to the initial value first to track a right history step, as if we hover in initial selection.
if (initialSelectedIndex !== undefined) dispatch("hoverInEntry", initialSelectedIndex);
dispatch("selectedIndex", entries.flat().indexOf(activeEntry));
const index = entries.flat().findIndex((entry) => entry.value === activeEntry.value);
if (index !== -1) {
dispatch("selectedIndex", index);
} else {
// eslint-disable-next-line no-console
console.error("Selected index not found in entries:", activeEntry);
}
}
}
function dispatchHoverInEntry(hoveredEntry: MenuListEntry) {
dispatch("hoverInEntry", entries.flat().indexOf(hoveredEntry));
const index = entries.flat().findIndex((entry) => entry.value === hoveredEntry.value);
if (index !== -1) {
dispatch("hoverInEntry", index);
} else {
// eslint-disable-next-line no-console
console.error("Hovered entry not found in entries:", hoveredEntry);
}
}
function dispatchHoverOutEntry() {
@@ -123,11 +137,12 @@
{open}
{activeEntry}
{entries}
entriesHash={entriesHash || 0n}
{drawIcon}
{interactive}
{virtualScrolling}
direction="Bottom"
scrollableY={true}
bind:this={menuList}
/>
</LayoutRow>
@@ -1,186 +0,0 @@
<script lang="ts">
import { createEventDispatcher, getContext, onMount, tick } from "svelte";
import type { MenuListEntry, ActionShortcut } from "@graphite/messages";
import type { FontsState } from "@graphite/state-providers/fonts";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const fonts = getContext<FontsState>("fonts");
const dispatch = createEventDispatcher<{
fontFamily: string;
fontStyle: string;
changeFont: { fontFamily: string; fontStyle: string; fontFileUrl: string | undefined };
}>();
let menuList: MenuList | undefined;
export let fontFamily: string;
export let fontStyle: string;
export let isStyle = false;
export let disabled = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
let open = false;
let entries: MenuListEntry[] = [];
let activeEntry: MenuListEntry | undefined = undefined;
let minWidth = isStyle ? 0 : 300;
$: watchFont(fontFamily, fontStyle);
async function watchFont(..._: string[]) {
// We set this function's result to a local variable to avoid reading from `entries` which causes Svelte to trigger an update that results in an infinite loop
const newEntries = await getEntries();
entries = newEntries;
activeEntry = getActiveEntry(newEntries);
}
async function setOpen() {
open = true;
// Scroll to the active entry (the scroller div does not yet exist so we must wait for the component to render)
await tick();
if (activeEntry) {
const index = entries.indexOf(activeEntry);
menuList?.scrollViewTo(Math.max(0, index * 20 - 190));
}
}
function toggleOpen() {
if (!disabled) {
open = !open;
if (open) setOpen();
}
}
async function selectFont(newName: string) {
let family;
let style;
if (isStyle) {
dispatch("fontStyle", newName);
family = fontFamily;
style = newName;
} else {
dispatch("fontFamily", newName);
family = newName;
style = "Regular (400)";
}
const fontFileUrl = await fonts.getFontFileUrl(family, style);
dispatch("changeFont", { fontFamily: family, fontStyle: style, fontFileUrl });
}
async function getEntries(): Promise<MenuListEntry[]> {
const x = isStyle ? fonts.getFontStyles(fontFamily) : fonts.fontNames();
return (await x).map((entry: { name: string; url: URL | undefined }) => ({
value: entry.name,
label: entry.name,
font: entry.url,
action: () => selectFont(entry.name),
}));
}
function getActiveEntry(entries: MenuListEntry[]): MenuListEntry {
const selectedChoice = isStyle ? fontStyle : fontFamily;
return entries.find((entry) => entry.value === selectedChoice) as MenuListEntry;
}
onMount(async () => {
entries = await getEntries();
activeEntry = getActiveEntry(entries);
});
</script>
<!-- TODO: Combine this widget into the DropdownInput widget -->
<LayoutRow class="font-input">
<LayoutRow
class="dropdown-box"
classes={{ disabled }}
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
tabindex={disabled ? -1 : 0}
on:click={toggleOpen}
data-floating-menu-spawner
>
<TextLabel class="dropdown-label">{activeEntry?.value || ""}</TextLabel>
<IconLabel class="dropdown-arrow" icon="DropdownArrow" />
</LayoutRow>
<MenuList
on:naturalWidth={({ detail }) => isStyle && (minWidth = detail)}
{activeEntry}
on:activeEntry={({ detail }) => (activeEntry = detail)}
{open}
on:open={({ detail }) => (open = detail)}
entries={[entries]}
minWidth={isStyle ? 0 : minWidth}
virtualScrollingEntryHeight={isStyle ? 0 : 20}
scrollableY={true}
bind:this={menuList}
/>
</LayoutRow>
<style lang="scss" global>
.font-input {
position: relative;
.dropdown-box {
align-items: center;
white-space: nowrap;
background: var(--color-1-nearblack);
height: 24px;
border-radius: 2px;
.dropdown-label {
margin: 0;
margin-left: 8px;
flex: 1 1 100%;
}
.dropdown-arrow {
margin: 6px 2px;
flex: 0 0 auto;
}
&:hover,
&.open {
background: var(--color-6-lowergray);
.text-label {
color: var(--color-f-white);
}
}
&.open {
border-radius: 2px 2px 0 0;
}
&.disabled {
background: var(--color-2-mildblack);
.text-label {
color: var(--color-8-uppergray);
}
}
}
.menu-list .floating-menu-container .floating-menu-content {
max-height: 400px;
padding: 4px 0;
}
}
</style>