mirror of
https://github.com/gradio-app/gradio.git
synced 2025-03-31 12:20:26 +08:00
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:
parent
d6c289b346
commit
92139f3d7d
8
.changeset/light-nights-design.md
Normal file
8
.changeset/light-nights-design.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
"@gradio/atoms": minor
|
||||
"@gradio/icons": minor
|
||||
"@gradio/imageeditor": minor
|
||||
"gradio": minor
|
||||
---
|
||||
|
||||
feat:refresh the `ImageEditor` UI
|
@ -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}
|
@ -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():
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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 |
@ -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 |
19
js/icons/src/Layers.svelte
Normal file
19
js/icons/src/Layers.svelte
Normal 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 |
@ -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";
|
||||
|
@ -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)"
|
||||
|
@ -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");
|
||||
|
@ -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%;
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 */
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user