@gradio/imageeditor 0.19.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/Example.svelte +9 -3
  3. package/Index.svelte +23 -24
  4. package/InteractiveImageEditor.svelte +99 -61
  5. package/dist/Example.svelte +9 -3
  6. package/dist/Example.svelte.d.ts +4 -18
  7. package/dist/Index.svelte +23 -24
  8. package/dist/Index.svelte.d.ts +2 -19
  9. package/dist/InteractiveImageEditor.svelte +99 -61
  10. package/dist/InteractiveImageEditor.svelte.d.ts +15 -32
  11. package/dist/shared/Anchor.svelte +13 -11
  12. package/dist/shared/Anchor.svelte.d.ts +4 -19
  13. package/dist/shared/Controls.svelte +63 -39
  14. package/dist/shared/Controls.svelte.d.ts +13 -28
  15. package/dist/shared/IconButton.svelte +38 -17
  16. package/dist/shared/IconButton.svelte.d.ts +5 -19
  17. package/dist/shared/ImageEditor.svelte +235 -160
  18. package/dist/shared/ImageEditor.svelte.d.ts +55 -2
  19. package/dist/shared/Layers.svelte +43 -29
  20. package/dist/shared/Layers.svelte.d.ts +10 -25
  21. package/dist/shared/Resize.svelte +22 -16
  22. package/dist/shared/Resize.svelte.d.ts +6 -22
  23. package/dist/shared/SecondaryToolbar.svelte +34 -18
  24. package/dist/shared/SecondaryToolbar.svelte.d.ts +10 -25
  25. package/dist/shared/Toolbar.svelte +60 -76
  26. package/dist/shared/Toolbar.svelte.d.ts +9 -24
  27. package/dist/shared/brush/BrushOptions.svelte +49 -29
  28. package/dist/shared/brush/BrushOptions.svelte.d.ts +6 -21
  29. package/dist/shared/brush/BrushSize.svelte +12 -9
  30. package/dist/shared/brush/BrushSize.svelte.d.ts +5 -20
  31. package/dist/shared/brush/ColorField.svelte +21 -16
  32. package/dist/shared/brush/ColorField.svelte.d.ts +6 -22
  33. package/dist/shared/brush/ColorPicker.svelte +22 -16
  34. package/dist/shared/brush/ColorPicker.svelte.d.ts +4 -18
  35. package/dist/shared/brush/ColorSwatch.svelte +41 -21
  36. package/dist/shared/brush/ColorSwatch.svelte.d.ts +10 -24
  37. package/dist/shared/brush/brush.js +7 -8
  38. package/dist/shared/core/editor.d.ts +5 -0
  39. package/dist/shared/core/editor.js +43 -2
  40. package/dist/shared/resize/resize.d.ts +2 -0
  41. package/dist/shared/resize/resize.js +27 -18
  42. package/package.json +9 -9
  43. package/shared/Anchor.svelte +13 -11
  44. package/shared/Controls.svelte +63 -39
  45. package/shared/IconButton.svelte +38 -17
  46. package/shared/ImageEditor.svelte +235 -160
  47. package/shared/Layers.svelte +43 -29
  48. package/shared/Resize.svelte +22 -16
  49. package/shared/SecondaryToolbar.svelte +34 -18
  50. package/shared/Toolbar.svelte +60 -76
  51. package/shared/brush/BrushOptions.svelte +49 -29
  52. package/shared/brush/BrushSize.svelte +12 -9
  53. package/shared/brush/ColorField.svelte +21 -16
  54. package/shared/brush/ColorPicker.svelte +22 -16
  55. package/shared/brush/ColorSwatch.svelte +41 -21
  56. package/shared/brush/brush.ts +8 -10
  57. package/shared/core/editor.ts +49 -2
  58. package/shared/resize/resize.ts +28 -20
@@ -56,6 +56,7 @@ export interface ImageEditorContext {
56
56
  resize_canvas: (width: number, height: number) => void;
57
57
  reset: () => void;
58
58
  set_background_image: (image: Sprite) => void;
59
+ request_render: () => void;
59
60
  pad_bottom: number;
60
61
  }
