Restyle and refactor shortcut labels to send hints bar and welcome screen layouts from Rust (#3447)

* Restyle UserInputLabel and refactor its usages to have all input its data sent from Rust

* Replace the welcome screen quick buttons with ones sent by backend

* Add the ShortcutLabel widget to the backend

* Replace hints bar with a backend-controlled layout; show mouse icons in place of mouse labels
This commit is contained in:
Keavon Chambers
2025-12-04 01:04:14 -08:00
committed by GitHub
parent 6ed42d06bb
commit 810ce40e9b
78 changed files with 981 additions and 997 deletions
@@ -18,7 +18,7 @@
{:else if isWidgetSection(layoutGroup)}
<WidgetSection widgetData={layoutGroup} layoutTarget={layout.layoutTarget} class={className} {classes} />
{:else if isWidgetTable(layoutGroup)}
<WidgetTable widgetData={layoutGroup} layoutTarget={layout.layoutTarget} />
<WidgetTable widgetData={layoutGroup} unstyled={layoutGroup.unstyled} layoutTarget={layout.layoutTarget} />
{:else}
<TextLabel styles={{ color: "#d6536e" }}>Error: The widget layout that belongs here has an invalid layout group type</TextLabel>
{/if}
@@ -27,6 +27,7 @@
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import ImageLabel from "@graphite/components/widgets/labels/ImageLabel.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import ShortcutLabel from "@graphite/components/widgets/labels/ShortcutLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
@@ -127,6 +128,11 @@
{#if iconLabel}
<IconLabel {...exclude(iconLabel)} />
{/if}
{@const shortcutLabel = narrowWidgetProps(component.props, "ShortcutLabel")}
{@const shortcutLabelShortcut = shortcutLabel?.shortcut ? { ...shortcutLabel, shortcut: shortcutLabel.shortcut } : undefined}
{#if shortcutLabel && shortcutLabelShortcut}
<ShortcutLabel {...exclude(shortcutLabelShortcut)} />
{/if}
{@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")}
{#if imageLabel}
<ImageLabel {...exclude(imageLabel)} />
@@ -5,15 +5,18 @@
export let widgetData: WidgetTableFromJsMessages;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let layoutTarget: any; // TODO: Give this a real type
export let layoutTarget: any;
export let unstyled = false;
$: columns = widgetData.tableWidgets.length > 0 ? widgetData.tableWidgets[0].length : 0;
</script>
<table>
<table class:unstyled>
<tbody>
{#each widgetData.tableWidgets as row}
<tr>
{#each row as cell}
<td>
<td colspan={row.length < columns ? columns - row.length + 1 : undefined}>
<WidgetSpan widgetData={{ rowWidgets: [cell] }} {layoutTarget} narrow={true} />
</td>
{/each}
@@ -23,7 +26,7 @@
</table>
<style lang="scss">
table {
table:not(.unstyled) {
background: var(--color-3-darkgray);
border: none;
border-spacing: 4px;
@@ -1,4 +1,6 @@
<script lang="ts">
import type { ActionShortcut } from "@graphite/messages";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
@@ -6,7 +8,7 @@
export let disabled = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
// Callbacks
export let action: (index: number) => void;
</script>
@@ -1,5 +1,6 @@
<script lang="ts">
import { type IconName, type IconSize } from "@graphite/icons";
import type { ActionShortcut } from "@graphite/messages";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
@@ -10,7 +11,7 @@
export let active = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
// Callbacks
export let action: (e?: MouseEvent) => void;
@@ -32,7 +33,7 @@
{disabled}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
tabindex={active ? -1 : 0}
{...$$restProps}
>
@@ -1,4 +1,5 @@
<script lang="ts">
import type { ActionShortcut } from "@graphite/messages";
import { IMAGE_BASE64_STRINGS } from "@graphite/utility-functions/images";
let className = "";
@@ -10,7 +11,7 @@
export let height: string | undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
// Callbacks
export let action: (e?: MouseEvent) => void;
@@ -26,7 +27,7 @@
class={`image-button ${className} ${extraClasses}`.trim()}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
alt=""
on:click={action}
/>
@@ -1,5 +1,5 @@
<script lang="ts">
import type { FrontendGraphDataType } from "@graphite/messages";
import type { FrontendGraphDataType, ActionShortcut } from "@graphite/messages";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -7,7 +7,7 @@
export let dataType: FrontendGraphDataType;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
// Callbacks
export let action: (e?: MouseEvent) => void;
</script>
@@ -20,7 +20,7 @@
on:click={action}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
tabindex="-1"
>
{#if !exposed}
@@ -1,7 +1,7 @@
<script lang="ts">
import { type IconName, type PopoverButtonStyle } from "@graphite/icons";
import type { MenuDirection } from "@graphite/messages";
import type { MenuDirection, ActionShortcut } from "@graphite/messages";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -13,7 +13,7 @@
export let icon: IconName | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
export let disabled = false;
export let popoverMinWidth = 1;
@@ -2,7 +2,7 @@
import { createEventDispatcher } from "svelte";
import type { IconName } from "@graphite/icons";
import type { MenuListEntry } from "@graphite/messages";
import type { MenuListEntry, ActionShortcut } from "@graphite/messages";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
import ConditionalWrapper from "@graphite/components/layout/ConditionalWrapper.svelte";
@@ -25,7 +25,7 @@
export let narrow = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
export let menuListChildren: MenuListEntry[][] | undefined = undefined;
// Callbacks
@@ -66,7 +66,7 @@
style:min-width={minWidth > 0 ? `${minWidth}px` : undefined}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
data-emphasized={emphasized || undefined}
data-disabled={disabled || undefined}
data-text-button
@@ -2,6 +2,7 @@
import { createEventDispatcher } from "svelte";
import type { IconName } from "@graphite/icons";
import type { ActionShortcut } from "@graphite/messages";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
@@ -13,7 +14,7 @@
export let icon: IconName = "Checkmark";
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
export let forLabel: bigint | undefined = undefined;
let inputElement: HTMLInputElement | undefined;
@@ -55,7 +56,7 @@
on:keydown={(e) => e.key === "Enter" && toggleCheckboxFromLabel(e)}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
>
<LayoutRow class="checkbox-box">
<IconLabel icon={displayIcon} />
@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { FillChoice, MenuDirection } from "@graphite/messages";
import type { FillChoice, MenuDirection, ActionShortcut } from "@graphite/messages";
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
@@ -19,7 +19,7 @@
// export let allowTransparency = false; // TODO: Implement
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
$: outlined = outlineFactor > 0.0001;
@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Curve, CurveManipulatorGroup } from "@graphite/messages";
import type { Curve, CurveManipulatorGroup, ActionShortcut } from "@graphite/messages";
import { clamp } from "@graphite/utility-functions/math";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -18,7 +18,7 @@
export let disabled = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
const GRID_SIZE = 4;
@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { MenuListEntry } from "@graphite/messages";
import type { MenuListEntry, ActionShortcut } from "@graphite/messages";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -23,7 +23,7 @@
export let narrow = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
export let minWidth = 0;
export let maxWidth = 0;
@@ -1,6 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { ActionShortcut } from "@graphite/messages";
import { operatingSystem } from "@graphite/utility-functions/platform";
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
@@ -27,7 +28,7 @@
export let textarea = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let hideContextMenu = false;
@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher, getContext, onMount, tick } from "svelte";
import type { MenuListEntry } from "@graphite/messages";
import type { MenuListEntry, ActionShortcut } from "@graphite/messages";
import type { FontsState } from "@graphite/state-providers/fonts";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
@@ -25,7 +25,7 @@
export let disabled = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
let open = false;
let entries: MenuListEntry[] = [];
@@ -1,9 +1,9 @@
<script lang="ts">
import { createEventDispatcher, onMount, onDestroy } from "svelte";
import { evaluateMathExpression } from "@graphite/../wasm/pkg/graphite_wasm.js";
import { evaluateMathExpression } from "@graphite/../wasm/pkg/graphite_wasm";
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input";
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@graphite/messages";
import type { NumberInputMode, NumberInputIncrementBehavior, ActionShortcut } from "@graphite/messages";
import { browserVersion, isDesktop } from "@graphite/utility-functions/platform";
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
@@ -20,7 +20,7 @@
export let label: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
// Disabled
export let disabled = false;
@@ -30,7 +30,7 @@
on:click={() => handleEntryClick(entry)}
data-tooltip-label={entry.tooltipLabel}
data-tooltip-description={entry.tooltipDescription}
data-tooltip-shortcut={entry.tooltipShortcut}
data-tooltip-shortcut={entry.tooltipShortcut?.shortcut ? JSON.stringify(entry.tooltipShortcut.shortcut) : undefined}
tabindex={index === selectedIndex ? -1 : 0}
{disabled}
>
@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { ReferencePoint } from "@graphite/messages";
import type { ReferencePoint, ActionShortcut } from "@graphite/messages";
const dispatch = createEventDispatcher<{ value: ReferencePoint }>();
@@ -9,14 +9,20 @@
export let disabled = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
function setValue(newValue: ReferencePoint) {
dispatch("value", newValue);
}
</script>
<div class="reference-point-input" class:disabled data-tooltip-label={tooltipLabel} data-tooltip-description={tooltipDescription} data-tooltip-shortcut={tooltipShortcut}>
<div
class="reference-point-input"
class:disabled
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
>
<button on:click={() => setValue("TopLeft")} class="row-1 col-1" class:active={value === "TopLeft"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("TopCenter")} class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("TopRight")} class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div /></button>
@@ -18,7 +18,7 @@
// export let disabled = false;
// export let tooltipLabel: string | undefined = undefined;
// export let tooltipDescription: string | undefined = undefined;
// export let tooltipShortcut: string | undefined = undefined;
// export let tooltipShortcut: ActionShortcut | undefined = undefined;
let markerTrack: LayoutRow | undefined = undefined;
let positionRestore: number | undefined = undefined;
@@ -1,6 +1,8 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { ActionShortcut } from "@graphite/messages";
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
const dispatch = createEventDispatcher<{ commitText: string }>();
@@ -9,7 +11,7 @@
export let label: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
export let disabled = false;
let self: FieldInput | undefined;
@@ -1,6 +1,8 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { ActionShortcut } from "@graphite/messages";
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
const dispatch = createEventDispatcher<{ commitText: string }>();
@@ -9,7 +11,7 @@
export let label: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
export let placeholder: string | undefined = undefined;
// Disabled
export let disabled = false;
@@ -1,5 +1,6 @@
<script lang="ts">
import { type IconName, ICONS, ICON_SVG_STRINGS } from "@graphite/icons";
import type { ActionShortcut } from "@graphite/messages";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -11,7 +12,7 @@
export let disabled = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
$: iconSizeClass = ((icon: IconName) => {
const iconData = ICONS[icon];
@@ -1,4 +1,6 @@
<script lang="ts">
import type { ActionShortcut } from "@graphite/messages";
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
@@ -8,7 +10,7 @@
export let height: string | undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
$: extraClasses = Object.entries(classes)
.flatMap(([className, stateName]) => (stateName ? [className] : []))
@@ -22,7 +24,7 @@
class={`image-label ${className} ${extraClasses}`.trim()}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
alt=""
/>
@@ -0,0 +1,152 @@
<script lang="ts">
import type { IconName } from "@graphite/icons";
import type { ActionShortcut, KeyRaw, LabeledShortcut, MouseMotion } from "@graphite/messages";
import { operatingSystem } from "@graphite/utility-functions/platform";
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";
export let shortcut: ActionShortcut;
function keyTextOrIconList(keyGroup: LabeledShortcut): { label?: string; icon?: IconName; mouseMotion?: MouseMotion }[] {
const list = keyGroup.map((labeledKeyOrMouseMotion) => {
// Use a mouse icon if it's a mouse motion instead of a key
if (typeof labeledKeyOrMouseMotion === "string") return { mouseMotion: labeledKeyOrMouseMotion };
// `key` is the name of the `Key` enum in Rust, while `label` is the localized string to display (if it doesn't become an icon)
let key = labeledKeyOrMouseMotion.key;
const label = labeledKeyOrMouseMotion.label;
// Replace Alt and Accel keys with their Mac-specific equivalents
if (operatingSystem() === "Mac") {
if (key === "Alt") key = "Option";
if (key === "Accel") key = "Command";
}
// Either display an icon...
const icon = keyboardHintIcon(key);
if (icon) return { icon };
// ...or display text
return { label };
});
// Consolidate consecutive labels into a concatenated single label
const consolidatedList: typeof list = [];
list.forEach((item) => {
const lastItem = consolidatedList[consolidatedList.length - 1];
if (item.label && lastItem?.label) lastItem.label += " " + item.label;
else consolidatedList.push(item);
});
return consolidatedList;
}
function keyboardHintIcon(input: KeyRaw): IconName | undefined {
switch (input) {
case "ArrowDown":
return "KeyboardArrowDown";
case "ArrowLeft":
return "KeyboardArrowLeft";
case "ArrowRight":
return "KeyboardArrowRight";
case "ArrowUp":
return "KeyboardArrowUp";
case "Backspace":
return "KeyboardBackspace";
case "Enter":
return "KeyboardEnter";
case "Space":
return "KeyboardSpace";
case "Tab":
return "KeyboardTab";
case "Command":
return operatingSystem() === "Mac" ? "KeyboardCommand" : undefined;
case "Control":
return operatingSystem() === "Mac" ? "KeyboardControl" : undefined;
case "Option":
return operatingSystem() === "Mac" ? "KeyboardOption" : undefined;
case "Shift":
return operatingSystem() === "Mac" ? "KeyboardShift" : undefined;
default:
return undefined;
}
}
function mouseHintIcon(input?: MouseMotion): IconName {
return `MouseHint${input}` as IconName;
}
</script>
<LayoutRow class="shortcut-label">
{#each keyTextOrIconList(shortcut.shortcut) as { label, icon, mouseMotion }}
{#if label}
<div class="key-label">
<TextLabel>{label}</TextLabel>
</div>
{:else if icon}
<div class="key-icon">
<IconLabel {icon} />
</div>
{:else if mouseMotion}
<div class="mouse-icon">
<IconLabel icon={mouseHintIcon(mouseMotion)} />
</div>
{/if}
{/each}
</LayoutRow>
<style lang="scss" global>
.shortcut-label {
.key-icon,
.key-label {
display: flex;
align-items: center;
height: 16px;
padding: 0 4px;
border-radius: 4px;
background: var(--color-3-darkgray);
color: var(--color-b-lightgray);
fill: var(--color-b-lightgray);
}
svg {
fill: var(--color-b-lightgray);
.dim {
fill: var(--color-8-uppergray);
}
}
.floating-menu-content .row > & {
.key-label,
.key-icon,
.mouse-icon {
color: var(--color-8-uppergray);
background: none;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
.key-icon svg {
fill: var(--color-8-uppergray);
}
.mouse-icon svg {
// 3 shades brighter than the 8-uppergray of key labels/icons
fill: var(--color-b-lightgray);
.dim {
// 3 shades darker than the 8-uppergray of key labels/icons
fill: var(--color-5-dullgray);
}
}
}
}
</style>
@@ -1,4 +1,6 @@
<script lang="ts">
import type { ActionShortcut } from "@graphite/messages";
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
@@ -16,7 +18,7 @@
export let multiline = false;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let tooltipShortcut: ActionShortcut | undefined = undefined;
export let forCheckbox: bigint | undefined = undefined;
$: extraClasses = Object.entries(classes)
@@ -41,7 +43,7 @@
style={`${styleName} ${extraStyles}`.trim() || undefined}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
for={forCheckbox !== undefined ? `checkbox-input-${forCheckbox}` : undefined}
>
<slot />
@@ -1,273 +0,0 @@
<script lang="ts">
import { getContext } from "svelte";
import type { IconName } from "@graphite/icons";
import { type KeyRaw, type LayoutKeysGroup, type Key, type MouseMotion } from "@graphite/messages";
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
import { operatingSystem } from "@graphite/utility-functions/platform";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
type LabelData = { label?: string; icon?: IconName; width: string };
// Keys that become icons if they are listed here with their units of width
const ICON_WIDTHS_MAC = {
Shift: 2,
Control: 2,
Option: 2,
Command: 2,
};
const ICON_WIDTHS = {
ArrowUp: 1,
ArrowRight: 1,
ArrowDown: 1,
ArrowLeft: 1,
Backspace: 2,
Enter: 2,
Tab: 2,
Space: 3,
...(operatingSystem() === "Mac" ? ICON_WIDTHS_MAC : {}),
};
const fullscreen = getContext<FullscreenState>("fullscreen");
export let keysWithLabelsGroups: LayoutKeysGroup[] = [];
export let mouseMotion: MouseMotion | undefined = undefined;
export let requiresLock = false;
export let textOnly = false;
$: keyboardLockInfoMessage = watchKeyboardLockInfoMessage($fullscreen.keyboardLockApiSupported);
$: displayKeyboardLockNotice = requiresLock && !$fullscreen.keyboardLocked;
function watchKeyboardLockInfoMessage(keyboardLockApiSupported: boolean): string {
const RESERVED = "This keyboard shortcut is reserved by the browser.";
const USE_FULLSCREEN = "It is made available in fullscreen mode.";
const USE_SECURE_CTX = "It is made available in fullscreen mode when this website is served from a secure context (https or localhost).";
const SWITCH_BROWSER = "Use a Chromium-based browser (like Chrome or Edge) in fullscreen mode to directly use the shortcut.";
if (keyboardLockApiSupported) return `${RESERVED} ${USE_FULLSCREEN}`;
if (!("chrome" in window)) return `${RESERVED} ${SWITCH_BROWSER}`;
if (!window.isSecureContext) return `${RESERVED} ${USE_SECURE_CTX}`;
return RESERVED;
}
function keyTextOrIconList(keyGroup: LayoutKeysGroup): LabelData[] {
return keyGroup.map((key) => keyTextOrIcon(key));
}
function keyTextOrIcon(keyWithLabel: Key): LabelData {
// `key` is the name of the `Key` enum in Rust, while `label` is the localized string to display (if it doesn't become an icon)
let key = keyWithLabel.key;
const label = keyWithLabel.label;
// Replace Alt and Accel keys with their Mac-specific equivalents
if (operatingSystem() === "Mac") {
if (key === "Alt") key = "Option";
if (key === "Accel") key = "Command";
}
// Either display an icon...
// @ts-expect-error We want undefined if it isn't in the object
const iconWidth: number | undefined = ICON_WIDTHS[key];
const icon = iconWidth !== undefined && iconWidth > 0 && (keyboardHintIcon(key) || false);
if (icon) return { icon, width: `width-${iconWidth}` };
// ...or display text
return { label, width: `width-${label.length}` };
}
function mouseHintIcon(input?: MouseMotion): IconName {
return `MouseHint${input}` as IconName;
}
function keyboardHintIcon(input: KeyRaw): IconName | undefined {
switch (input) {
case "ArrowDown":
return "KeyboardArrowDown";
case "ArrowLeft":
return "KeyboardArrowLeft";
case "ArrowRight":
return "KeyboardArrowRight";
case "ArrowUp":
return "KeyboardArrowUp";
case "Backspace":
return "KeyboardBackspace";
case "Command":
return "KeyboardCommand";
case "Control":
return "KeyboardControl";
case "Enter":
return "KeyboardEnter";
case "Option":
return "KeyboardOption";
case "Shift":
return "KeyboardShift";
case "Space":
return "KeyboardSpace";
case "Tab":
return "KeyboardTab";
default:
return undefined;
}
}
</script>
{#if displayKeyboardLockNotice}
<IconLabel class="user-input-label keyboard-lock-notice" icon="Info" tooltipDescription={keyboardLockInfoMessage} />
{:else}
<LayoutRow class="user-input-label" classes={{ "text-only": textOnly }}>
{#each keysWithLabelsGroups as keysWithLabels, groupIndex}
{#if groupIndex > 0}
<Separator type="Related" />
{/if}
{#each keyTextOrIconList(keysWithLabels) as keyInfo}
<div class={`input-key ${keyInfo.width}`}>
{#if keyInfo.icon}
<IconLabel icon={keyInfo.icon} />
{:else if keyInfo.label !== undefined}
<TextLabel>{keyInfo.label}</TextLabel>
{/if}
</div>
{/each}
{/each}
{#if mouseMotion}
<div class="input-mouse">
<IconLabel icon={mouseHintIcon(mouseMotion)} />
</div>
{/if}
{#if $$slots.default}
<div class="hint-text">
<slot />
</div>
{/if}
</LayoutRow>
{/if}
<style lang="scss" global>
.user-input-label {
flex: 0 0 auto;
align-items: center;
white-space: nowrap;
&.text-only {
display: flex;
.input-key {
display: flex;
align-items: center;
.icon-label {
margin: calc(calc(18px - 12px) / 2) 0;
}
& + .input-key {
margin-left: 4px;
}
}
}
&:not(.text-only) {
.input-key,
.input-mouse {
& + .input-key,
& + .input-mouse {
margin-left: 2px;
}
}
.input-key {
display: flex;
justify-content: center;
align-items: center;
font-family: "Source Code Pro", monospace;
font-size: 12px;
font-weight: 400;
text-align: center;
height: 16px;
border-radius: 4px;
background: var(--color-3-darkgray);
color: var(--color-b-lightgray);
.icon-label {
fill: var(--color-b-lightgray);
}
.text-label {
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height,
// so moving it up 1 pixel by using 15px makes them agree.
line-height: 15px;
}
&.width-1 {
width: 16px;
}
&.width-2 {
width: 24px;
}
&.width-3 {
width: 32px;
}
&.width-4 {
width: 40px;
}
&.width-5 {
width: 48px;
}
.icon-label {
margin: 1px;
}
}
}
.input-mouse {
.bright {
fill: var(--color-b-lightgray);
}
.dim {
fill: var(--color-8-uppergray);
}
}
.hint-text:not(:empty) {
margin-left: 4px;
}
.floating-menu-content .row > & {
.input-key {
border-color: var(--color-3-darkgray);
color: var(--color-8-uppergray);
}
.input-key .icon-label svg,
&.keyboard-lock-notice.keyboard-lock-notice svg,
.input-mouse .bright {
fill: var(--color-8-uppergray);
}
.input-mouse .dim {
fill: var(--color-3-darkgray);
}
}
.floating-menu-content .row:hover > & {
.input-key {
border-color: var(--color-8-uppergray);
}
.input-mouse .dim {
fill: var(--color-8-uppergray);
}
}
}
</style>