mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 07:18:04 +08:00
* 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
209 lines
5.8 KiB
Svelte
209 lines
5.8 KiB
Svelte
<script lang="ts">
|
|
import { createEventDispatcher } from "svelte";
|
|
|
|
import type { MenuListEntry, ActionShortcut } from "@graphite/messages";
|
|
|
|
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 DASH_ENTRY = { value: "", label: "-" };
|
|
|
|
const dispatch = createEventDispatcher<{ selectedIndex: number; hoverInEntry: number; hoverOutEntry: number }>();
|
|
|
|
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;
|
|
export let minWidth = 0;
|
|
export let maxWidth = 0;
|
|
|
|
let activeEntry = makeActiveEntry();
|
|
let activeEntrySkipWatcher = false;
|
|
let initialSelectedIndex: number | undefined = undefined;
|
|
let open = false;
|
|
|
|
$: watchSelectedIndex(selectedIndex);
|
|
$: watchEntries(entries);
|
|
$: watchActiveEntry(activeEntry);
|
|
$: watchOpen(open);
|
|
|
|
function watchOpen(open: boolean) {
|
|
initialSelectedIndex = open ? selectedIndex : undefined;
|
|
}
|
|
|
|
// Called only when `selectedIndex` is changed from outside this component
|
|
function watchSelectedIndex(_?: typeof selectedIndex) {
|
|
activeEntrySkipWatcher = true;
|
|
activeEntry = makeActiveEntry();
|
|
}
|
|
|
|
// Called only when `entries` is changed from outside this component
|
|
function watchEntries(_?: typeof entries) {
|
|
activeEntrySkipWatcher = true;
|
|
activeEntry = makeActiveEntry();
|
|
}
|
|
|
|
// 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);
|
|
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) {
|
|
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() {
|
|
if (initialSelectedIndex !== undefined) dispatch("hoverOutEntry", initialSelectedIndex);
|
|
}
|
|
|
|
function makeActiveEntry(): MenuListEntry {
|
|
const allEntries = entries.flat();
|
|
|
|
if (selectedIndex !== undefined && selectedIndex >= 0 && selectedIndex < allEntries.length) {
|
|
return allEntries[selectedIndex];
|
|
}
|
|
return DASH_ENTRY;
|
|
}
|
|
|
|
function unFocusDropdownBox(e: FocusEvent) {
|
|
const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]") || undefined;
|
|
if (blurTarget !== self?.div?.()) open = false;
|
|
}
|
|
</script>
|
|
|
|
<LayoutRow
|
|
class="dropdown-input"
|
|
classes={{ narrow }}
|
|
styles={{
|
|
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
|
|
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
|
|
}}
|
|
bind:this={self}
|
|
data-dropdown-input
|
|
>
|
|
<LayoutRow
|
|
class="dropdown-box"
|
|
classes={{ disabled, open }}
|
|
{tooltipLabel}
|
|
{tooltipDescription}
|
|
{tooltipShortcut}
|
|
on:click={() => !disabled && (open = true)}
|
|
on:blur={unFocusDropdownBox}
|
|
tabindex={disabled ? -1 : 0}
|
|
data-floating-menu-spawner
|
|
>
|
|
{#if activeEntry.icon}
|
|
<IconLabel class="dropdown-icon" icon={activeEntry.icon} />
|
|
{/if}
|
|
<TextLabel class="dropdown-label">{activeEntry.label}</TextLabel>
|
|
<IconLabel class="dropdown-arrow" icon="DropdownArrow" />
|
|
</LayoutRow>
|
|
<MenuList
|
|
on:naturalWidth={({ detail }) => (minWidth = detail)}
|
|
on:activeEntry={({ detail }) => (activeEntry = detail)}
|
|
on:hoverInEntry={({ detail }) => dispatchHoverInEntry(detail)}
|
|
on:hoverOutEntry={() => dispatchHoverOutEntry()}
|
|
on:open={({ detail }) => (open = detail)}
|
|
{open}
|
|
{activeEntry}
|
|
{entries}
|
|
entriesHash={entriesHash || 0n}
|
|
{drawIcon}
|
|
{interactive}
|
|
{virtualScrolling}
|
|
direction="Bottom"
|
|
scrollableY={true}
|
|
/>
|
|
</LayoutRow>
|
|
|
|
<style lang="scss" global>
|
|
.dropdown-input {
|
|
position: relative;
|
|
--widget-height: 24px;
|
|
|
|
&.narrow.narrow {
|
|
--widget-height: 20px;
|
|
}
|
|
|
|
.dropdown-box {
|
|
align-items: center;
|
|
white-space: nowrap;
|
|
border-radius: 2px;
|
|
background: var(--color-1-nearblack);
|
|
height: var(--widget-height);
|
|
|
|
.dropdown-label {
|
|
margin: 0;
|
|
margin-left: 8px;
|
|
flex: 1 1 100%;
|
|
}
|
|
|
|
.dropdown-icon {
|
|
margin: 4px 8px;
|
|
flex: 0 0 auto;
|
|
|
|
& + .dropdown-label {
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
|
|
.dropdown-arrow {
|
|
margin: 4px;
|
|
margin-right: 2px;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
&:hover,
|
|
&.open {
|
|
background: var(--color-4-dimgray);
|
|
}
|
|
|
|
&.disabled {
|
|
background: var(--color-2-mildblack);
|
|
|
|
.text-label {
|
|
color: var(--color-8-uppergray);
|
|
}
|
|
|
|
svg {
|
|
fill: var(--color-8-uppergray);
|
|
}
|
|
}
|
|
}
|
|
|
|
.menu-list .floating-menu-container .floating-menu-content {
|
|
max-height: 400px;
|
|
}
|
|
}
|
|
</style>
|