refresh the ImageEditor UI (#8042)

* changes

* icons

* asd

* add changeset

* ui

* fix empty text position

* fix button toggle

* cleanup

* add changeset

* lint

* fix test

* fix again

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
pngwn 2024-04-17 16:45:35 -04:00 committed by GitHub
parent d6c289b346
commit 92139f3d7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 454 additions and 323 deletions

View File

@ -0,0 +1,8 @@
---
"@gradio/atoms": minor
"@gradio/icons": minor
"@gradio/imageeditor": minor
"gradio": minor
---
feat:refresh the `ImageEditor` UI

View File

@ -1 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_editor_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "\n", "def predict(im):\n", " return im[\"composite\"]\n", "\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Group():\n", " with gr.Row():\n", " im = gr.ImageEditor(\n", " type=\"numpy\",\n", " crop_size=\"1:1\",\n", " elem_id=\"image_editor\",\n", " )\n", " im_preview = gr.Image()\n", " with gr.Group():\n", " with gr.Row():\n", "\n", " n_upload = gr.Label(\n", " 0,\n", " label=\"upload\",\n", " elem_id=\"upload\",\n", " )\n", " n_change = gr.Label(\n", " 0,\n", " label=\"change\",\n", " elem_id=\"change\",\n", " )\n", " n_input = gr.Label(\n", " 0,\n", " label=\"input\",\n", " elem_id=\"input\",\n", " )\n", " n_apply = gr.Label(\n", " 0,\n", " label=\"apply\",\n", " elem_id=\"apply\",\n", " )\n", " clear_btn = gr.Button(\"Clear\", elem_id=\"clear\")\n", "\n", " im.upload(\n", " lambda x: int(x) + 1, outputs=n_upload, inputs=n_upload, show_progress=\"hidden\"\n", " )\n", " im.change(\n", " lambda x: int(x) + 1, outputs=n_change, inputs=n_change, show_progress=\"hidden\"\n", " )\n", " im.input(\n", " lambda x: int(x) + 1, outputs=n_input, inputs=n_input, show_progress=\"hidden\"\n", " )\n", " im.apply(\n", " lambda x: int(x) + 1, outputs=n_apply, inputs=n_apply, show_progress=\"hidden\"\n", " )\n", " im.change(predict, outputs=im_preview, inputs=im, show_progress=\"hidden\")\n", " clear_btn.click(\n", " lambda: None,\n", " None,\n", " im,\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_editor_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "\n", "def predict(im):\n", " return im[\"composite\"]\n", "\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Group():\n", " with gr.Row():\n", " im = gr.ImageEditor(\n", " type=\"numpy\",\n", " crop_size=\"1:1\",\n", " elem_id=\"image_editor\",\n", " interactive=True,\n", " )\n", " im_preview = gr.Image()\n", " with gr.Group():\n", " with gr.Row():\n", "\n", " n_upload = gr.Label(\n", " 0,\n", " label=\"upload\",\n", " elem_id=\"upload\",\n", " )\n", " n_change = gr.Label(\n", " 0,\n", " label=\"change\",\n", " elem_id=\"change\",\n", " )\n", " n_input = gr.Label(\n", " 0,\n", " label=\"input\",\n", " elem_id=\"input\",\n", " )\n", " n_apply = gr.Label(\n", " 0,\n", " label=\"apply\",\n", " elem_id=\"apply\",\n", " )\n", " clear_btn = gr.Button(\"Clear\", elem_id=\"clear\")\n", "\n", " im.upload(\n", " lambda x: int(x) + 1, outputs=n_upload, inputs=n_upload, show_progress=\"hidden\"\n", " )\n", " im.change(\n", " lambda x: int(x) + 1, outputs=n_change, inputs=n_change, show_progress=\"hidden\"\n", " )\n", " im.input(\n", " lambda x: int(x) + 1, outputs=n_input, inputs=n_input, show_progress=\"hidden\"\n", " )\n", " im.apply(\n", " lambda x: int(x) + 1, outputs=n_apply, inputs=n_apply, show_progress=\"hidden\"\n", " )\n", " im.change(predict, outputs=im_preview, inputs=im, show_progress=\"hidden\")\n", " clear_btn.click(\n", " lambda: None,\n", " None,\n", " im,\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

View File

@ -12,6 +12,7 @@ with gr.Blocks() as demo:
type="numpy",
crop_size="1:1",
elem_id="image_editor",
interactive=True,
)
im_preview = gr.Image()
with gr.Group():

View File

@ -4,7 +4,7 @@
export let label = "";
export let show_label = false;
export let pending = false;
export let size: "small" | "large" = "small";
export let size: "small" | "large" | "medium" = "small";
export let padded = true;
export let highlight = false;
export let disabled = false;
@ -12,6 +12,7 @@
export let color = "var(--block-label-text-color)";
export let transparent = false;
export let background = "var(--background-fill-primary)";
export let offset = 0;
$: _color = highlight ? "var(--color-accent)" : color;
</script>
@ -27,9 +28,14 @@
class:transparent
style:color={!disabled && _color ? _color : "var(--block-label-text-color)"}
style:--bg-color={!disabled ? background : "auto"}
style:margin-left={offset + "px"}
>
{#if show_label}<span>{label}</span>{/if}
<div class:small={size === "small"} class:large={size === "large"}>
<div
class:small={size === "small"}
class:large={size === "large"}
class:medium={size === "medium"}
>
<Icon />
</div>
</button>
@ -65,22 +71,6 @@
border: 1px solid var(--button-secondary-border-color);
}
/* .padded {
padding: 2px;
background: var(--background-fill-primary);
box-shadow: var(--shadow-drop);
border: 1px solid var(--button-secondary-border-color);
} */
/* .padded {
padding: 2px;
background: var(--background-fill-primary);
box-shadow: var(--shadow-drop);
border: 1px solid var(--button-secondary-border-color);
} */
button:hover,
button.highlight {
cursor: pointer;
@ -109,6 +99,11 @@
height: 14px;
}
.medium {
width: 20px;
height: 20px;
}
.large {
width: 22px;
height: 22px;

View File

@ -12,13 +12,12 @@
display: flex;
max-height: 100%;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
height: auto;
align-items: flex-end;
padding-bottom: var(--spacing-xl);
color: var(--block-label-text-color);
flex-shrink: 0;
width: 95%;
}
.show_border {

View File

@ -5,6 +5,6 @@
viewBox="0 0 24 24"
><path
fill="currentColor"
d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10s10-4.47 10-10S17.53 2 12 2z"
d="M2.753 2.933a.75.75 0 0 1 .814-.68l3.043.272c2.157.205 4.224.452 5.922.732c1.66.273 3.073.594 3.844.983c.197.1.412.233.578.415c.176.192.352.506.28.9c-.067.356-.304.59-.487.729a3.001 3.001 0 0 1-.695.369c-1.02.404-2.952.79-5.984 1.169c-1.442.18-2.489.357-3.214.522c.205.045.43.089.674.132c.992.174 2.241.323 3.568.437a31.21 31.21 0 0 1 3.016.398c.46.087.893.186 1.261.296c.352.105.707.236.971.412c.13.086.304.225.42.437a.988.988 0 0 1 .063.141A1.75 1.75 0 0 0 14.5 12.25v.158c-.758.154-1.743.302-2.986.444c-2.124.243-3.409.55-4.117.859c-.296.128-.442.236-.508.3c.026.037.073.094.156.17c.15.138.369.29.65.45c.56.316 1.282.61 1.979.838l2.637.814a.75.75 0 1 1-.443 1.433l-2.655-.819c-.754-.247-1.58-.578-2.257-.96a5.082 5.082 0 0 1-.924-.65c-.255-.233-.513-.544-.62-.935c-.12-.441-.016-.88.274-1.244c.261-.328.656-.574 1.113-.773c.92-.4 2.387-.727 4.545-.974c1.366-.156 2.354-.313 3.041-.462a16.007 16.007 0 0 0-.552-.114a29.716 29.716 0 0 0-2.865-.378c-1.352-.116-2.649-.27-3.7-.454c-.524-.092-1-.194-1.395-.307c-.376-.106-.75-.241-1.021-.426a1.186 1.186 0 0 1-.43-.49a.934.934 0 0 1 .059-.873c.13-.213.32-.352.472-.442a3.23 3.23 0 0 1 .559-.251c.807-.287 2.222-.562 4.37-.83c2.695-.338 4.377-.666 5.295-.962c-.638-.21-1.623-.427-2.89-.635c-1.65-.273-3.679-.515-5.816-.718l-3.038-.272a.75.75 0 0 1-.68-.814M17 12.25a.75.75 0 0 0-1.5 0v4.19l-.72-.72a.75.75 0 1 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l2-2a.75.75 0 1 0-1.06-1.06l-.72.72z"
/></svg
>

Before

Width:  |  Height:  |  Size: 205 B

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,10 +1,6 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 24 24"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
><path
fill="currentColor"
d="M13.75 2a2.25 2.25 0 0 1 2.236 2.002V4h1.764A2.25 2.25 0 0 1 20 6.25V11h-1.5V6.25a.75.75 0 0 0-.75-.75h-2.129c-.404.603-1.091 1-1.871 1h-3.5c-.78 0-1.467-.397-1.871-1H6.25a.75.75 0 0 0-.75.75v13.5c0 .414.336.75.75.75h4.78a3.99 3.99 0 0 0 .505 1.5H6.25A2.25 2.25 0 0 1 4 19.75V6.25A2.25 2.25 0 0 1 6.25 4h1.764a2.25 2.25 0 0 1 2.236-2h3.5Zm2.245 2.096L16 4.25c0-.052-.002-.103-.005-.154ZM13.75 3.5h-3.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5ZM15 12a3 3 0 0 0-3 3v5c0 .556.151 1.077.415 1.524l3.494-3.494a2.25 2.25 0 0 1 3.182 0l3.494 3.494c.264-.447.415-.968.415-1.524v-5a3 3 0 0 0-3-3h-5Zm0 11a2.985 2.985 0 0 1-1.524-.415l3.494-3.494a.75.75 0 0 1 1.06 0l3.494 3.494A2.985 2.985 0 0 1 20 23h-5Zm5-7a1 1 0 1 1 0-2a1 1 0 0 1 0 2Z"
d="M13.75 2a2.25 2.25 0 0 1 2.236 2.002V4h1.764A2.25 2.25 0 0 1 20 6.25V11h-1.5V6.25a.75.75 0 0 0-.75-.75h-2.129c-.404.603-1.091 1-1.871 1h-3.5c-.78 0-1.467-.397-1.871-1H6.25a.75.75 0 0 0-.75.75v13.5c0 .414.336.75.75.75h4.78a4 4 0 0 0 .505 1.5H6.25A2.25 2.25 0 0 1 4 19.75V6.25A2.25 2.25 0 0 1 6.25 4h1.764a2.25 2.25 0 0 1 2.236-2zm2.245 2.096L16 4.25q0-.078-.005-.154M13.75 3.5h-3.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5M15 12a3 3 0 0 0-3 3v5c0 .556.151 1.077.415 1.524l3.494-3.494a2.25 2.25 0 0 1 3.182 0l3.494 3.494c.264-.447.415-.968.415-1.524v-5a3 3 0 0 0-3-3zm0 11a3 3 0 0 1-1.524-.415l3.494-3.494a.75.75 0 0 1 1.06 0l3.494 3.494A3 3 0 0 1 20 23zm5-7a1 1 0 1 1 0-2 1 1 0 0 1 0 2"
/></svg
>

Before

Width:  |  Height:  |  Size: 869 B

After

Width:  |  Height:  |  Size: 793 B

View File

@ -0,0 +1,19 @@
<svg
width="100%"
height="100%"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.35327 10.9495L6.77663 15.158C7.12221 15.4229 7.50051 15.5553 7.91154 15.5553C8.32258 15.5553 8.70126 15.4229 9.0476 15.158L14.471 10.9495"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
<path
d="M7.23461 11.4324C7.23406 11.432 7.2335 11.4316 7.23295 11.4312L1.81496 7.2268C1.81471 7.22661 1.81446 7.22641 1.8142 7.22621C1.52269 6.99826 1.39429 6.73321 1.39429 6.37014C1.39429 6.00782 1.52236 5.74301 1.81325 5.51507C1.8136 5.5148 1.81394 5.51453 1.81428 5.51426L7.2331 1.30812C7.45645 1.13785 7.67632 1.06653 7.91159 1.06653C8.14692 1.06653 8.36622 1.13787 8.58861 1.30787C8.58915 1.30828 8.58969 1.30869 8.59023 1.30911L14.0082 5.51462C14.0085 5.51485 14.0088 5.51507 14.0091 5.51529C14.3008 5.74345 14.4289 6.00823 14.4289 6.37014C14.4289 6.73356 14.3006 6.99862 14.01 7.22634C14.0096 7.22662 14.0093 7.22689 14.0089 7.22717L8.59007 11.4322C8.36672 11.6024 8.14686 11.6738 7.91159 11.6738C7.67628 11.6738 7.45699 11.6024 7.23461 11.4324Z"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -25,6 +25,7 @@ export { default as Info } from "./Info.svelte";
export { default as Image } from "./Image.svelte";
export { default as ImagePaste } from "./ImagePaste.svelte";
export { default as JSON } from "./JSON.svelte";
export { default as Layers } from "./Layers.svelte";
export { default as Like } from "./Like.svelte";
export { default as LineChart } from "./LineChart.svelte";
export { default as Maximise } from "./Maximise.svelte";

View File

@ -122,7 +122,7 @@
coords: { clientX: 100, clientY: 100 }
});
await userEvent.click(canvas.getByLabelText("Color button"));
await userEvent.click(canvas.getByLabelText("Draw button"));
var availableColors = document.querySelectorAll(
"button.color:not(.empty):not(.selected):not(.hidden)"

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { IconButton } from "@gradio/atoms";
import { Clear, Undo, Redo, Check } from "@gradio/icons";
import { Clear, Undo, Redo, Check, Trash } from "@gradio/icons";
/**
* Can the current image be undone?
@ -56,7 +56,7 @@
}}
/>
<IconButton
Icon={Clear}
Icon={Trash}
label="Clear canvas"
on:click={(event) => {
dispatch("remove_image");

View File

@ -28,6 +28,7 @@
child_bottom: number;
}>;
active_tool: Writable<tool>;
toolbar_box: Writable<DOMRect>;
crop: Writable<[number, number, number, number]>;
position_spring: Spring<{
x: number;
@ -74,6 +75,7 @@
export let crop_constraint = false;
let dimensions = writable(crop_size);
export let height = 0;
let editor_box: EditorContext["editor_box"] = writable({
parent_width: 0,
@ -90,6 +92,8 @@
child_bottom: 0
});
$: height = $editor_box.child_height;
const crop = writable<[number, number, number, number]>([0, 0, 1, 1]);
const position_spring = spring(
{ x: 0, y: 0 },
@ -117,6 +121,7 @@
PartialRecord<context_type, (dimensions?: typeof $dimensions) => void>
> = writable({});
const contexts: Writable<context_type[]> = writable([]);
const toolbar_box: Writable<DOMRect> = writable(new DOMRect());
const sort_order = ["bg", "layers", "crop", "draw", "erase"] as const;
const editor_context = setContext<EditorContext>(EDITOR_KEY, {
@ -124,6 +129,7 @@
current_layer: writable(null),
dimensions,
editor_box,
toolbar_box,
active_tool,
crop,
position_spring,
@ -334,7 +340,9 @@
style:transform="translate({$position_spring.x}px, {$position_spring.y}px)"
></div>
</div>
<slot />
<div class="tools-wrap">
<slot />
</div>
<div
class="border"
style:width="{$crop[2] * $editor_box.child_width}px"
@ -348,17 +356,18 @@
<style>
.wrap {
display: flex;
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
align-items: flex-start;
}
.border {
position: absolute;
border: var(--block-border-color) 1px solid;
pointer-events: none;
border-radius: var(--radius-md);
}
.stage-wrap {
@ -366,6 +375,16 @@
margin-bottom: var(--size-1);
}
.tools-wrap {
display: flex;
justify-content: center;
align-items: flex-end;
padding: 0 var(--spacing-xl) 0 0;
border: 1px solid var(--block-border-color);
border-radius: var(--radius-sm);
margin: var(--spacing-xxl) 0 var(--spacing-xxl) 0;
}
.image-container {
display: flex;
height: 100%;

View File

@ -188,6 +188,7 @@
}
let active_mode: "webcam" | "color" | null = null;
let editor_height = 0;
</script>
<BlockLabel
@ -197,6 +198,7 @@
/>
<ImageEditor
bind:this={editor}
bind:height={editor_height}
{changeable}
on:save
on:change={handle_change}
@ -207,6 +209,8 @@
crop_constraint={!!crop_constraint}
>
<Tools {i18n}>
<Layers layer_files={value?.layers || null} />
{#if sources && sources.length}
<Sources
{i18n}
@ -235,10 +239,8 @@
{/if}
</Tools>
<Layers layer_files={value?.layers || null} />
{#if !bg && !history && active_mode !== "webcam"}
<div class="empty wrap">
<div class="empty wrap" style:height={`${editor_height}px`}>
{#if sources && sources.length}
<div>Upload an image</div>
{/if}
@ -259,7 +261,6 @@
flex-direction: column;
justify-content: center;
align-items: center;
position: absolute;
height: 100%;
width: 100%;
@ -269,6 +270,7 @@
z-index: var(--layer-top);
text-align: center;
color: var(--body-text-color);
top: var(--size-8);
}
.wrap {
@ -279,11 +281,10 @@
min-height: var(--size-60);
color: var(--block-label-text-color);
line-height: var(--line-md);
height: 100%;
padding-top: var(--size-3);
/* height: 100%; */
font-size: var(--text-lg);
pointer-events: none;
transform: translateY(-30px);
/* margin-top: var(--size-8); */
}
.or {

View File

@ -1,10 +1,11 @@
<script lang="ts">
import { createEventDispatcher, getContext, onMount, tick } from "svelte";
import { DropdownArrow } from "@gradio/icons";
import { getContext, onMount, tick } from "svelte";
import { click_outside } from "../utils/events";
import { layer_manager, type LayerScene } from "./utils";
import { EDITOR_KEY, type EditorContext } from "../ImageEditor.svelte";
import type { FileData } from "@gradio/client";
import { Layers } from "@gradio/icons";
let show_layers = false;
@ -110,8 +111,10 @@
use:click_outside={() => (show_layers = false)}
>
<button aria-label="Show Layers" on:click={() => (show_layers = !show_layers)}
>Layers<span class="layer-toggle"><DropdownArrow /></span></button
>
><span class="icon"><Layers /></span> Layer {layers.findIndex(
(l) => l === $current_layer
) + 1}
</button>
{#if show_layers}
<ul>
{#each layers as layer, i (i)}
@ -129,50 +132,45 @@
</li>
</ul>
{/if}
<span class="sep"></span>
</div>
<style>
.layer-toggle {
width: 20px;
transform: rotate(0deg);
}
.closed .layer-toggle {
transform: rotate(-90deg);
.icon {
width: 14px;
margin-right: var(--spacing-md);
color: var(--block-label-text-color);
margin-right: var(--spacing-lg);
margin-top: 1px;
}
.layer-wrap {
position: absolute;
bottom: 0;
left: 0;
display: inline-block;
border: 1px solid var(--block-border-color);
border-radius: var(--radius-md);
transition: var(--button-transition);
box-shadow: var(--button-shadow);
text-align: left;
border-bottom: none;
border-left: none;
border-bottom-right-radius: 0;
border-top-left-radius: 0;
background-color: var(--background-fill-primary);
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.layer-wrap button {
display: inline-flex;
justify-content: flex-start;
align-items: flex-start;
padding: var(--size-2) var(--size-4);
width: 100%;
border-bottom: 1px solid var(--block-border-color);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--scale-000);
line-height: var(--line-sm);
padding-bottom: 1px;
margin-left: var(--spacing-xl);
padding: var(--spacing-sm) 0;
}
.layer-wrap li:last-child button {
border-bottom: none;
text-align: center;
padding: 2.5px;
}
.closed > button {
@ -180,12 +178,43 @@
}
.layer-wrap button:hover {
background-color: var(--background-fill-secondary);
background-color: none;
}
.layer-wrap button:hover .icon {
color: var(--color-accent);
}
.selected_layer {
background-color: var(--color-accent) !important;
color: white;
background-color: var(--block-background-fill);
font-weight: bold;
}
ul {
position: absolute;
bottom: 0;
left: 0;
background: #fff;
width: calc(100% + 1px);
list-style: none;
z-index: var(--layer-top);
border: 1px solid var(--block-border-color);
padding: var(--spacing-sm) 0;
text-wrap: none;
transform: translate(-1px, 1px);
border-radius: var(--radius-sm);
border-bottom-right-radius: 0;
}
.layer-wrap ul > li > button {
margin-left: 0;
}
.sep {
height: 12px;
background-color: var(--block-border-color);
width: 1px;
display: block;
margin-left: var(--spacing-xl);
}
</style>

View File

@ -23,7 +23,7 @@
color_mode: "fixed" | "defaults";
}
type brush_option_type = "color" | "size";
type brush_option_type = boolean;
</script>
<script lang="ts">
@ -32,11 +32,9 @@
import { getContext, onMount, tick } from "svelte";
import { type ToolContext, TOOL_KEY } from "./Tools.svelte";
import { Palette, BrushSize as SizeIcon } from "@gradio/icons";
import { type EditorContext, EDITOR_KEY } from "../ImageEditor.svelte";
import { draw_path, type DrawCommand } from "./brush";
import BrushColor from "./BrushColor.svelte";
import BrushSize from "./BrushSize.svelte";
import BrushOptions from "./BrushOptions.svelte";
import type { FederatedPointerEvent } from "pixi.js";
export let default_size: Brush["default_size"];
@ -46,7 +44,7 @@
export let mode: "erase" | "draw";
const processed_colors = colors
? colors.map(process_color).filter((_, i) => i < 5)
? colors.map(process_color).filter((_, i) => i < 4)
: [];
let selected_color =
@ -56,32 +54,7 @@
? "black"
: process_color(default_color);
const paint_meta = {
color: {
icon: Palette,
label: "Color",
order: 0,
id: "brush_color",
cb() {
current_option = "color";
}
},
size: {
icon: SizeIcon,
label: "Size",
order: 1,
id: "brush_size",
cb() {
current_option = "size";
}
}
} as const;
let brush_options: (typeof paint_meta)[brush_option_type][];
$: brush_options =
mode === "draw" ? Object.values(paint_meta) : [paint_meta.size];
let current_option: brush_option_type | null = null;
let brush_options: brush_option_type = false;
const {
pixi,
@ -90,7 +63,8 @@
command_manager,
register_context,
editor_box,
crop
crop,
toolbar_box
} = getContext<EditorContext>(EDITOR_KEY);
const { active_tool, register_tool, current_color } =
@ -216,8 +190,7 @@
onMount(() => {
const unregister = register_tool(mode, {
default: null,
options: brush_options
cb: toggle_options
});
return () => {
@ -232,19 +205,37 @@
return tinycolor(color).toRgbString();
}
$: {
if ($active_tool !== mode) {
current_option = null;
}
}
let pos = { x: 0, y: 0 };
$: brush_size =
(selected_size / $dimensions[0]) * $editor_box.child_width * 2;
function debounce_toggle(): (should_close?: boolean) => void {
let timeout: NodeJS.Timeout | null = null;
return function executedFunction(should_close?: boolean) {
const later = (): void => {
if (timeout) {
clearTimeout(timeout);
}
if (should_close !== undefined) {
brush_options = should_close;
return;
}
brush_options = !brush_options;
};
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(later, 100);
};
}
const toggle_options = debounce_toggle();
</script>
<svelte:window
on:keydown={({ key }) => key === "Escape" && (current_option = null)}
on:keydown={({ key }) => key === "Escape" && toggle_options(false)}
/>
<span
@ -260,23 +251,23 @@
style:opacity={brush_cursor ? 1 : 0}
/>
{#if current_option === "color" && colors}
{#if brush_options}
<div>
<BrushColor
on:click_outside={() => (current_option = null)}
<BrushOptions
show_swatch={mode === "draw"}
on:click_outside={() => toggle_options()}
colors={processed_colors}
bind:selected_color
{color_mode}
bind:recent_colors
bind:selected_size
dimensions={$dimensions}
parent_width={$editor_box.parent_width}
parent_height={$editor_box.parent_height}
parent_left={$editor_box.parent_left}
toolbar_box={$toolbar_box}
/>
</div>
{:else if current_option === "size"}
<BrushSize
on:click_outside={() => (current_option = null)}
max={$dimensions[0] / 10}
min={1}
bind:selected_size
/>
{/if}
<style>

View File

@ -6,11 +6,19 @@
import ColorPicker from "./ColorPicker.svelte";
import ColorSwatch from "./ColorSwatch.svelte";
import ColorField from "./ColorField.svelte";
import BrushSize from "./BrushSize.svelte";
export let colors: string[];
export let selected_color: string;
export let color_mode: Brush["color_mode"] | undefined = undefined;
export let recent_colors: (string | null)[] = [];
export let selected_size: number;
export let dimensions: [number, number];
export let parent_width: number;
export let parent_height: number;
export let parent_left: number;
export let toolbar_box: DOMRect;
export let show_swatch: boolean;
let color_picker = false;
let current_mode: "hex" | "rgb" | "hsl" = "hex";
@ -54,15 +62,29 @@
let c_width = 0;
let c_height = 0;
let wrap_el: HTMLDivElement;
let anchor_right = false;
let anchor_top = false;
let full_screen = false;
let anchor: "default" | "center" | "top" = "default";
let left_anchor = 0;
let top_anchor = null;
$: {
if (wrap_el && (width || height || c_height || c_width)) {
const box = wrap_el.getBoundingClientRect();
color_picker;
anchor_right = box.width + 30 > width / 2;
anchor_top = box.y < 80;
if (box.width > parent_width) {
full_screen = true;
} else if (parent_width > box.width + 230 * 2 + 25) {
anchor = "default";
left_anchor = toolbar_box.right - parent_left + 24;
} else {
anchor = "center";
}
if (box.height + 50 > parent_height) {
anchor = "top";
top_anchor = true;
}
}
}
</script>
@ -73,12 +95,13 @@
class="wrap"
class:padded={!color_picker}
use:click_outside={() => dispatch("click_outside")}
class:right={anchor_right}
class:top={anchor_top}
class:bottom={!anchor_top}
class:anchor_default={anchor === "default"}
class:anchor_center={anchor === "center"}
class:anchor_top={anchor === "top"}
bind:this={wrap_el}
bind:clientWidth={c_width}
bind:clientHeight={c_height}
class:anchor={`${left_anchor}px`}
>
{#if color_mode === "defaults"}
{#if color_picker}
@ -91,49 +114,67 @@
/>
{/if}
{/if}
<ColorSwatch
bind:color_picker
{colors}
on:select={({ detail }) => handle_color_selection(detail, "core")}
on:edit={({ detail }) => handle_color_selection(detail, "user")}
user_colors={color_mode === "defaults" ? recent_colors : null}
{selected_color}
{current_mode}
/>
{#if show_swatch}
<ColorSwatch
bind:color_picker
{colors}
on:select={({ detail }) => handle_color_selection(detail, "core")}
on:edit={({ detail }) => handle_color_selection(detail, "user")}
user_colors={color_mode === "defaults" ? recent_colors : null}
{selected_color}
{current_mode}
/>
{/if}
{#if color_picker || show_swatch}
<div class="sep"></div>
{/if}
<BrushSize max={dimensions[0] / 10} min={1} bind:selected_size />
</div>
<style>
.wrap {
width: 230px;
max-width: 230px;
width: 90%;
position: absolute;
display: flex;
flex-direction: column;
gap: 5px;
background: var(--background-fill-secondary);
border: 1px solid var(--block-border-color);
border-radius: var(--radius-md);
box-shadow:
0 0 5px rgba(0, 0, 0, 0.1),
0 5px 30px rgba(0, 0, 0, 0.2);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
padding-bottom: var(--size-2);
pointer-events: all;
cursor: default;
z-index: var(--layer-top);
overflow: hidden;
}
.bottom {
bottom: 85px;
.anchor_default {
position: absolute;
left: var(--left-anchor);
bottom: 8px;
}
.anchor_center {
position: absolute;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
}
.top {
top: 30px;
.anchor_top {
position: absolute;
top: 1px;
left: 50%;
transform: translateX(-50%);
}
.right {
right: 10px;
}
.padded {
padding-top: var(--size-3);
.sep {
height: 1px;
background-color: var(--block-border-color);
margin: 0 var(--size-3);
margin-top: var(--size-1);
}
</style>

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { click_outside } from "../utils/events";
import { createEventDispatcher } from "svelte";
import { BrushSize } from "@gradio/icons";
export let selected_size: number;
export let min: number;
@ -40,36 +41,28 @@
class:top={anchor_top}
class:bottom={!anchor_top}
>
<span>
<BrushSize />
</span>
<input type="range" bind:value={selected_size} {min} {max} step={1} />
</div>
<style>
.wrap {
width: 180px;
position: absolute;
width: 100%;
display: flex;
flex-direction: column;
gap: 5px;
gap: var(--size-4);
background: var(--background-fill-secondary);
border: 1px solid var(--block-border-color);
border-radius: var(--radius-md);
box-shadow:
0 0 5px rgba(0, 0, 0, 0.1),
0 5px 30px rgba(0, 0, 0, 0.2);
padding: var(--size-2);
padding: 0 var(--size-4);
cursor: default;
padding-top: var(--size-2-5);
}
.bottom {
bottom: 85px;
input {
width: 100%;
}
.top {
top: 30px;
}
.right {
right: 10px;
span {
width: 26px;
color: var(--body-text-color);
}
</style>

View File

@ -4,6 +4,7 @@
export let selected_color: string;
export let colors: string[];
export let user_colors: (string | null)[] | null = [];
import { Palette } from "@gradio/icons";
export let show_empty = false;
export let current_mode: "hex" | "rgb" | "hsl" = "hex";
@ -50,44 +51,75 @@
}
</script>
{#if user_colors}
<div class="swatch">
{#each user_colors as color, i}
<button
on:click={() => handle_select("edit", { index: i, color })}
class="color"
class:empty={color === null}
style="background-color: {color}"
class:selected={`edit-${i}` === current_index}
></button>
{/each}
<button
on:click={handle_picker_click}
class="color colorpicker"
class:hidden={!color_picker}
></button>
</div>
{#if !color_picker}
<span class:lg={user_colors}></span>
{/if}
<menu class="swatch">
{#each _colors as color, i}
<button
on:click={() => handle_select("select", { index: i, color })}
class="color"
class:empty={color === null}
style="background-color: {color}"
class:selected={`select-${i}` === current_index}
></button>
{/each}
</menu>
<div class="swatch-wrap">
<span class="icon-wrap">
<Palette />
</span>
<div>
{#if user_colors}
<div class="swatch">
{#each user_colors as color, i}
<button
on:click={() => handle_select("edit", { index: i, color })}
class="color"
class:empty={color === null}
style="background-color: {color}"
class:selected={`edit-${i}` === current_index}
></button>
{/each}
<button
on:click={handle_picker_click}
class="color colorpicker"
class:hidden={!color_picker}
></button>
</div>
{/if}
<menu class="swatch">
{#each _colors as color, i}
<button
on:click={() => handle_select("select", { index: i, color })}
class="color"
class:empty={color === null}
style="background-color: {color}"
class:selected={`select-${i}` === current_index}
></button>
{/each}
</menu>
</div>
</div>
<style>
.icon-wrap {
width: 18px;
margin-top: 6px;
margin-left: var(--size-4);
}
.swatch-wrap {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
span {
margin-top: 0px;
}
span.lg {
margin-top: var(--spacing-xl);
}
.swatch {
display: flex;
width: 100%;
gap: var(--size-2);
justify-content: center;
margin-bottom: var(--size-1);
gap: var(--size-2-5);
justify-content: space-around;
margin-bottom: var(--size-2);
margin-right: var(--size-6);
padding-left: var(--size-2);
}
.empty {
@ -96,7 +128,7 @@
display: flex;
justify-content: center;
align-items: center;
padding-top: 1px;
padding-top: 4px;
text-align: center;
font-size: var(--scale-0);
cursor: pointer;

View File

@ -170,12 +170,7 @@
$: $editor_box && get_measurements();
onMount(() =>
register_tool("crop", {
default: "crop",
options: []
})
);
onMount(() => register_tool("crop"));
</script>
{#if $active_tool === "crop" && measured}
@ -191,6 +186,3 @@
on:crop_end={({ detail }) => handle_crop("stop", detail)}
/>
{/if}
<style>
</style>

View File

@ -332,6 +332,7 @@
left: 0;
width: 100%;
height: 100%;
/* transform: translateY(-20px); */
}
@ -340,5 +341,6 @@
width: 100%;
height: 100%;
border: 1px solid black;
z-index: var(--layer-top);
}
</style>

View File

@ -122,7 +122,7 @@
--hitbox-corner-offset: -25px;
--handle-corner-offset: calc(50% - 5px);
--handle-mid-offset: calc(50% - 2px);
z-index: 1;
z-index: var(--layer-top);
}
/* hitbox positions */

View File

@ -3,13 +3,14 @@
import { type ToolContext, TOOL_KEY } from "./Tools.svelte";
import { type EditorContext, EDITOR_KEY } from "../ImageEditor.svelte";
import {
Upload as UploadIcon,
Image as ImageIcon,
Webcam as WebcamIcon,
ImagePaste
} from "@gradio/icons";
import { Upload } from "@gradio/upload";
import { Webcam } from "@gradio/image";
import { type I18nFormatter } from "@gradio/utils";
import { IconButton } from "@gradio/atoms";
import { add_bg_color, add_bg_image } from "./sources";
import type { FileData } from "@gradio/client";
@ -24,7 +25,7 @@
export let mirror_webcam = true;
export let i18n: I18nFormatter;
const { active_tool, register_tool } = getContext<ToolContext>(TOOL_KEY);
const { active_tool } = getContext<ToolContext>(TOOL_KEY);
const { pixi, dimensions, register_context, reset, editor_box } =
getContext<EditorContext>(EDITOR_KEY);
@ -37,12 +38,14 @@
const sources_meta = {
upload: {
icon: UploadIcon,
icon: ImageIcon,
label: "Upload",
order: 0,
id: "bg_upload",
cb() {
upload.open_file_upload();
$active_tool = "bg";
}
},
webcam: {
@ -52,6 +55,7 @@
id: "bg_webcam",
cb() {
active_mode = "webcam";
$active_tool = "bg";
}
},
clipboard: {
@ -61,6 +65,7 @@
id: "bg_clipboard",
cb() {
process_clipboard();
$active_tool = null;
}
}
} as const;
@ -152,53 +157,58 @@
},
reset_fn: () => {}
});
onMount(() => {
return register_tool("bg", {
default: "bg_upload",
options: sources_list || []
});
});
</script>
<svelte:window on:keydown={handle_key} />
{#if $active_tool === "bg"}
<div class="upload-container">
<Upload
hidden={true}
bind:this={upload}
filetype="image/*"
on:load={handle_upload}
on:error
{root}
disable_click={!sources.includes("upload")}
format="blob"
></Upload>
{#if active_mode === "webcam"}
<div
class="modal"
style:max-width="{$editor_box.child_width}px"
style:max-height="{$editor_box.child_height}px"
style:top="{$editor_box.child_top - $editor_box.parent_top}px"
>
<div class="modal-inner">
<Webcam
{root}
on:capture={handle_upload}
on:error
on:drag
{mirror_webcam}
streaming={false}
mode="image"
include_audio={false}
{i18n}
/>
</div>
<div class="source-wrap">
{#each sources_list as { icon, label, id, cb } (id)}
<IconButton
Icon={icon}
size="medium"
padded={false}
label={label + " button"}
hasPopup={true}
transparent={true}
on:click={cb}
/>
{/each}
<span class="sep"></span>
</div>
<div class="upload-container">
<Upload
hidden={true}
bind:this={upload}
filetype="image/*"
on:load={handle_upload}
on:error
{root}
disable_click={!sources.includes("upload")}
format="blob"
></Upload>
{#if active_mode === "webcam"}
<div
class="modal"
style:max-width="{$editor_box.child_width}px"
style:max-height="{$editor_box.child_height}px"
style:top="{$editor_box.child_top - $editor_box.parent_top}px"
>
<div class="modal-inner">
<Webcam
{root}
on:capture={handle_upload}
on:error
on:drag
{mirror_webcam}
streaming={false}
mode="image"
include_audio={false}
{i18n}
/>
</div>
{/if}
</div>
{/if}
</div>
{/if}
</div>
<style>
.modal {
@ -216,4 +226,20 @@
.modal-inner {
width: 100%;
}
.sep {
height: 12px;
background-color: var(--block-border-color);
width: 1px;
display: block;
margin-left: var(--spacing-xl);
}
.source-wrap {
display: flex;
justify-content: center;
align-items: center;
margin-left: var(--spacing-lg);
height: 100%;
}
</style>

View File

@ -28,18 +28,16 @@
}
export interface ToolContext {
register_tool: (type: tool, meta: ToolMeta) => () => void;
register_tool: (type: tool, opts?: { cb: () => void }) => () => void;
active_tool: {
set: (tool: tool) => void;
subscribe(
this: void,
run: Subscriber<tool | null>,
invalidate?: Invalidator<tool | null>
): Unsubscriber;
};
activate_subtool: (
sub_tool: upload_tool | transform_tool | brush_tool | eraser_tool | null,
cb?: (...args: any[]) => void
) => void;
current_color: Writable<string>;
}
</script>
@ -52,29 +50,22 @@
import { Image, Crop, Brush, Erase } from "@gradio/icons";
import { type I18nFormatter } from "@gradio/utils";
const { current_history, active_tool } =
const { active_tool, toolbar_box, editor_box } =
getContext<EditorContext>(EDITOR_KEY);
export let i18n: I18nFormatter;
let tools: tool[] = [];
const metas: Record<tool, ToolMeta | null> = {
draw: null,
erase: null,
crop: null,
bg: null
};
$: sub_menu = $active_tool && metas[$active_tool];
const cbs: Record<string, () => void> = {};
let current_color = writable("#000000");
let sub_tool: upload_tool | transform_tool | brush_tool | eraser_tool | null;
const tool_context: ToolContext = {
current_color,
register_tool: (type: tool, meta: ToolMeta) => {
register_tool: (type: tool, opts?: { cb: () => void }) => {
tools = [...tools, type];
metas[type] = meta;
if (opts?.cb) {
cbs[type] = opts.cb;
}
return () => {
tools = tools.filter((tool) => tool !== type);
@ -82,15 +73,8 @@
},
active_tool: {
subscribe: active_tool.subscribe
},
activate_subtool: (
_sub_tool: upload_tool | transform_tool | brush_tool | eraser_tool | null,
cb?: (...args: any[]) => void
) => {
sub_tool = _sub_tool;
if (cb) cb();
subscribe: active_tool.subscribe,
set: active_tool.set
}
};
@ -104,11 +88,6 @@
icon: typeof Image;
}
> = {
bg: {
order: 0,
label: i18n("Image"),
icon: Image
},
crop: {
order: 1,
label: i18n("Transform"),
@ -123,44 +102,49 @@
order: 2,
label: i18n("Erase"),
icon: Erase
},
bg: {
order: 0,
label: i18n("Background"),
icon: Image
}
} as const;
let toolbar_width: number;
let toolbar_wrap: HTMLDivElement;
$: toolbar_width, $editor_box, get_dimensions();
function get_dimensions(): void {
if (!toolbar_wrap) return;
$toolbar_box = toolbar_wrap.getBoundingClientRect();
}
function handle_click(e: Event, tool: tool): void {
e.stopPropagation();
$active_tool = tool;
cbs[tool] && cbs[tool]();
}
</script>
<slot />
<div class="toolbar-wrap">
<Toolbar show_border={false}>
{#if sub_menu}
{#each sub_menu.options as meta (meta.id)}
<IconButton
highlight={sub_tool === meta.id && meta.id !== "brush_size"}
color={$active_tool === "draw" && meta.id === "brush_size"
? $current_color
: undefined}
on:click={() => tool_context.activate_subtool(meta.id, meta.cb)}
Icon={meta.icon}
size="large"
padded={false}
label={meta.label + " button"}
hasPopup={true}
transparent={true}
/>
{/each}
{/if}
</Toolbar>
<div
class="toolbar-wrap"
bind:clientWidth={toolbar_width}
bind:this={toolbar_wrap}
>
<Toolbar show_border={false}>
{#each tools as tool (tool)}
<IconButton
disabled={tool === "bg" && !!$current_history.previous}
highlight={$active_tool === tool}
on:click={() => ($active_tool = tool)}
on:click={(e) => handle_click(e, tool)}
Icon={tools_meta[tool].icon}
size="large"
size="medium"
padded={false}
label={tools_meta[tool].label + " button"}
transparent={true}
offset={tool === "draw" ? -2 : tool === "erase" ? -6 : 0}
/>
{/each}
</Toolbar>
@ -169,9 +153,9 @@
<style>
.toolbar-wrap {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
margin-left: var(--spacing-xl);
height: 100%;
}
</style>

View File

@ -1,3 +1,5 @@
import { tick } from "svelte";
/**
* Svelte action to handle clicks outside of a DOM node
* @param node DOM node to check the click is outside of