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
@@ -12,8 +12,8 @@
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.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 UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte";
let self: FloatingMenu | undefined;
let scroller: LayoutCol | undefined;
@@ -447,8 +447,8 @@
<TextLabel class="entry-label" styles={{ "font-family": `${!entry.font ? "inherit" : entry.value}` }}>{entry.label}</TextLabel>
{#if entry.shortcutKeys?.keys.length}
<UserInputLabel keysWithLabelsGroups={[entry.shortcutKeys.keys]} requiresLock={entry.shortcutRequiresLock} textOnly={true} />
{#if entry.tooltipShortcut?.shortcut.length}
<ShortcutLabel shortcut={entry.tooltipShortcut} />
{/if}
{#if entry.children?.length}
@@ -499,7 +499,7 @@
margin: 4px 0;
div {
background: var(--color-4-dimgray);
background: var(--color-3-darkgray);
}
}
@@ -535,7 +535,7 @@
margin: 0 4px;
}
.user-input-label {
.shortcut-label {
margin-left: 12px;
}
@@ -2,10 +2,12 @@
import { getContext } from "svelte";
import type { Editor } from "@graphite/editor";
import type { LabeledShortcut } from "@graphite/messages";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import ShortcutLabel from "@graphite/components/widgets/labels/ShortcutLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const tooltip = getContext<TooltipState>("tooltip");
@@ -15,7 +17,15 @@
$: label = filterTodo($tooltip.element?.getAttribute("data-tooltip-label")?.trim());
$: description = filterTodo($tooltip.element?.getAttribute("data-tooltip-description")?.trim());
$: shortcut = filterTodo($tooltip.element?.getAttribute("data-tooltip-shortcut")?.trim());
$: shortcutJSON = $tooltip.element?.getAttribute("data-tooltip-shortcut")?.trim();
$: shortcut = ((shortcutJSON) => {
if (!shortcutJSON) return undefined;
try {
return JSON.parse(shortcutJSON) as LabeledShortcut;
} catch {
return undefined;
}
})(shortcutJSON);
// TODO: Once all TODOs are replaced with real text, remove this function
function filterTodo(text: string | undefined): string | undefined {
@@ -24,8 +34,8 @@
}
</script>
<div class="tooltip" style:top={`${$tooltip.position.y}px`} style:left={`${$tooltip.position.x}px`}>
{#if label || description}
{#if label || description}
<div class="tooltip" style:top={`${$tooltip.position.y}px`} style:left={`${$tooltip.position.x}px`}>
<FloatingMenu open={true} type="Tooltip" direction="Bottom" bind:this={self}>
{#if label || shortcut}
<LayoutRow class="tooltip-header">
@@ -33,7 +43,7 @@
<TextLabel class="tooltip-label">{label}</TextLabel>
{/if}
{#if shortcut}
<TextLabel class="tooltip-shortcut">{shortcut}</TextLabel>
<ShortcutLabel shortcut={{ shortcut }} />
{/if}
</LayoutRow>
{/if}
@@ -41,8 +51,8 @@
<TextLabel class="tooltip-description">{description}</TextLabel>
{/if}
</FloatingMenu>
{/if}
</div>
</div>
{/if}
<style lang="scss" global>
.tooltip {
@@ -62,17 +72,10 @@
white-space: pre-wrap;
}
.text-label + .tooltip-shortcut {
.text-label + .shortcut-label {
margin-left: 8px;
}
.tooltip-shortcut {
color: var(--color-b-lightgray);
background: var(--color-3-darkgray);
padding: 0 4px;
border-radius: 4px;
}
.tooltip-description {
color: var(--color-b-lightgray);
}
@@ -519,7 +519,7 @@
.floating-menu-content {
background: var(--color-2-mildblack);
box-shadow: rgba(var(--color-0-black-rgb), 0.5) 0 2px 4px;
border: 1px solid var(--color-4-dimgray);
border: 1px solid var(--color-3-darkgray);
border-radius: 4px;
color: var(--color-e-nearwhite);
font-size: inherit;
@@ -615,7 +615,7 @@
&.top .tail,
&.topleft .tail,
&.topright .tail {
border-color: var(--color-4-dimgray) transparent transparent transparent;
border-color: var(--color-3-darkgray) transparent transparent transparent;
&::before {
border-color: var(--color-2-mildblack) transparent transparent transparent;
@@ -633,7 +633,7 @@
&.bottom .tail,
&.bottomleft .tail,
&.bottomright .tail {
border-color: transparent transparent var(--color-4-dimgray) transparent;
border-color: transparent transparent var(--color-3-darkgray) transparent;
&::before {
border-color: transparent transparent var(--color-2-mildblack) transparent;
@@ -649,7 +649,7 @@
}
&.left .tail {
border-color: transparent transparent transparent var(--color-4-dimgray);
border-color: transparent transparent transparent var(--color-3-darkgray);
&::before {
border-color: transparent transparent transparent var(--color-2-mildblack);
@@ -665,7 +665,7 @@
}
&.right .tail {
border-color: transparent var(--color-4-dimgray) transparent transparent;
border-color: transparent var(--color-3-darkgray) transparent transparent;
&::before {
border-color: transparent var(--color-2-mildblack) transparent transparent;
@@ -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> = {};
@@ -7,7 +9,7 @@
export let styles: Record<string, string | number | 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;
// TODO: Add middle-click drag scrolling
export let scrollableX = false;
export let scrollableY = false;
@@ -30,7 +32,7 @@
<div
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
data-scrollable-x={scrollableX ? "" : undefined}
data-scrollable-y={scrollableY ? "" : undefined}
class={`layout-col ${className} ${extraClasses}`.trim()}
@@ -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> = {};
@@ -7,7 +9,7 @@
export let styles: Record<string, string | number | 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;
// TODO: Add middle-click drag scrolling
export let scrollableX = false;
export let scrollableY = false;
@@ -30,7 +32,7 @@
<div
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
data-scrollable-x={scrollableX ? "" : undefined}
data-scrollable-y={scrollableY ? "" : undefined}
class={`layout-row ${className} ${extraClasses}`.trim()}
+6 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { getContext, onMount, onDestroy, tick } from "svelte";
import { shortcutAltClick } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import {
defaultWidgetLayout,
@@ -11,7 +12,7 @@
UpdateLayersPanelControlBarRightLayout,
UpdateLayersPanelBottomBarLayout,
} from "@graphite/messages";
import type { DataBuffer, LayerPanelEntry } from "@graphite/messages";
import type { ActionShortcut, DataBuffer, LayerPanelEntry } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import { operatingSystem } from "@graphite/utility-functions/platform";
import { extractPixelData } from "@graphite/utility-functions/rasterization";
@@ -73,6 +74,8 @@
let layersPanelControlBarRightLayout = defaultWidgetLayout();
let layersPanelBottomBarLayout = defaultWidgetLayout();
const altClickKeys: ActionShortcut = shortcutAltClick();
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (updateLayersPanelControlBarLeftLayout) => {
patchWidgetLayout(layersPanelControlBarLeftLayout, updateLayersPanelControlBarLeftLayout);
@@ -622,7 +625,7 @@
? "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" : "")}
data-tooltip-shortcut="Alt Click"
data-tooltip-shortcut={altClickKeys?.shortcut ? JSON.stringify(altClickKeys.shortcut) : undefined}
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
tabindex="0"
></button>
@@ -634,7 +637,7 @@
icon="Clipped"
class="clipped-arrow"
tooltipDescription="Clipping mask is active. To release it, perform the shortcut on the layer border."
tooltipShortcut="Alt Click"
tooltipShortcut={altClickKeys}
/>
{/if}
<div class="thumbnail">
@@ -0,0 +1,141 @@
<script lang="ts">
import { getContext, onMount, onDestroy } from "svelte";
import type { Editor } from "@graphite/editor";
import { defaultWidgetLayout, patchWidgetLayout, UpdateWelcomeScreenButtonsLayout } from "@graphite/messages";
import { extractPixelData } from "@graphite/utility-functions/rasterization";
import LayoutCol from "@graphite/components/layout/LayoutCol.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";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
let welcomePanelButtonsLayout = defaultWidgetLayout();
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateWelcomeScreenButtonsLayout, (updateWelcomeScreenButtonsLayout) => {
patchWidgetLayout(welcomePanelButtonsLayout, updateWelcomeScreenButtonsLayout);
welcomePanelButtonsLayout = welcomePanelButtonsLayout;
});
editor.handle.requestWelcomeScreenButtonsLayout();
});
onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdateWelcomeScreenButtonsLayout);
});
function dropFile(e: DragEvent) {
if (!e.dataTransfer) return;
e.preventDefault();
Array.from(e.dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile();
if (!file) return;
if (file.type.includes("svg")) {
const svgData = await file.text();
editor.handle.pasteSvg(file.name, svgData);
return;
}
if (file.type.startsWith("image")) {
const imageData = await extractPixelData(file);
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
return;
}
const graphiteFileSuffix = "." + editor.handle.fileExtension();
if (file.name.endsWith(graphiteFileSuffix)) {
const content = await file.text();
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
editor.handle.openDocumentFile(documentName, content);
return;
}
});
}
</script>
<LayoutCol class="welcome-panel" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
<LayoutCol class="top-spacer"></LayoutCol>
<LayoutCol class="content-container">
<LayoutCol class="content">
<LayoutRow class="logotype">
<IconLabel icon="GraphiteLogotypeSolid" />
</LayoutRow>
<LayoutRow class="actions">
<WidgetLayout layout={welcomePanelButtonsLayout} />
</LayoutRow>
</LayoutCol>
</LayoutCol>
<LayoutCol class="bottom-message">
{#if new Date().getFullYear() === 2025}
<TextLabel italic={true} disabled={true}>
September 2025 release — <a href="https://youtube.com/watch?v=Vl5BA4g3QXM" target="_blank">What's new? (video)</a>
— Note: some older documents may render differently and require manual fixes.
<a href="https://ec6796b4.graphite-editor.pages.dev/" target="_blank">Need the old version?</a>
</TextLabel>
{/if}
</LayoutCol>
</LayoutCol>
<style lang="scss" global>
.welcome-panel {
background: var(--color-2-mildblack);
margin: 4px;
border-radius: 2px;
justify-content: space-between;
.content-container {
flex: 0 0 auto;
justify-content: center;
.content {
flex: 0 0 auto;
align-items: center;
.logotype {
margin-top: 8px;
margin-bottom: 40px;
svg {
width: auto;
height: 120px;
}
}
.actions {
margin-bottom: 8px;
table {
border-spacing: 8px;
margin: -8px;
td {
padding: 0;
}
}
}
}
}
.top-spacer {
flex: 0 1 48px;
}
.bottom-message {
flex: 0 0 48px;
align-items: center;
justify-content: end;
.text-label {
white-space: wrap;
margin: 0 1em;
}
}
}
</style>
+2 -1
View File
@@ -588,10 +588,11 @@
data-visibility-button
size={24}
icon={node.visible ? "EyeVisible" : "EyeHidden"}
hoverIcon={node.visible ? "EyeHide" : "EyeShow"}
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
tooltipLabel={node.visible ? "Visible" : "Hidden"}
tooltipLabel={node.visible ? "Hide" : "Show"}
/>
<svg class="border-mask" width="0" height="0">
@@ -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>
@@ -2,45 +2,25 @@
import { getContext, onMount } from "svelte";
import type { Editor } from "@graphite/editor";
import { type HintData, type HintInfo, type LayoutKeysGroup, UpdateInputHints } from "@graphite/messages";
import { operatingSystem } from "@graphite/utility-functions/platform";
import { defaultWidgetLayout, patchWidgetLayout, UpdateStatusBarHintsLayout } from "@graphite/messages";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
let hintData: HintData = [];
function inputKeysForPlatform(hint: HintInfo): LayoutKeysGroup[] {
return operatingSystem() === "Mac" && hint.keyGroupsMac ? hint.keyGroupsMac : hint.keyGroups;
}
let statusBarHintsLayout = defaultWidgetLayout();
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateInputHints, (data) => {
hintData = data.hintData;
editor.subscriptions.subscribeJsMessage(UpdateStatusBarHintsLayout, (updateStatusBarHintsLayout) => {
patchWidgetLayout(statusBarHintsLayout, updateStatusBarHintsLayout);
statusBarHintsLayout = statusBarHintsLayout;
});
});
</script>
<LayoutRow class="status-bar">
<LayoutRow class="hint-groups">
{#each hintData as hintGroup, index (hintGroup)}
{#if index !== 0}
<Separator type="Section" />
{/if}
{#each hintGroup as hint (hint)}
{#if hint.plus}
<LayoutRow class="plus">+</LayoutRow>
{/if}
{#if hint.slash}
<LayoutRow class="slash">/</LayoutRow>
{/if}
<UserInputLabel mouseMotion={hint.mouse} keysWithLabelsGroups={inputKeysForPlatform(hint)}>{hint.label}</UserInputLabel>
{/each}
{/each}
</LayoutRow>
<WidgetLayout class="hints" layout={statusBarHintsLayout} />
</LayoutRow>
<style lang="scss" global>
@@ -49,30 +29,29 @@
width: 100%;
flex: 0 0 auto;
.hint-groups {
flex: 0 0 auto;
max-width: 100%;
margin: 0 -4px;
.hints {
overflow: hidden;
--row-height: 24px;
margin: 0 4px;
max-width: calc(100% - 2 * 4px);
.separator.section {
// Width of section separator (12px) minus the margin of the surrounding user input labels (8px)
margin: 0 calc(12px - 8px);
}
.plus,
.slash {
flex: 0 0 auto;
.text-label,
.shortcut-label {
align-items: center;
font-weight: 700;
flex-shrink: 0;
+ .text-label,
+ .shortcut-label {
margin-left: 4px;
}
}
.user-input-label {
margin: 0 8px;
.text-label:not(.bold) + .shortcut-label {
margin-left: 12px;
}
& + .user-input-label {
margin-left: 0;
}
.text-label.bold {
padding: 0 4px;
}
}
}
@@ -1,6 +1,8 @@
<script lang="ts">
import { getContext } from "svelte";
import { shortcutF11 } from "@graphite/../wasm/pkg/graphite_wasm";
import type { ActionShortcut } from "@graphite/messages";
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -8,6 +10,8 @@
const fullscreen = getContext<FullscreenState>("fullscreen");
const f11Keys: ActionShortcut = shortcutF11();
async function handleClick() {
if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen();
else fullscreen.enterFullscreen();
@@ -19,7 +23,7 @@
on:click={handleClick}
tooltipLabel={$fullscreen.windowFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
tooltipDescription={$fullscreen.keyboardLockApiSupported ? "While fullscreen, keyboard shortcuts normally reserved by the browser become available." : ""}
tooltipShortcut="F11"
tooltipShortcut={f11Keys}
>
<IconLabel icon={$fullscreen.windowFullscreen ? "FullscreenExit" : "FullscreenEnter"} />
</LayoutRow>
@@ -3,8 +3,10 @@
import Document from "@graphite/components/panels/Document.svelte";
import Layers from "@graphite/components/panels/Layers.svelte";
import Properties from "@graphite/components/panels/Properties.svelte";
import Welcome from "@graphite/components/panels/Welcome.svelte";
const PANEL_COMPONENTS = {
Welcome,
Document,
Layers,
Properties,
@@ -17,23 +19,16 @@
import { getContext, tick } from "svelte";
import type { Editor } from "@graphite/editor";
import { type LayoutKeysGroup, type Key } from "@graphite/messages";
import type { AppWindowState } from "@graphite/state-providers/app-window";
import { operatingSystem, isEventSupported } from "@graphite/utility-functions/platform";
import { extractPixelData } from "@graphite/utility-functions/rasterization";
import { isEventSupported } from "@graphite/utility-functions/platform";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte";
const BUTTON_LEFT = 0;
const BUTTON_MIDDLE = 1;
const appWindow = getContext<AppWindowState>("appWindow");
const editor = getContext<Editor>("editor");
export let tabMinWidths = false;
@@ -54,52 +49,6 @@
let tabElements: (LayoutRow | undefined)[] = [];
function platformModifiedAccelKey(browserReservedKey: boolean): LayoutKeysGroup {
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
const ALT: Key = { key: "Alt", label: "Alt" };
const COMMAND: Key = { key: "Command", label: "Command" };
const CONTROL: Key = { key: "Control", label: "Ctrl" };
// Only consider the browser reserved key on web platforms
const reservedKey = $appWindow.platform === "Web" ? browserReservedKey : false;
// Return either Command (Mac) or Control (Windows/Linux), with Alt added if the browser reserves the shortcut
if (operatingSystem() === "Mac") return reservedKey ? [ALT, COMMAND] : [COMMAND];
return reservedKey ? [CONTROL, ALT] : [CONTROL];
}
function dropFile(e: DragEvent) {
if (!e.dataTransfer) return;
e.preventDefault();
Array.from(e.dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile();
if (!file) return;
if (file.type.includes("svg")) {
const svgData = await file.text();
editor.handle.pasteSvg(file.name, svgData);
return;
}
if (file.type.startsWith("image")) {
const imageData = await extractPixelData(file);
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
return;
}
const graphiteFileSuffix = "." + editor.handle.fileExtension();
if (file.name.endsWith(graphiteFileSuffix)) {
const content = await file.text();
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
editor.handle.openDocumentFile(documentName, content);
return;
}
});
}
function onEmptySpaceAction(e: MouseEvent) {
if (e.target !== e.currentTarget) return;
if (e.button === BUTTON_MIDDLE || (e.button === BUTTON_LEFT && e.detail === 2)) emptySpaceAction?.();
@@ -162,66 +111,10 @@
</LayoutRow>
{/each}
</LayoutRow>
<!-- <PopoverButton style="VerticalEllipsis">
<TextLabel bold={true}>Panel Options</TextLabel>
<TextLabel multiline={true}>Coming soon</TextLabel>
</PopoverButton> -->
</LayoutRow>
<LayoutCol class="panel-body">
{#if panelType}
<svelte:component this={PANEL_COMPONENTS[panelType]} />
{:else}
<LayoutCol class="empty-panel" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
<LayoutCol class="top-spacer"></LayoutCol>
<LayoutCol class="content-container">
<LayoutCol class="content">
<LayoutRow class="logotype">
<IconLabel icon="GraphiteLogotypeSolid" />
</LayoutRow>
<LayoutRow class="actions">
<table>
<tbody>
<tr>
<td>
<TextButton label="New Document" icon="File" flush={true} action={() => editor.handle.newDocumentDialog()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiedAccelKey(true), { key: "KeyN", label: "N" }]]} />
</td>
</tr>
<tr>
<td>
<TextButton label="Open Document" icon="Folder" flush={true} action={() => editor.handle.openDocument()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiedAccelKey(false), { key: "KeyO", label: "O" }]]} />
</td>
</tr>
<tr>
<td colspan="2">
<TextButton label="Open Demo Artwork" icon="Image" flush={true} action={() => editor.handle.demoArtworkDialog()} />
</td>
</tr>
<tr>
<td colspan="2">
<TextButton label="Support the Development Fund" icon="Heart" flush={true} action={() => editor.handle.visitUrl("https://graphite.rs/donate/")} />
</td>
</tr>
</tbody>
</table>
</LayoutRow>
</LayoutCol>
</LayoutCol>
<LayoutCol class="bottom-message">
{#if new Date().getFullYear() === 2025}
<TextLabel italic={true} disabled={true}>
September 2025 release — <a href="https://youtube.com/watch?v=Vl5BA4g3QXM" target="_blank">What's new? (video)</a>
— Note: some older documents may render differently and require manual fixes.
<a href="https://ec6796b4.graphite-editor.pages.dev/" target="_blank">Need the old version?</a>
</TextLabel>
{/if}
</LayoutCol>
</LayoutCol>
{/if}
</LayoutCol>
</LayoutCol>
@@ -323,7 +216,7 @@
left: -1px;
width: 1px;
height: 16px;
background: var(--color-4-dimgray);
background: var(--color-5-dullgray);
}
&:last-of-type {
@@ -335,15 +228,11 @@
right: -1px;
width: 1px;
height: 16px;
background: var(--color-4-dimgray);
background: var(--color-5-dullgray);
}
}
}
}
// .popover-button {
// margin: 2px 4px;
// }
}
.panel-body {
@@ -354,66 +243,11 @@
> div {
padding-bottom: 4px;
}
.empty-panel {
background: var(--color-2-mildblack);
margin: 4px;
border-radius: 2px;
justify-content: space-between;
.content-container {
flex: 0 0 auto;
justify-content: center;
.content {
flex: 0 0 auto;
align-items: center;
.logotype {
margin-top: 8px;
margin-bottom: 40px;
svg {
width: auto;
height: 120px;
}
}
.actions {
margin-bottom: 8px;
table {
border-spacing: 8px;
margin: -8px;
td {
padding: 0;
}
}
}
}
}
.top-spacer {
flex: 0 1 48px;
}
.bottom-message {
flex: 0 0 48px;
align-items: center;
justify-content: end;
.text-label {
white-space: wrap;
margin: 0 1em;
}
}
}
}
// Needed for the viewport hole punch on desktop
.viewport-hole-punch &.document-panel,
.viewport-hole-punch &.document-panel .panel-body:not(:has(.empty-panel)) {
.viewport-hole-punch &.document-panel .panel-body:not(:has(.welcome-panel)) {
background: none;
}
}
@@ -135,7 +135,7 @@
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["document"] }} data-subdivision-name="document">
<Panel
class="document-panel"
panelType={$portfolio.documents.length > 0 ? "Document" : undefined}
panelType={$portfolio.documents.length > 0 ? "Document" : "Welcome"}
tabCloseButtons={true}
tabMinWidths={true}
tabLabels={documentTabLabels}