61
62
  export declare class ImageEditor {
@@ -107,9 +108,13 @@ export declare class ImageEditor {
107
108
  private overlay_graphics;
108
109
  private pad_bottom;
109
110
  private theme_mode;
111
+ private render_idle_remaining;
112
+ private static readonly WAKE_EVENTS;
113
+ private wake_render_loop_bound;
110
114
  constructor(options: ImageEditorOptions);
111
115
  get context(): ImageEditorContext;
112
116
  init(): Promise<void>;
117
+ private wake_render_loop;
113
118
  private setup_containers;
114
119
  resize_canvas(width: number, height: number): void;
115
120
  reset(): void;
@@ -16,6 +16,7 @@ const spring_config = {
16
16
  stiffness: 0.45,
17
17
  damping: 0.8
18
18
  };
19
+ const RENDER_IDLE_TIMEOUT_MS = 500;
19
20
  class EditorState {
20
21
  state;
21
22
  scale;
@@ -114,6 +115,15 @@ export class ImageEditor {
114
115
  overlay_graphics;
115
116
  pad_bottom;
116
117
  theme_mode;
118
+ render_idle_remaining = RENDER_IDLE_TIMEOUT_MS;
119
+ static WAKE_EVENTS = [
120
+ "pointerdown",
121
+ "pointermove",
122
+ "pointerup",
123
+ "pointerenter",
124
+ "wheel"
125
+ ];
126
+ wake_render_loop_bound = this.wake_render_loop.bind(this);
117
127
  constructor(options) {
118
128
  this.pad_bottom = options.pad_bottom || 0;
119
129
  this.dark = options.dark || false;
@@ -178,7 +188,8 @@ export class ImageEditor {
178
188
  execute_command: this.execute_command.bind(this),
179
189
  resize_canvas: this.resize_canvas.bind(this),
180
190
  reset: this.reset.bind(this),
181
- set_background_image: this.set_background_image.bind(this)
191
+ set_background_image: this.set_background_image.bind(this),
192
+ request_render: this.wake_render_loop.bind(this)
182
193
  };
183
194
  }
184
195
  async init() {
@@ -222,14 +233,17 @@ export class ImageEditor {
222
233
  this.dimensions_value = dimensions;
223
234
  this.image_container.width = dimensions.width;
224
235
  this.image_container.height = dimensions.height;
236
+ this.wake_render_loop();
225
237
  });
226
238
  this.scale.subscribe((scale) => {
227
239
  this.scale_value = scale;
240
+ this.wake_render_loop();
228
241
  });
229
242
  this.position.subscribe((position) => {
230
243
  this.position_value = position;
244
+ this.wake_render_loop();
231
245
  });
232
- this.app.ticker.add(() => {
246
+ this.app.ticker.add((ticker) => {
233
247
  this.image_container.scale.set(this.scale_value);
234
248
  this.image_container.position.set(this.position_value.x, this.position_value.y);
235
249
  const effective_width = this.dimensions_value.width * this.scale_value;
@@ -283,9 +297,23 @@ export class ImageEditor {
283
297
  else {
284
298
  this.overlay_graphics.clear();
285
299
  }
300
+ this.render_idle_remaining -= ticker.deltaMS;
301
+ if (this.render_idle_remaining <= 0) {
302
+ ticker.stop();
303
+ }
286
304
  });
305
+ for (const event_name of ImageEditor.WAKE_EVENTS) {
306
+ canvas.addEventListener(event_name, this.wake_render_loop_bound);
307
+ }
308
+ this.wake_render_loop();
287
309
  this.ready_resolve();
288
310
  }
311
+ wake_render_loop() {
312
+ this.render_idle_remaining = RENDER_IDLE_TIMEOUT_MS;
313
+ if (this.app?.ticker && !this.app.ticker.started) {
314
+ this.app.ticker.start();
315
+ }
316
+ }
289
317
  setup_containers() {
290
318
  this.image_container = new Container({
291
319
  eventMode: "static",
@@ -355,6 +383,7 @@ export class ImageEditor {
355
383
  if (this.outline_container) {
356
384
  this.outline_container.position.set(app_center_x, app_center_y);
357
385
  }
386
+ this.wake_render_loop();
358
387
  }
359
388
  reset() {
360
389
  const zoom = this.tools.get("zoom");
@@ -366,6 +395,7 @@ export class ImageEditor {
366
395
  if (brush) {
367
396
  this.set_tool("draw");
368
397
  }
398
+ this.wake_render_loop();
369
399
  }
370
400
  async set_image_properties(properties) {
371
401
  let hard = typeof properties.animate !== "undefined" ? !properties.animate : true;
@@ -431,12 +461,14 @@ export class ImageEditor {
431
461
  for (const tool of this.tools.values()) {
432
462
  tool.set_tool(this.current_tool, this.current_subtool);
433
463
  }
464
+ this.wake_render_loop();
434
465
  }
435
466
  set_subtool(subtool) {
436
467
  this.current_subtool = subtool;
437
468
  for (const tool of this.tools.values()) {
438
469
  tool.set_tool(this.current_tool, this.current_subtool);
439
470
  }
471
+ this.wake_render_loop();
440
472
  }
441
473
  set_background_image(image) {
442
474
  this.background_image = image;
@@ -556,6 +588,7 @@ export class ImageEditor {
556
588
  this.event_callbacks.set(event, this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || []);
557
589
  }
558
590
  notify(event) {
591
+ this.wake_render_loop();
559
592
  for (const callback of this.event_callbacks.get(event) || []) {
560
593
  callback();
561
594
  }
@@ -563,6 +596,12 @@ export class ImageEditor {
563
596
  destroy() {
564
597
  if (!this.app)
565
598
  return;
599
+ const canvas = this.app.canvas;
600
+ if (canvas) {
601
+ for (const event_name of ImageEditor.WAKE_EVENTS) {
602
+ canvas.removeEventListener(event_name, this.wake_render_loop_bound);
603
+ }
604
+ }
566
605
  this.app?.destroy();
567
606
  }
568
607
  resize(width, height) {
@@ -587,8 +626,10 @@ export class ImageEditor {
587
626
  set_layer_options(layer_options) {
588
627
  this.layer_options = layer_options;
589
628
  this.layer_manager.set_layer_options(layer_options, this.width, this.height);
629
+ this.wake_render_loop();
590
630
  }
591
631
  toggle_layer_visibility(id) {
592
632
  this.layer_manager.toggle_layer_visibility(id);
633
+ this.wake_render_loop();
593
634
  }
594
635
  }
@@ -61,6 +61,8 @@ export declare class ResizeTool implements Tool {
61
61
  private dom_mouseup_handler;
62
62
  private event_callbacks;
63
63
  private last_scale;
64
+ private readonly handle_pointer_move_bound;
65
+ private readonly handle_pointer_up_bound;
64
66
  private original_state;
65
67
  /**
66
68
  * @method setup
@@ -84,6 +84,8 @@ export class ResizeTool {
84
84
  dom_mouseup_handler = null;
85
85
  event_callbacks = new Map();
86
86
  last_scale = 1;
87
+ handle_pointer_move_bound = this.handle_pointer_move.bind(this);
88
+ handle_pointer_up_bound = this.handle_pointer_up.bind(this);
87
89
  original_state = null;
88
90
  /**
89
91
  * @method setup
@@ -165,7 +167,7 @@ export class ResizeTool {
165
167
  * @returns {void}
166
168
  */
167
169
  cleanup() {
168
- if (this.resize_ui_container) {
170
+ if (this.resize_ui_container && this.image_editor_context?.app) {
169
171
  this.image_editor_context.app.stage.removeChild(this.resize_ui_container);
170
172
  }
171
173
  this.cleanup_event_listeners();
@@ -181,16 +183,20 @@ export class ResizeTool {
181
183
  this.current_tool = tool;
182
184
  this.current_subtool = subtool;
183
185
  if (tool === "image" && subtool === "size") {
184
- if (this.image_editor_context.background_image) {
185
- const storedBorderRegion = this.image_editor_context.background_image.borderRegion;
186
+ const context = this.image_editor_context;
187
+ if (!context)
188
+ return;
189
+ if (context.background_image) {
190
+ const storedBorderRegion = context.background_image
191
+ .borderRegion;
186
192
  if (typeof storedBorderRegion === "number" && storedBorderRegion > 0) {
187
193
  this.borderRegion = storedBorderRegion;
188
194
  }
189
195
  }
190
196
  this.show_resize_ui();
191
- if (this.image_editor_context.background_image) {
192
- const bg = this.image_editor_context.background_image;
193
- const container = this.image_editor_context.image_container;
197
+ if (context.background_image) {
198
+ const bg = context.background_image;
199
+ const container = context.image_container;
194
200
  const bg_width = bg.width;
195
201
  const bg_height = bg.height;
196
202
  const container_bounds = container.getLocalBounds();
@@ -685,9 +691,9 @@ export class ResizeTool {
685
691
  setup_event_listeners() {
686
692
  const stage = this.image_editor_context.app.stage;
687
693
  stage.eventMode = "static";
688
- stage.on("pointermove", this.handle_pointer_move.bind(this));
689
- stage.on("pointerup", this.handle_pointer_up.bind(this));
690
- stage.on("pointerupoutside", this.handle_pointer_up.bind(this));
694
+ stage.on("pointermove", this.handle_pointer_move_bound);
695
+ stage.on("pointerup", this.handle_pointer_up_bound);
696
+ stage.on("pointerupoutside", this.handle_pointer_up_bound);
691
697
  this.setup_dom_event_listeners();
692
698
  }
693
699
  /**
@@ -695,7 +701,10 @@ export class ResizeTool {
695
701
  */
696
702
  setup_dom_event_listeners() {
697
703
  this.cleanup_dom_event_listeners();
698
- const canvas = this.image_editor_context.app.canvas;
704
+ const context = this.image_editor_context;
705
+ if (!context?.app?.canvas)
706
+ return;
707
+ const canvas = context.app.canvas;
699
708
  this.dom_mousedown_handler = (e) => {
700
709
  if (this.current_subtool !== "size")
701
710
  return;
@@ -780,9 +789,9 @@ export class ResizeTool {
780
789
  * Cleans up DOM event listeners
781
790
  */
782
791
  cleanup_dom_event_listeners() {
783
- const canvas = this.image_editor_context.app.canvas;
792
+ const canvas = this.image_editor_context?.app?.canvas;
784
793
  if (this.dom_mousedown_handler) {
785
- canvas.removeEventListener("mousedown", this.dom_mousedown_handler);
794
+ canvas?.removeEventListener("mousedown", this.dom_mousedown_handler);
786
795
  this.dom_mousedown_handler = null;
787
796
  }
788
797
  if (this.dom_mousemove_handler) {
@@ -798,10 +807,10 @@ export class ResizeTool {
798
807
  * removes event listeners for the resize tool
799
808
  */
800
809
  cleanup_event_listeners() {
801
- const stage = this.image_editor_context.app.stage;
802
- stage.off("pointermove", this.handle_pointer_move.bind(this));
803
- stage.off("pointerup", this.handle_pointer_up.bind(this));
804
- stage.off("pointerupoutside", this.handle_pointer_up.bind(this));
810
+ const stage = this.image_editor_context?.app?.stage;
811
+ stage?.off("pointermove", this.handle_pointer_move_bound);
812
+ stage?.off("pointerup", this.handle_pointer_up_bound);
813
+ stage?.off("pointerupoutside", this.handle_pointer_up_bound);
805
814
  this.cleanup_dom_event_listeners();
806
815
  }
807
816
  /**
@@ -1197,7 +1206,7 @@ export class ResizeTool {
1197
1206
  this.resize_ui_container.visible = true;
1198
1207
  this.update_resize_ui();
1199
1208
  }
1200
- if (this.image_editor_context.background_image) {
1209
+ if (this.image_editor_context?.background_image) {
1201
1210
  const bg = this.image_editor_context.background_image;
1202
1211
  this.current_cursor = bg.cursor;
1203
1212
  bg.cursor = "move";
@@ -1212,7 +1221,7 @@ export class ResizeTool {
1212
1221
  if (this.resize_ui_container) {
1213
1222
  this.resize_ui_container.visible = false;
1214
1223
  }
1215
- if (this.image_editor_context.background_image) {
1224
+ if (this.image_editor_context?.background_image) {
1216
1225
  const bg = this.image_editor_context.background_image;
1217
1226
  bg.cursor = this.current_cursor;
1218
1227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/imageeditor",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -11,16 +11,16 @@
11
11
  "pixi-filters": "^6.1.4",
12
12
  "pixi.js": "^8.14.0",
13
13
  "tinycolor2": "^1.6.0",
14
- "@gradio/atoms": "^0.25.0",
15
- "@gradio/icons": "^0.15.1",
16
- "@gradio/image": "^0.27.0",
17
- "@gradio/statustracker": "^0.15.0",
18
- "@gradio/upload": "^0.18.0",
19
- "@gradio/utils": "^0.13.0",
20
- "@gradio/client": "^2.3.0"
14
+ "@gradio/atoms": "^0.26.1",
15
+ "@gradio/client": "^2.4.0",
16
+ "@gradio/image": "^0.28.1",
17
+ "@gradio/icons": "^0.16.0",
18
+ "@gradio/statustracker": "^0.15.2",
19
+ "@gradio/utils": "^0.14.0",
20
+ "@gradio/upload": "^0.18.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@gradio/preview": "^0.16.2"
23
+ "@gradio/preview": "^0.17.0"
24
24
  },
25
25
  "main_changeset": true,
26
26
  "main": "./Index.svelte",
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { spring } from "svelte/motion";
3
3
  import { Image } from "@gradio/icons";
4
- import { onMount, createEventDispatcher } from "svelte";
4
+ import { onMount } from "svelte";
5
5
 
6
6
  const positions = [
7
7
  "top-left",
@@ -14,9 +14,11 @@
14
14
  "bottom",
15
15
  "bottom-right"
16
16
  ] as const;
17
- const dispatch = createEventDispatcher<{
18
- position: (typeof positions)[number];
19
- }>();
17
+ let {
18
+ onposition
19
+ }: {
20
+ onposition?: (position: (typeof positions)[number]) => void;
21
+ } = $props();
20
22
 
21
23
  const spring_opt = {
22
24
  stiffness: 0.1,
@@ -35,8 +37,8 @@
35
37
  };
36
38
  const arrow_spring = spring<Arrow[]>([], spring_opt);
37
39
 
38
- let last_i = 0;
39
- let expanded = true;
40
+ let last_i = $state(0);
41
+ let expanded = $state(true);
40
42
  const box_size = (120 - 5 * 2) / 3;
41
43
 
42
44
  async function handle_box_hover(i: number): Promise<void> {
@@ -49,7 +51,7 @@
49
51
  dimensions.set(init);
50
52
  }
51
53
 
52
- let last_pos = 0;
54
+ let last_pos = $state(0);
53
55
  async function handle_box_click(i: number, stagger = false): Promise<void> {
54
56
  if (expanded && stagger) return;
55
57
 
@@ -77,7 +79,7 @@
77
79
  dimensions.set([120 + 10, 120 + 10]);
78
80
  arrow_spring.set(eventual_arrows);
79
81
 
80
- dispatch("position", positions[i]);
82
+ onposition?.(positions[i]);
81
83
  }
82
84
 
83
85
  function get_valid_offsets(anchorPoint: {
@@ -215,14 +217,14 @@
215
217
  class="box-wrap"
216
218
  role="grid"
217
219
  tabindex="0"
218
- on:mouseleave={() => handle_box_click(last_pos, true)}
220
+ onmouseleave={() => handle_box_click(last_pos, true)}
219
221
  >
220
222
  {#each { length: 9 } as _, i}
221
223
  <button
222
224
  class="box"
223
225
  class:active-box={last_pos === i}
224
- on:mouseenter={() => handle_box_hover(i)}
225
- on:click={() => handle_box_click(i)}
226
+ onmouseenter={() => handle_box_hover(i)}
227
+ onclick={() => handle_box_click(i)}
226
228
  >
227
229
  <span class:active={last_pos === i} class="icon-wrap">
228
230
  <Image />
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher } from "svelte";
3
2
  import { IconButton, IconButtonWrapper } from "@gradio/atoms";
4
3
  import {
5
4
  Check,
@@ -11,30 +10,45 @@
11
10
  Undo,
12
11
  Redo
13
12
  } from "@gradio/icons";
14
- import type { Writable } from "svelte/store";
15
-
16
- export let can_save = false;
17
- export let changeable = false;
18
- export let current_zoom = 1;
19
- export let tool: string;
20
- export let min_zoom = true;
21
- export let enable_download = false;
22
- export let can_undo: boolean;
23
- export let can_redo: boolean;
24
-
25
- const dispatch = createEventDispatcher<{
26
- remove_image: void;
27
- undo: void;
28
- redo: void;
29
- save: void;
30
- zoom_in: void;
31
- zoom_out: void;
32
- set_zoom: number | "fit";
33
- pan: void;
34
- download: void;
35
- }>();
36
-
37
- let show_zoom_popup = false;
13
+ let {
14
+ can_save = false,
15
+ changeable = false,
16
+ current_zoom = 1,
17
+ tool,
18
+ min_zoom = true,
19
+ enable_download = false,
20
+ can_undo,
21
+ can_redo,
22
+ onremove_image,
23
+ onundo,
24
+ onredo,
25
+ onsave,
26
+ onzoom_in,
27
+ onzoom_out,
28
+ onset_zoom,
29
+ onpan,
30
+ ondownload
31
+ }: {
32
+ can_save?: boolean;
33
+ changeable?: boolean;
34
+ current_zoom?: number;
35
+ tool: string;
36
+ min_zoom?: boolean;
37
+ enable_download?: boolean;
38
+ can_undo: boolean;
39
+ can_redo: boolean;
40
+ onremove_image?: () => void;
41
+ onundo?: () => void;
42
+ onredo?: () => void;
43
+ onsave?: () => void;
44
+ onzoom_in?: () => void;
45
+ onzoom_out?: () => void;
46
+ onset_zoom?: (zoom: number | "fit") => void;
47
+ onpan?: () => void;
48
+ ondownload?: () => void;
49
+ } = $props();
50
+
51
+ let show_zoom_popup = $state(false);
38
52
 
39
53
  function handle_zoom_click(e: MouseEvent): void {
40
54
  e.stopPropagation();
@@ -42,7 +56,7 @@
42
56
  }
43
57
 
44
58
  function handle_zoom_change(zoom: number | "fit"): void {
45
- dispatch("set_zoom", zoom);
59
+ onset_zoom?.(zoom);
46
60
  show_zoom_popup = false;
47
61
  }
48
62
 
@@ -52,7 +66,7 @@
52
66
  }
53
67
  }
54
68
 
55
- $: formatted_zoom = Math.round(current_zoom * 100);
69
+ let formatted_zoom = $derived(Math.round(current_zoom * 100));
56
70
  </script>
57
71
 
58
72
  <IconButtonWrapper>
@@ -61,7 +75,7 @@
61
75
  Icon={Download}
62
76
  label="Download"
63
77
  onclick={(event) => {
64
- dispatch("download");
78
+ ondownload?.();
65
79
  event.stopPropagation();
66
80
  }}
67
81
  />
@@ -72,7 +86,7 @@
72
86
  label="Pan"
73
87
  onclick={(e) => {
74
88
  e.stopPropagation();
75
- dispatch("pan");
89
+ onpan?.();
76
90
  }}
77
91
  highlight={tool === "pan"}
78
92
  size="small"
@@ -85,7 +99,7 @@
85
99
  Icon={ZoomOut}
86
100
  label="Zoom out"
87
101
  onclick={(event) => {
88
- dispatch("zoom_out");
102
+ onzoom_out?.();
89
103
  event.stopPropagation();
90
104
  }}
91
105
  />
@@ -93,7 +107,7 @@
93
107
  Icon={ZoomIn}
94
108
  label="Zoom in"
95
109
  onclick={(event) => {
96
- dispatch("zoom_in");
110
+ onzoom_in?.();
97
111
  event.stopPropagation();
98
112
  }}
99
113
  />
@@ -102,20 +116,30 @@
102
116
  <span
103
117
  role="button"
104
118
  tabindex="0"
105
- on:click={handle_zoom_click}
106
- on:keydown={handle_zoom_keydown}>{formatted_zoom}%</span
119
+ onclick={handle_zoom_click}
120
+ onkeydown={handle_zoom_keydown}>{formatted_zoom}%</span
107
121
  >
108
122
  {#if show_zoom_popup}
109
123
  <div class="zoom-controls">
110
124
  <ul>
111
125
  <li>
112
- <button on:click|stopPropagation={() => handle_zoom_change("fit")}>
126
+ <button
127
+ onclick={(event) => {
128
+ event.stopPropagation();
129
+ handle_zoom_change("fit");
130
+ }}
131
+ >
113
132
  Fit to screen
114
133
  </button>
115
134
  </li>
116
135
  {#each [0.25, 0.5, 1, 2, 4] as zoom}
117
136
  <li>
118
- <button on:click|stopPropagation={() => handle_zoom_change(zoom)}>
137
+ <button
138
+ onclick={(event) => {
139
+ event.stopPropagation();
140
+ handle_zoom_change(zoom);
141
+ }}
142
+ >
119
143
  {zoom * 100}%
120
144
  </button>
121
145
  </li>
@@ -130,7 +154,7 @@
130
154
  Icon={Undo}
131
155
  label="Undo"
132
156
  onclick={(event) => {
133
- dispatch("undo");
157
+ onundo?.();
134
158
  event.stopPropagation();
135
159
  }}
136
160
  disabled={!can_undo}
@@ -140,7 +164,7 @@
140
164
  Icon={Redo}
141
165
  label="Redo"
142
166
  onclick={(event) => {
143
- dispatch("redo");
167
+ onredo?.();
144
168
  event.stopPropagation();
145
169
  }}
146
170
  disabled={!can_redo}
@@ -152,7 +176,7 @@
152
176
  Icon={Check}
153
177
  label="Save changes"
154
178
  onclick={(event) => {
155
- dispatch("save");
179
+ onsave?.();
156
180
  event.stopPropagation();
157
181
  }}
158
182
  color="var(--color-accent)"
@@ -163,7 +187,7 @@
163
187
  Icon={Trash}
164
188
  label="Clear canvas"
165
189
  onclick={(event) => {
166
- dispatch("remove_image");
190
+ onremove_image?.();
167
191
  event.stopPropagation();
168
192
  }}
169
193
  />
@@ -1,23 +1,44 @@
1
1
  <script lang="ts">
2
2
  import { type Component } from "svelte";
3
3
  import type { ColorInput } from "tinycolor2";
4
- export let Icon: Component<any>;
5
- export let label = "";
6
- export let show_label = false;
7
- export let pending = false;
8
- export let size: "small" | "large" | "medium" = "small";
9
- export let padded = true;
10
- export let highlight = false;
11
- export let disabled = false;
12
- export let hasPopup = false;
13
- export let color: string | ColorInput = "var(--block-label-text-color)";
14
- export let transparent = false;
15
- export let background = "var(--background-fill-primary)";
16
- export let offset = 0;
17
- export let label_position: "left" | "right" = "left";
18
- export let roundedness: "quite" | "very" = "quite";
19
- export let onclick: ((event: MouseEvent) => void) | undefined = undefined;
20
- $: _color = highlight ? "var(--color-accent)" : color.toString();
4
+
5
+ let {
6
+ Icon,
7
+ label = "",
8
+ show_label = false,
9
+ pending = false,
10
+ size = "small",
11
+ padded = true,
12
+ highlight = false,
13
+ disabled = false,
14
+ hasPopup = false,
15
+ color = "var(--block-label-text-color)",
16
+ transparent = false,
17
+ background = "var(--background-fill-primary)",
18
+ offset = 0,
19
+ label_position = "left",
20
+ roundedness = "quite",
21
+ onclick = undefined
22
+ }: {
23
+ Icon: Component<any>;
24
+ label?: string;
25
+ show_label?: boolean;
26
+ pending?: boolean;
27
+ size?: "small" | "large" | "medium";
28
+ padded?: boolean;
29
+ highlight?: boolean;
30
+ disabled?: boolean;
31
+ hasPopup?: boolean;
32
+ color?: string | ColorInput;
33
+ transparent?: boolean;
34
+ background?: string;
35
+ offset?: number;
36
+ label_position?: "left" | "right";
37
+ roundedness?: "quite" | "very";
38
+ onclick?: (event: MouseEvent) => void;
39
+ } = $props();
40
+
41
+ let _color = $derived(highlight ? "var(--color-accent)" : color.toString());
21
42
  </script>
22
43
 
23
44
  <button