mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-26 02:28:12 +08:00
Replace text-only tooltips with custom richly styled tooltips (#3436)
* Replace the title attribute with custom FloatingMenu tooltips * Separate tooltip labels and descriptions into two styled blocks * Move keyboard shortcut tooltips to a separate section at the bottom * Update shortcut key styling in tooltips and hints bar * Fix .to_string()
This commit is contained in:
@@ -31,6 +31,14 @@
|
||||
blue: [0, 0, 1],
|
||||
magenta: [1, 0, 1],
|
||||
};
|
||||
const PURE_COLORS_GRAYABLE = [
|
||||
["Red", "#ff0000", "#4c4c4c"],
|
||||
["Yellow", "#ffff00", "#e3e3e3"],
|
||||
["Green", "#00ff00", "#969696"],
|
||||
["Cyan", "#00ffff", "#b2b2b2"],
|
||||
["Blue", "#0000ff", "#1c1c1c"],
|
||||
["Magenta", "#ff00ff", "#696969"],
|
||||
];
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
@@ -420,7 +428,13 @@
|
||||
>
|
||||
<LayoutCol class="pickers-and-gradient">
|
||||
<LayoutRow class="pickers">
|
||||
<LayoutCol class="saturation-value-picker" title={disabled ? "Saturation and value (disabled)" : "Saturation and value"} on:pointerdown={onPointerDown} data-saturation-value-picker>
|
||||
<LayoutCol
|
||||
class="saturation-value-picker"
|
||||
data-tooltip-label="Saturation and Value"
|
||||
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
|
||||
on:pointerdown={onPointerDown}
|
||||
data-saturation-value-picker
|
||||
>
|
||||
{#if !isNone}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
|
||||
{/if}
|
||||
@@ -434,12 +448,24 @@
|
||||
/>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="hue-picker" title={disabled ? "Hue (disabled)" : "Hue"} on:pointerdown={onPointerDown} data-hue-picker>
|
||||
<LayoutCol
|
||||
class="hue-picker"
|
||||
data-tooltip-label="Hue"
|
||||
data-tooltip-description={"The shade along the spectrum of the rainbow." + (disabled ? "\n\nDisabled (read-only)." : "")}
|
||||
on:pointerdown={onPointerDown}
|
||||
data-hue-picker
|
||||
>
|
||||
{#if !isNone}
|
||||
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="alpha-picker" title={disabled ? "Alpha (disabled)" : "Alpha"} on:pointerdown={onPointerDown} data-alpha-picker>
|
||||
<LayoutCol
|
||||
class="alpha-picker"
|
||||
data-tooltip-label="Alpha"
|
||||
data-tooltip-description={"The level of translucency." + (disabled ? "\n\nDisabled (read-only)." : "")}
|
||||
on:pointerdown={onPointerDown}
|
||||
data-alpha-picker
|
||||
>
|
||||
{#if !isNone}
|
||||
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
|
||||
{/if}
|
||||
@@ -479,11 +505,11 @@
|
||||
class="choice-preview"
|
||||
classes={{ outlined, transparency }}
|
||||
styles={{ "--outline-amount": outlineFactor }}
|
||||
tooltip={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before any change was made (right)" : "The present color choice"}
|
||||
tooltipDescription={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before it was changed (right)." : "The present color choice."}
|
||||
>
|
||||
{#if !newColor.equals(oldColor) && !disabled}
|
||||
<div class="swap-button-background"></div>
|
||||
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltip="Swap" />
|
||||
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltipLabel="Swap" />
|
||||
{/if}
|
||||
<LayoutCol class="new-color" classes={{ none: isNone }}>
|
||||
{#if !newColor.equals(oldColor)}
|
||||
@@ -496,9 +522,12 @@
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
<!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltip="Color model, color space, and HDR (coming soon)" /> -->
|
||||
<!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltipDescription="Color model, color space, and HDR (coming soon)." /> -->
|
||||
<LayoutRow>
|
||||
<TextLabel tooltip={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}>Hex</TextLabel>
|
||||
<TextLabel
|
||||
tooltipLabel="Hex Color Code"
|
||||
tooltipDescription={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}>Hex</TextLabel
|
||||
>
|
||||
<Separator type="Related" />
|
||||
<LayoutRow>
|
||||
<TextInput
|
||||
@@ -509,13 +538,14 @@
|
||||
setColorCode(detail);
|
||||
}}
|
||||
centered={true}
|
||||
tooltip={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}
|
||||
tooltipLabel="Hex Color Code"
|
||||
tooltipDescription={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}
|
||||
bind:this={hexCodeInputWidget}
|
||||
/>
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
<LayoutRow>
|
||||
<TextLabel tooltip="Red/Green/Blue channels of the color, integers 0–255">RGB</TextLabel>
|
||||
<TextLabel tooltipLabel="Red/Green/Blue" tooltipDescription="Integers 0–255.">RGB</TextLabel>
|
||||
<Separator type="Related" />
|
||||
<LayoutRow>
|
||||
{#each rgbChannels as [channel, strength], index}
|
||||
@@ -535,15 +565,17 @@
|
||||
min={0}
|
||||
max={255}
|
||||
minWidth={1}
|
||||
tooltip={`${{ r: "Red", g: "Green", b: "Blue" }[channel]} channel, integers 0–255`}
|
||||
tooltipLabel={{ r: "Red Channel", g: "Green Channel", b: "Blue Channel" }[channel]}
|
||||
tooltipDescription="Integers 0–255."
|
||||
/>
|
||||
{/each}
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
<LayoutRow>
|
||||
<TextLabel tooltip={"Hue/Saturation/Value, also known as Hue/Saturation/Brightness (HSB).\nNot to be confused with Hue/Saturation/Lightness (HSL), a different color model."}>
|
||||
HSV
|
||||
</TextLabel>
|
||||
<TextLabel
|
||||
tooltipLabel="Hue/Saturation/Value"
|
||||
tooltipDescription="Also known as Hue/Saturation/Brightness (HSB). Not to be confused with Hue/Saturation/Lightness (HSL), a different color model.">HSV</TextLabel
|
||||
>
|
||||
<Separator type="Related" />
|
||||
<LayoutRow>
|
||||
{#each hsvChannels as [channel, strength], index}
|
||||
@@ -565,17 +597,22 @@
|
||||
unit={channel === "h" ? "°" : "%"}
|
||||
minWidth={1}
|
||||
displayDecimalPlaces={1}
|
||||
tooltip={{
|
||||
h: `Hue component, the shade along the spectrum of the rainbow`,
|
||||
s: `Saturation component, the vividness from grayscale to full color`,
|
||||
v: "Value component, the brightness from black to full color",
|
||||
tooltipLabel={{
|
||||
h: "Hue Component",
|
||||
s: "Saturation Component",
|
||||
v: "Value Component",
|
||||
}[channel]}
|
||||
tooltipDescription={{
|
||||
h: "The shade along the spectrum of the rainbow.",
|
||||
s: "The vividness from grayscale to full color.",
|
||||
v: "The brightness from black to full color.",
|
||||
}[channel]}
|
||||
/>
|
||||
{/each}
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
<LayoutRow>
|
||||
<TextLabel tooltip="Scale of translucency, from transparent (0%) to opaque (100%), for the color's alpha channel">Alpha</TextLabel>
|
||||
<TextLabel tooltipLabel="Alpha" tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%).">Alpha</TextLabel>
|
||||
<Separator type="Related" />
|
||||
<NumberInput
|
||||
value={!isNone ? alpha * 100 : undefined}
|
||||
@@ -594,29 +631,54 @@
|
||||
unit="%"
|
||||
mode="Range"
|
||||
displayDecimalPlaces={1}
|
||||
tooltip="Scale of translucency, from transparent (0%) to opaque (100%), for the color's alpha channel"
|
||||
tooltipLabel="Alpha"
|
||||
tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%)."
|
||||
/>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="leftover-space" />
|
||||
<LayoutRow>
|
||||
{#if allowNone && !gradient}
|
||||
<button class="preset-color none" {disabled} on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
|
||||
<button
|
||||
class="preset-color none"
|
||||
{disabled}
|
||||
on:click={() => setColorPreset("none")}
|
||||
data-tooltip-label="Set to No Color"
|
||||
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
|
||||
tabindex="0"
|
||||
></button>
|
||||
<Separator type="Related" />
|
||||
{/if}
|
||||
<button class="preset-color black" {disabled} on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
|
||||
<button
|
||||
class="preset-color black"
|
||||
{disabled}
|
||||
on:click={() => setColorPreset("black")}
|
||||
data-tooltip-label="Set to Black"
|
||||
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
|
||||
tabindex="0"
|
||||
></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color white" {disabled} on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
|
||||
<button
|
||||
class="preset-color white"
|
||||
{disabled}
|
||||
on:click={() => setColorPreset("white")}
|
||||
data-tooltip-label="Set to White"
|
||||
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
|
||||
tabindex="0"
|
||||
></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color pure" {disabled} on:click={setColorPresetSubtile} tabindex="-1">
|
||||
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red" />
|
||||
<div data-pure-tile="yellow" style="--pure-color: #ffff00; --pure-color-gray: #e3e3e3" title="Set to yellow" />
|
||||
<div data-pure-tile="green" style="--pure-color: #00ff00; --pure-color-gray: #969696" title="Set to green" />
|
||||
<div data-pure-tile="cyan" style="--pure-color: #00ffff; --pure-color-gray: #b2b2b2" title="Set to cyan" />
|
||||
<div data-pure-tile="blue" style="--pure-color: #0000ff; --pure-color-gray: #1c1c1c" title="Set to blue" />
|
||||
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta" />
|
||||
{#each PURE_COLORS_GRAYABLE as [name, color, gray]}
|
||||
<div
|
||||
data-pure-tile={name.toLowerCase()}
|
||||
style:--pure-color={color}
|
||||
style:--pure-color-gray={gray}
|
||||
data-tooltip-label="Set to Red"
|
||||
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
|
||||
/>
|
||||
{/each}
|
||||
</button>
|
||||
<Separator type="Related" />
|
||||
<IconButton icon="Eyedropper" size={24} {disabled} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
|
||||
<IconButton icon="Eyedropper" size={24} {disabled} action={activateEyedropperSample} tooltipLabel="Eyedropper" tooltipDescription="Sample a pixel color from the document." />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
export let interactive = false;
|
||||
export let scrollableY = false;
|
||||
export let virtualScrollingEntryHeight = 0;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let tooltipLabel: string | undefined = undefined;
|
||||
export let tooltipDescription: string | undefined = undefined;
|
||||
export let tooltipShortcut: string | undefined = undefined;
|
||||
|
||||
// Keep the child references outside of the entries array so as to avoid infinite recursion.
|
||||
let childReferences: MenuList[][] = [];
|
||||
@@ -423,7 +425,9 @@
|
||||
class="row"
|
||||
classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }}
|
||||
styles={{ height: virtualScrollingEntryHeight || "20px" }}
|
||||
{tooltip}
|
||||
{tooltipLabel}
|
||||
{tooltipDescription}
|
||||
{tooltipShortcut}
|
||||
on:click={() => !entry.disabled && onEntryClick(entry)}
|
||||
on:pointerenter={() => !entry.disabled && onEntryPointerEnter(entry)}
|
||||
on:pointerleave={() => !entry.disabled && onEntryPointerLeave(entry)}
|
||||
|
||||
@@ -118,7 +118,13 @@
|
||||
<TextLabel>{nodeCategory[0]}</TextLabel>
|
||||
</summary>
|
||||
{#each nodeCategory[1].nodes as nodeType}
|
||||
<TextButton {disabled} label={nodeType.name} tooltip={$nodeGraph.nodeDescriptions.get(nodeType.name)} action={() => dispatch("selectNodeType", nodeType.name)} />
|
||||
<TextButton
|
||||
{disabled}
|
||||
label={nodeType.name}
|
||||
tooltipLabel={nodeType.name}
|
||||
tooltipDescription={$nodeGraph.nodeDescriptions.get(nodeType.name)}
|
||||
action={() => dispatch("selectNodeType", nodeType.name)}
|
||||
/>
|
||||
{/each}
|
||||
</details>
|
||||
{:else}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
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 TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
let self: FloatingMenu | undefined;
|
||||
|
||||
$: 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());
|
||||
|
||||
// TODO: Once all TODOs are replaced with real text, remove this function
|
||||
function filterTodo(text: string | undefined): string | undefined {
|
||||
if (text?.trim().toUpperCase() === "TODO" && !editor.handle.inDevelopmentMode()) return "";
|
||||
return text;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="tooltip" style:top={`${$tooltip.position.y}px`} style:left={`${$tooltip.position.x}px`}>
|
||||
{#if label || description}
|
||||
<FloatingMenu open={true} type="Tooltip" direction="Bottom" bind:this={self}>
|
||||
{#if label || shortcut}
|
||||
<LayoutRow class="tooltip-header">
|
||||
{#if label}
|
||||
<TextLabel class="tooltip-label">{label}</TextLabel>
|
||||
{/if}
|
||||
{#if shortcut}
|
||||
<TextLabel class="tooltip-shortcut">{shortcut}</TextLabel>
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
{#if description}
|
||||
<TextLabel class="tooltip-description">{description}</TextLabel>
|
||||
{/if}
|
||||
</FloatingMenu>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
.floating-menu-content {
|
||||
max-width: Min(400px, 50vw);
|
||||
|
||||
.tooltip-header + .tooltip-description {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.text-label {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.text-label + .tooltip-shortcut {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user