Rename and reorganize several widgets (#1462)

* Rename SwatchPairInput -> WorkingColorsButton

* Remove unnecessary Svelte each-loop keys

* Rename (and migrate) MenuBarInput -> MenuListButton

* Rename PivotAssist -> PivotInput

* Rename PersistentScrollbar -> ScrollbarInput and CanvasRuler -> RulerInput

* Rename DIalogModal -> Dialog

* Rename WidgetRow -> WidgetSpan
This commit is contained in:
Keavon Chambers
2023-11-18 04:34:30 -08:00
committed by GitHub
parent e3f5e7001f
commit 719c96ecd8
38 changed files with 375 additions and 389 deletions
+2 -2
View File
@@ -272,10 +272,10 @@
.popover-button,
.color-button > button,
.color-picker .preset-color,
.swatch-pair .swatch > button,
.working-colors-button .swatch > button,
.radio-input button,
.menu-list,
.menu-bar-input .entry,
.menu-list-button .entry,
.layer-tree .expand-arrow,
.widget-section .header {
&:focus-visible {
+1 -1
View File
@@ -4,7 +4,7 @@ Each component represents a (usually reusable) part of the Graphite editor GUI.
## Floating Menus: `floating-menus/`
The temporary UI areas with dark backgrounds which hover over the top of the editor window content. Examples include popovers, dropdown menu selectors, and dialog modals.
The temporary UI areas with dark backgrounds which hover over the top of the editor window content. Examples include menu lists, popovers, and dialogs.
## Layout: `layout/`
@@ -319,7 +319,7 @@
<TextLabel tooltip="Red/Green/Blue channels of the color, integers 0255">RGB</TextLabel>
<Separator />
<LayoutRow>
{#each rgbChannels as [channel, strength], index (channel)}
{#each rgbChannels as [channel, strength], index}
{#if index > 0}
<Separator type="Related" />
{/if}
@@ -343,7 +343,7 @@
>
<Separator />
<LayoutRow>
{#each hsvChannels as [channel, strength], index (channel)}
{#each hsvChannels as [channel, strength], index}
{#if index > 0}
<Separator type="Related" />
{/if}
@@ -26,7 +26,7 @@
</script>
<!-- TODO: Use https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog for improved accessibility -->
<FloatingMenu open={true} class="dialog-modal" type="Dialog" direction="Center" bind:this={self} data-dialog-modal>
<FloatingMenu open={true} class="dialog" type="Dialog" direction="Center" bind:this={self} data-dialog>
<LayoutRow class="header-area">
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
@@ -39,11 +39,11 @@
{/if}
{#if $dialog.panicDetails}
<div class="widget-layout details">
<div class="widget-row"><TextLabel bold={true}>The editor crashed sorry about that</TextLabel></div>
<div class="widget-row"><TextLabel>Please report this by filing an issue on GitHub:</TextLabel></div>
<div class="widget-row"><TextButton label="Report Bug" icon="Warning" noBackground={true} action={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
<div class="widget-row"><TextLabel multiline={true}>Reload the editor to continue. If this occurs<br />immediately on repeated reloads, clear storage:</TextLabel></div>
<div class="widget-row">
<div class="widget-span row"><TextLabel bold={true}>The editor crashed sorry about that</TextLabel></div>
<div class="widget-span row"><TextLabel>Please report this by filing an issue on GitHub:</TextLabel></div>
<div class="widget-span row"><TextButton label="Report Bug" icon="Warning" noBackground={true} action={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
<div class="widget-span row"><TextLabel multiline={true}>Reload the editor to continue. If this occurs<br />immediately on repeated reloads, clear storage:</TextLabel></div>
<div class="widget-span row">
<TextButton
label="Clear Saved Documents"
icon="Trash"
@@ -75,7 +75,7 @@
</FloatingMenu>
<style lang="scss" global>
.dialog-modal {
.dialog {
position: absolute;
pointer-events: none;
width: 100%;
@@ -103,7 +103,7 @@
const menuOpen = open;
const flatEntries = entries.flat().filter((entry) => !entry.disabled);
const openChild = flatEntries.findIndex((entry) => entry.children?.length && entry.ref?.open);
const openChild = flatEntries.findIndex((entry) => (entry.children?.length ?? 0) > 0 && entry.ref?.open);
const openSubmenu = (highlightedEntry: MenuListEntry) => {
if (highlightedEntry.ref && highlightedEntry.children?.length) {
+13 -13
View File
@@ -27,12 +27,12 @@
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import Graph from "@graphite/components/views/Graph.svelte";
import CanvasRuler from "@graphite/components/widgets/metrics/CanvasRuler.svelte";
import PersistentScrollbar from "@graphite/components/widgets/metrics/PersistentScrollbar.svelte";
import RulerInput from "@graphite/components/widgets/inputs/RulerInput.svelte";
import ScrollbarInput from "@graphite/components/widgets/inputs/ScrollbarInput.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
let rulerHorizontal: CanvasRuler | undefined;
let rulerVertical: CanvasRuler | undefined;
let rulerHorizontal: RulerInput | undefined;
let rulerVertical: RulerInput | undefined;
let viewport: HTMLDivElement | undefined;
const editor = getContext<Editor>("editor");
@@ -441,11 +441,11 @@
</LayoutCol>
<LayoutCol class="table">
<LayoutRow class="ruler-or-scrollbar top-ruler">
<CanvasRuler origin={rulerOrigin.x} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Horizontal" bind:this={rulerHorizontal} />
<RulerInput origin={rulerOrigin.x} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Horizontal" bind:this={rulerHorizontal} />
</LayoutRow>
<LayoutRow class="viewport-container">
<LayoutCol class="ruler-or-scrollbar">
<CanvasRuler origin={rulerOrigin.y} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Vertical" bind:this={rulerVertical} />
<RulerInput origin={rulerOrigin.y} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Vertical" bind:this={rulerVertical} />
</LayoutCol>
<LayoutCol class="viewport-container" styles={{ cursor: canvasCursor }}>
{#if cursorEyedropper}
@@ -482,7 +482,7 @@
</div>
</LayoutCol>
<LayoutCol class="ruler-or-scrollbar right-scrollbar">
<PersistentScrollbar
<ScrollbarInput
direction="Vertical"
handleLength={scrollbarSize.y}
handlePosition={scrollbarPos.y}
@@ -492,7 +492,7 @@
</LayoutCol>
</LayoutRow>
<LayoutRow class="ruler-or-scrollbar bottom-scrollbar">
<PersistentScrollbar
<ScrollbarInput
direction="Horizontal"
handleLength={scrollbarSize.x}
handlePosition={scrollbarPos.x}
@@ -572,10 +572,10 @@
.widget-layout:last-of-type {
height: auto;
.widget-row {
.widget-span.row {
min-height: 0;
.swatch-pair {
.working-colors-button {
margin: 0;
}
@@ -594,16 +594,16 @@
flex: 0 0 auto;
}
.top-ruler .canvas-ruler {
.top-ruler .ruler-input {
padding-left: 16px;
margin-right: 16px;
}
.right-scrollbar .persistent-scrollbar {
.right-scrollbar .scrollbar-input {
margin-top: -16px;
}
.bottom-scrollbar .persistent-scrollbar {
.bottom-scrollbar .scrollbar-input {
margin-right: 16px;
}
@@ -1,8 +1,8 @@
<script lang="ts">
import { isWidgetColumn, isWidgetRow, isWidgetSection, type WidgetLayout } from "@graphite/wasm-communication/messages";
import { isWidgetSpanColumn, isWidgetSpanRow, isWidgetSection, type WidgetLayout } from "@graphite/wasm-communication/messages";
import WidgetSection from "@graphite/components/widgets/groups/WidgetSection.svelte";
import WidgetRow from "@graphite/components/widgets/WidgetRow.svelte";
import WidgetSection from "@graphite/components/widgets/WidgetSection.svelte";
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
export let layout: WidgetLayout;
let className = "";
@@ -14,15 +14,14 @@
.join(" ");
</script>
<!-- TODO: Refactor this component (together with `WidgetRow.svelte`) to be more logically consistent with our layout definition goals, in terms of naming and capabilities -->
<div class={`widget-layout ${className} ${extraClasses}`.trim()}>
{#each layout.layout as layoutGroup, index (index)}
{#if isWidgetColumn(layoutGroup) || isWidgetRow(layoutGroup)}
<WidgetRow widgetData={layoutGroup} layoutTarget={layout.layoutTarget} />
{#each layout.layout as layoutGroup}
{#if isWidgetSpanRow(layoutGroup) || isWidgetSpanColumn(layoutGroup)}
<WidgetSpan widgetData={layoutGroup} layoutTarget={layout.layoutTarget} />
{:else if isWidgetSection(layoutGroup)}
<WidgetSection widgetData={layoutGroup} layoutTarget={layout.layoutTarget} />
{:else}
<span style="color: #d6536e">Error: The widget row that belongs here has an invalid layout group type</span>
<span style="color: #d6536e">Error: The widget layout that belongs here has an invalid layout group type</span>
{/if}
{/each}
</div>
@@ -1,9 +1,9 @@
<script lang="ts">
import { isWidgetRow, isWidgetSection, type WidgetSection as WidgetSectionFromJsMessages } from "@graphite/wasm-communication/messages";
import { isWidgetSpanRow, isWidgetSpanColumn, isWidgetSection, type WidgetSection as WidgetSectionFromJsMessages } from "@graphite/wasm-communication/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import WidgetRow from "@graphite/components/widgets/WidgetRow.svelte";
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
export let widgetData: WidgetSectionFromJsMessages;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -20,9 +20,11 @@
</button>
{#if expanded}
<LayoutCol class="body">
{#each widgetData.layout as layoutGroup, index (index)}
{#if isWidgetRow(layoutGroup)}
<WidgetRow widgetData={layoutGroup} {layoutTarget} />
{#each widgetData.layout as layoutGroup}
{#if isWidgetSpanRow(layoutGroup)}
<WidgetSpan widgetData={layoutGroup} {layoutTarget} />
{:else if isWidgetSpanColumn(layoutGroup)}
<span style="color: #d6536e">Error: The WidgetSpan used here should be a row not a column</span>
{:else if isWidgetSection(layoutGroup)}
<svelte:self widgetData={layoutGroup} {layoutTarget} />
{:else}
@@ -105,7 +107,7 @@
border-radius: 0 0 4px 4px;
overflow: hidden;
.widget-row {
.widget-span.row {
&:first-child {
margin-top: calc(4px - 1px);
}
@@ -1,19 +1,18 @@
<script lang="ts">
import { getContext } from "svelte";
import ColorButton from "./buttons/ColorButton.svelte";
import { debouncer } from "@graphite/utility-functions/debounce";
import type { Editor } from "@graphite/wasm-communication/editor";
import type { Widget, WidgetColumn, WidgetRow } from "@graphite/wasm-communication/messages";
import { narrowWidgetProps, isWidgetColumn, isWidgetRow } from "@graphite/wasm-communication/messages";
import type { Widget, WidgetSpanColumn, WidgetSpanRow } from "@graphite/wasm-communication/messages";
import { narrowWidgetProps, isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/wasm-communication/messages";
import PivotAssist from "@graphite/components/widgets/assists/PivotAssist.svelte";
import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte";
import ColorButton from "@graphite/components/widgets/buttons/ColorButton.svelte";
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
import ParameterExposeButton from "@graphite/components/widgets/buttons/ParameterExposeButton.svelte";
import PopoverButton from "@graphite/components/widgets/buttons/PopoverButton.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
import WorkingColorsButton from "@graphite/components/widgets/buttons/WorkingColorsButton.svelte";
import CheckboxInput from "@graphite/components/widgets/inputs/CheckboxInput.svelte";
import CurveInput from "@graphite/components/widgets/inputs/CurveInput.svelte";
import DropdownInput from "@graphite/components/widgets/inputs/DropdownInput.svelte";
@@ -21,8 +20,8 @@
import LayerReferenceInput from "@graphite/components/widgets/inputs/LayerReferenceInput.svelte";
import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte";
import OptionalInput from "@graphite/components/widgets/inputs/OptionalInput.svelte";
import PivotInput from "@graphite/components/widgets/inputs/PivotInput.svelte";
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
import SwatchPairInput from "@graphite/components/widgets/inputs/SwatchPairInput.svelte";
import TextAreaInput from "@graphite/components/widgets/inputs/TextAreaInput.svelte";
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
@@ -35,7 +34,7 @@
const editor = getContext<Editor>("editor");
export let widgetData: WidgetColumn | WidgetRow;
export let widgetData: WidgetSpanRow | WidgetSpanColumn;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let layoutTarget: any;
@@ -43,16 +42,15 @@
$: widgets = watchWidgets(widgetData);
$: widgetsAndNextSiblingIsSuffix = watchWidgetsAndNextSiblingIsSuffix(widgets);
function watchDirection(widgetData: WidgetRow | WidgetColumn): "row" | "column" | "ERROR" {
if (isWidgetRow(widgetData)) return "row";
if (isWidgetColumn(widgetData)) return "column";
return "ERROR";
function watchDirection(widgetData: WidgetSpanRow | WidgetSpanColumn): "row" | "column" | undefined {
if (isWidgetSpanRow(widgetData)) return "row";
if (isWidgetSpanColumn(widgetData)) return "column";
}
function watchWidgets(widgetData: WidgetRow | WidgetColumn): Widget[] {
function watchWidgets(widgetData: WidgetSpanRow | WidgetSpanColumn): Widget[] {
let widgets: Widget[] = [];
if (isWidgetRow(widgetData)) widgets = widgetData.rowWidgets;
else if (isWidgetColumn(widgetData)) widgets = widgetData.columnWidgets;
if (isWidgetSpanRow(widgetData)) widgets = widgetData.rowWidgets;
else if (isWidgetSpanColumn(widgetData)) widgets = widgetData.columnWidgets;
return widgets;
}
@@ -83,8 +81,8 @@
<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components -->
<!-- TODO: Also rename this component, and probably move the `widget-${direction}` wrapper to be part of `WidgetLayout.svelte` as part of its refactor -->
<div class={`widget-${direction}`}>
{#each widgetsAndNextSiblingIsSuffix as [component, nextIsSuffix], index (index)}
<div class="widget-span" class:row={direction === "row"} class:column={direction === "column"}>
{#each widgetsAndNextSiblingIsSuffix as [component, nextIsSuffix], index}
{@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")}
{#if checkboxInput}
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => updateLayout(index, detail)} />
@@ -139,9 +137,9 @@
{#if optionalInput}
<OptionalInput {...exclude(optionalInput)} on:checked={({ detail }) => updateLayout(index, detail)} />
{/if}
{@const pivotAssist = narrowWidgetProps(component.props, "PivotAssist")}
{#if pivotAssist}
<PivotAssist {...exclude(pivotAssist)} on:position={({ detail }) => updateLayout(index, detail)} />
{@const pivotInput = narrowWidgetProps(component.props, "PivotInput")}
{#if pivotInput}
<PivotInput {...exclude(pivotInput)} on:position={({ detail }) => updateLayout(index, detail)} />
{/if}
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
{#if popoverButton}
@@ -162,9 +160,9 @@
{#if separator}
<Separator {...exclude(separator)} />
{/if}
{@const swatchPairInput = narrowWidgetProps(component.props, "SwatchPairInput")}
{#if swatchPairInput}
<SwatchPairInput {...exclude(swatchPairInput)} />
{@const workingColorsButton = narrowWidgetProps(component.props, "WorkingColorsButton")}
{#if workingColorsButton}
<WorkingColorsButton {...exclude(workingColorsButton)} />
{/if}
{@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")}
{#if textAreaInput}
@@ -190,13 +188,13 @@
</div>
<style lang="scss" global>
.widget-column {
.widget-span.column {
flex: 0 0 auto;
display: flex;
flex-direction: column;
}
.widget-row.widget-row {
.widget-span.row {
flex: 0 0 auto;
display: flex;
min-height: 32px;
@@ -10,7 +10,7 @@
</script>
<LayoutRow class="breadcrumb-trail-buttons" {tooltip}>
{#each labels as label, index (index)}
{#each labels as label, index}
<TextButton {label} emphasized={index === labels.length - 1} {disabled} action={() => !disabled && index !== labels.length - 1 && action(index)} />
{/each}
</LayoutRow>
@@ -0,0 +1,83 @@
<script lang="ts">
import type { MenuListEntry } from "@graphite/wasm-communication/messages";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
export let entry: MenuListEntry;
let entryRef: MenuList;
$: (entry.ref = entryRef), entry.ref;
function clickEntry(e: MouseEvent) {
// If there's no menu to open, trigger the action but don't try to open its non-existant children
if ((entry.children?.length ?? 0) === 0) {
if (entry.action && !entry.disabled) entry.action();
return;
}
// Focus the target so that keyboard inputs are sent to the dropdown
(e.target as HTMLElement | undefined)?.focus();
if (entry.ref) {
entry.ref.open = true;
} else {
throw new Error("The menu bar floating menu has no associated ref");
}
}
</script>
<div class="menu-list-button">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
on:click={(e) => clickEntry(e)}
on:keydown={(e) => entry.ref?.keydown(e, false)}
class="entry"
class:open={entry.ref?.open}
tabindex="0"
data-floating-menu-spawner={(entry.children?.length ?? 0) > 0 ? "" : "no-hover-transfer"}
>
{#if entry.icon}
<IconLabel icon={entry.icon} />
{/if}
{#if entry.label}
<TextLabel>{entry.label}</TextLabel>
{/if}
</div>
{#if (entry.children?.length ?? 0) > 0}
<MenuList
on:open={({ detail }) => entry.ref && (entry.ref.open = detail)}
open={entry.ref?.open || false}
entries={entry.children || []}
direction="Bottom"
minWidth={240}
drawIcon={true}
bind:this={entryRef}
/>
{/if}
</div>
<style lang="scss" global>
.menu-list-button {
display: flex;
position: relative;
.entry {
display: flex;
align-items: center;
white-space: nowrap;
background: none;
padding: 0 8px;
margin: 0;
border: 0;
border-radius: 2px;
&:hover,
&.open {
background: var(--color-5-dullgray);
}
}
}
</style>
@@ -88,12 +88,12 @@
}
}
.widget-row > & + .text-button,
.widget-span.row > & + .text-button,
.layout-row > & + .text-button {
margin-left: 8px;
}
.widget-column > & + .text-button,
.widget-span.column > & + .text-button,
.layout-column > & + .text-button {
margin-top: 8px;
}
@@ -35,7 +35,7 @@
}
</script>
<LayoutCol class="swatch-pair">
<LayoutCol class="working-colors-button">
<LayoutRow class="primary swatch">
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0" />
<ColorPicker open={primaryOpen} on:open={({ detail }) => (primaryOpen = detail)} color={primary} on:color={({ detail }) => primaryColorChanged(detail)} direction="Right" />
@@ -47,7 +47,7 @@
</LayoutCol>
<style lang="scss" global>
.swatch-pair {
.working-colors-button {
flex: 0 0 auto;
.swatch {
@@ -1,140 +0,0 @@
<script lang="ts">
import { getContext, onMount } from "svelte";
import { platformIsMac } from "@graphite/utility-functions/platform";
import type { Editor } from "@graphite/wasm-communication/editor";
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@graphite/wasm-communication/messages";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
// TODO: Apparently, Safari does not support the Keyboard.lock() API but does relax its authority over certain keyboard shortcuts in fullscreen mode, which we should take advantage of
const accelKey = platformIsMac() ? "Command" : "Control";
const LOCK_REQUIRING_SHORTCUTS: KeyRaw[][] = [
[accelKey, "KeyW"],
[accelKey, "KeyN"],
[accelKey, "Shift", "KeyN"],
[accelKey, "KeyT"],
[accelKey, "Shift", "KeyT"],
];
const editor = getContext<Editor>("editor");
let entries: MenuListEntry[] = [];
function clickEntry(menuListEntry: MenuListEntry, e: MouseEvent) {
// If there's no menu to open, trigger the action but don't try to open its non-existant children
if (!menuListEntry.children || menuListEntry.children.length === 0) {
if (menuListEntry.action && !menuListEntry.disabled) menuListEntry.action();
return;
}
// Focus the target so that keyboard inputs are sent to the dropdown
(e.target as HTMLElement | undefined)?.focus();
if (menuListEntry.ref) {
menuListEntry.ref.open = true;
entries = entries;
} else {
throw new Error("The menu bar floating menu has no associated ref");
}
}
onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => {
const arraysEqual = (a: KeyRaw[], b: KeyRaw[]): boolean => a.length === b.length && a.every((aValue, i) => aValue === b[i]);
const shortcutRequiresLock = (shortcut: LayoutKeysGroup): boolean => {
const shortcutKeys = shortcut.map((keyWithLabel) => keyWithLabel.key);
// If this shortcut matches any of the browser-reserved shortcuts
return LOCK_REQUIRING_SHORTCUTS.some((lockKeyCombo) => arraysEqual(shortcutKeys, lockKeyCombo));
};
const menuBarEntryToMenuListEntry = (entry: MenuBarEntry): MenuListEntry => ({
// From `MenuEntryCommon`
...entry,
// Shared names with fields that need to be converted from the type used in `MenuBarEntry` to that of `MenuListEntry`
action: () => editor.instance.updateLayout(updateMenuBarLayout.layoutTarget, entry.action.widgetId, undefined),
children: entry.children ? entry.children.map((entries) => entries.map((entry) => menuBarEntryToMenuListEntry(entry))) : undefined,
// New fields in `MenuListEntry`
shortcutRequiresLock: entry.shortcut ? shortcutRequiresLock(entry.shortcut.keys) : undefined,
value: undefined,
disabled: entry.disabled ?? undefined,
font: undefined,
ref: undefined,
});
entries = updateMenuBarLayout.layout.map(menuBarEntryToMenuListEntry);
});
});
</script>
<div class="menu-bar-input" data-menu-bar-input>
{#each entries as entry, index (index)}
<div class="entry-container">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
on:click={(e) => clickEntry(entry, e)}
on:keydown={(e) => entry.ref?.keydown(e, false)}
class="entry"
class:open={entry.ref?.open}
tabindex="0"
data-floating-menu-spawner={entry.children && entry.children.length > 0 ? "" : "no-hover-transfer"}
>
{#if entry.icon}
<IconLabel icon={entry.icon} />
{/if}
{#if entry.label}
<TextLabel>{entry.label}</TextLabel>
{/if}
</div>
{#if entry.children && entry.children.length > 0}
<MenuList
on:open={({ detail }) => {
if (entry.ref) {
entry.ref.open = detail;
entries = entries;
}
}}
open={entry.ref?.open || false}
entries={entry.children || []}
direction="Bottom"
minWidth={240}
drawIcon={true}
bind:this={entry.ref}
/>
{/if}
</div>
{/each}
</div>
<style lang="scss" global>
.menu-bar-input {
display: flex;
.entry-container {
display: flex;
position: relative;
.entry {
display: flex;
align-items: center;
white-space: nowrap;
padding: 0 8px;
background: none;
border: 0;
margin: 0;
border-radius: 2px;
&:hover,
&.open {
background: var(--color-5-dullgray);
}
}
}
}
</style>
@@ -14,7 +14,7 @@
}
</script>
<div class="pivot-assist" class:disabled>
<div class="pivot-input" class:disabled>
<button on:click={() => setPosition("TopLeft")} class="row-1 col-1" class:active={position === "TopLeft"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setPosition("TopCenter")} class="row-1 col-2" class:active={position === "TopCenter"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setPosition("TopRight")} class="row-1 col-3" class:active={position === "TopRight"} tabindex="-1" {disabled}><div /></button>
@@ -27,7 +27,7 @@
</div>
<style lang="scss" global>
.pivot-assist {
.pivot-input {
position: relative;
flex: 0 0 auto;
width: 24px;
@@ -24,7 +24,7 @@
</script>
<LayoutRow class="radio-input" classes={{ disabled }}>
{#each entries as entry, index (index)}
{#each entries as entry, index}
<button
class:active={index === selectedIndex}
class:mixed={selectedIndex === undefined}
@@ -15,7 +15,7 @@
export let mediumDivisions = 5;
export let minorDivisions = 2;
let canvasRuler: HTMLDivElement | undefined;
let rulerInput: HTMLDivElement | undefined;
let rulerLength = 0;
let svgBounds = { width: "0px", height: "0px" };
@@ -76,11 +76,11 @@
}
export function resize() {
if (!canvasRuler) return;
if (!rulerInput) return;
const isVertical = direction === "Vertical";
const newLength = isVertical ? canvasRuler.clientHeight : canvasRuler.clientWidth;
const newLength = isVertical ? rulerInput.clientHeight : rulerInput.clientWidth;
const roundedUp = (Math.floor(newLength / majorMarkSpacing) + 1) * majorMarkSpacing;
if (roundedUp !== rulerLength) {
@@ -98,17 +98,17 @@
}
</script>
<div class={`canvas-ruler ${direction.toLowerCase()}`} bind:this={canvasRuler}>
<div class={`ruler-input ${direction.toLowerCase()}`} bind:this={rulerInput}>
<svg style:width={svgBounds.width} style:height={svgBounds.height}>
<path d={svgPath} />
{#each svgTexts as svgText, index (index)}
{#each svgTexts as svgText}
<text transform={svgText.transform}>{svgText.text}</text>
{/each}
</svg>
</div>
<style lang="scss" global>
.canvas-ruler {
.ruler-input {
flex: 1 1 100%;
background: var(--color-4-dimgray);
overflow: hidden;
@@ -103,7 +103,7 @@
});
</script>
<div class={`persistent-scrollbar ${direction.toLowerCase()}`}>
<div class={`scrollbar-input ${direction.toLowerCase()}`}>
<button class="arrow decrease" on:pointerdown={() => changePosition(-50)} tabindex="-1" />
<div class="scroll-track" bind:this={scrollTrack} on:pointerdown={grabArea}>
<div class="scroll-thumb" on:pointerdown={grabHandle} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
@@ -112,7 +112,7 @@
</div>
<style lang="scss" global>
.persistent-scrollbar {
.scrollbar-input {
display: flex;
flex: 1 1 100%;
@@ -120,11 +120,11 @@
<IconLabel class="user-input-label keyboard-lock-notice" icon="Info" tooltip={keyboardLockInfoMessage} />
{:else}
<LayoutRow class="user-input-label" classes={{ "text-only": textOnly }}>
{#each keysWithLabelsGroups as keysWithLabels, groupIndex (groupIndex)}
{#each keysWithLabelsGroups as keysWithLabels, groupIndex}
{#if groupIndex > 0}
<Separator type="Related" />
{/if}
{#each keyTextOrIconList(keysWithLabels) as keyInfo, keyIndex (keyIndex)}
{#each keyTextOrIconList(keysWithLabels) as keyInfo}
<div class={`input-key ${keyInfo.width}`}>
{#if keyInfo.icon}
<IconLabel icon={keyInfo.icon} />
@@ -3,12 +3,15 @@
</script>
<script lang="ts">
import { getContext } from "svelte";
import { getContext, onMount } from "svelte";
import type { PortfolioState } from "@graphite/state-providers/portfolio";
import { platformIsMac } from "@graphite/utility-functions/platform";
import type { Editor } from "@graphite/wasm-communication/editor";
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@graphite/wasm-communication/messages";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import MenuBarInput from "@graphite/components/widgets/inputs/MenuBarInput.svelte";
import MenuListButton from "@graphite/components/widgets/buttons/MenuListButton.svelte";
import WindowButtonsMac from "@graphite/components/window/title-bar/WindowButtonsMac.svelte";
import WindowButtonsWeb from "@graphite/components/window/title-bar/WindowButtonsWeb.svelte";
import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte";
@@ -17,25 +20,73 @@
export let platform: Platform;
export let maximized: boolean;
const editor = getContext<Editor>("editor");
const portfolio = getContext<PortfolioState>("portfolio");
// TODO: Apparently, Safari does not support the Keyboard.lock() API but does relax its authority over certain keyboard shortcuts in fullscreen mode, which we should take advantage of
const ACCEL_KEY = platformIsMac() ? "Command" : "Control";
const LOCK_REQUIRING_SHORTCUTS: KeyRaw[][] = [
[ACCEL_KEY, "KeyW"],
[ACCEL_KEY, "KeyN"],
[ACCEL_KEY, "Shift", "KeyN"],
[ACCEL_KEY, "KeyT"],
[ACCEL_KEY, "Shift", "KeyT"],
];
let entries: MenuListEntry[] = [];
$: docIndex = $portfolio.activeDocumentIndex;
$: displayName = $portfolio.documents[docIndex]?.displayName || "";
$: windowTitle = `${displayName}${displayName && " - "}Graphite`;
onMount(() => {
const arraysEqual = (a: KeyRaw[], b: KeyRaw[]): boolean => a.length === b.length && a.every((aValue, i) => aValue === b[i]);
const shortcutRequiresLock = (shortcut: LayoutKeysGroup): boolean => {
const shortcutKeys = shortcut.map((keyWithLabel) => keyWithLabel.key);
// If this shortcut matches any of the browser-reserved shortcuts
return LOCK_REQUIRING_SHORTCUTS.some((lockKeyCombo) => arraysEqual(shortcutKeys, lockKeyCombo));
};
editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => {
const menuBarEntryToMenuListEntry = (entry: MenuBarEntry): MenuListEntry => ({
// From `MenuEntryCommon`
...entry,
// Shared names with fields that need to be converted from the type used in `MenuBarEntry` to that of `MenuListEntry`
action: () => editor.instance.updateLayout(updateMenuBarLayout.layoutTarget, entry.action.widgetId, undefined),
children: entry.children ? entry.children.map((entries) => entries.map((entry) => menuBarEntryToMenuListEntry(entry))) : undefined,
// New fields in `MenuListEntry`
shortcutRequiresLock: entry.shortcut ? shortcutRequiresLock(entry.shortcut.keys) : undefined,
value: undefined,
disabled: entry.disabled ?? undefined,
font: undefined,
ref: undefined,
});
entries = updateMenuBarLayout.layout.map(menuBarEntryToMenuListEntry);
});
});
</script>
<LayoutRow class="title-bar">
<LayoutRow class="header-part">
<!-- Menu bar (or on Mac: window buttons) -->
<LayoutRow class="left">
{#if platform === "Mac"}
<WindowButtonsMac {maximized} />
{:else}
<MenuBarInput />
{#each entries as entry}
<MenuListButton {entry} />
{/each}
{/if}
</LayoutRow>
<LayoutRow class="header-part">
<!-- Document title -->
<LayoutRow class="center">
<WindowTitle text={windowTitle} />
</LayoutRow>
<LayoutRow class="header-part">
<!-- Window buttons (except on Mac) -->
<LayoutRow class="right">
{#if platform === "Windows" || platform === "Linux"}
<WindowButtonsWindows {maximized} />
{:else if platform === "Web"}
@@ -49,18 +100,18 @@
height: 28px;
flex: 0 0 auto;
.header-part {
> .layout-row {
flex: 1 1 100%;
&:nth-child(1) {
&.left {
justify-content: flex-start;
}
&:nth-child(2) {
&.center {
justify-content: center;
}
&:nth-child(3) {
&.right {
justify-content: flex-end;
}
}
@@ -60,7 +60,7 @@
<LayoutCol class="panel">
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }}>
<LayoutRow class="tab-group" scrollableX={true}>
{#each tabLabels as tabLabel, tabIndex (tabIndex)}
{#each tabLabels as tabLabel, tabIndex}
<LayoutRow
class="tab"
classes={{ active: tabIndex === tabActiveIndex }}
@@ -7,7 +7,7 @@
import type { FrontendDocumentDetails } from "@graphite/wasm-communication/messages";
import DialogModal from "@graphite/components/floating-menus/DialogModal.svelte";
import Dialog from "@graphite/components/floating-menus/Dialog.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import Panel from "@graphite/components/window/workspace/Panel.svelte";
@@ -124,7 +124,7 @@
</LayoutCol>
</LayoutRow>
{#if $dialog.visible}
<DialogModal />
<Dialog />
{/if}
</LayoutRow>