From 42ae2d8453924e5999874e34242454b7874c46a1 Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:23:47 +0800 Subject: [PATCH 1/3] feat(example): rework menu examples (#15684) * Migrate to svelte ts for API example * Clean up * Use `$derived` `selectedWebview` * Do the change in onchange instead of state effects * Set initial x y and sizes * revert ai slop on main.ts * More cleanup * Remove ConsoleMessage * Remove View type * license header * Use `ViewProps` * Use same button spacing through out * Move theme example to top level theme switch * Fix menu views * More cleanups * Merge branch 'dev' into api-example-svelte-ts * Final cleanup * Move types to svelte files * fallback to empty string not false * Initial work * Bump deps * Make dragging with states work * Fix sorting * Move everything to `MenuBuilder.svelte` * Add remove button * Fix Menu and Tray * Make menu items reactive * Sync checked states * Add placeholder to menu builder * Set user-select to none * Fix text in the console * Rework tray page layout * Close all menus on MenuBuilder unmounted * Clean up menus properly * Add initial text and icon on the menu builders * Move MenuItems to MenuBuilder * format * Merge branch 'dev' into api-example-svelte-ts * Merge branch 'api-example-svelte-ts' into menu-example-rework * Re-create all the elements as a work around * Reorder the menu items on builder changes * On one tray for the tray page * Update menus on add and remove * Same for the tray * Space and align checkboxes * Merge remote-tracking branch 'upstream/dev' into menu-example-rework * Clean up menus properly * Merge branch 'dev' into menu-example-rework * Remove duplicated webview in new command * Merge branch 'dev' into menu-example-rework --- crates/tauri-macros/src/lib.rs | 42 +- crates/tauri/src/menu/plugin.rs | 5 +- examples/api/package.json | 12 +- examples/api/src-tauri/src/lib.rs | 1 + .../api/src/components/MenuBuilder.svelte | 326 +++++-- .../api/src/components/MenuItemBuilder.svelte | 163 ---- .../src/components/MenuItemComponent.svelte | 79 ++ examples/api/src/views/Menu.svelte | 95 +- examples/api/src/views/Tray.svelte | 157 ++-- examples/api/src/views/Window.svelte | 18 +- pnpm-lock.yaml | 836 ++++++++++-------- 11 files changed, 995 insertions(+), 739 deletions(-) delete mode 100644 examples/api/src/components/MenuItemBuilder.svelte create mode 100644 examples/api/src/components/MenuItemComponent.svelte diff --git a/crates/tauri-macros/src/lib.rs b/crates/tauri-macros/src/lib.rs index cf02da7f49c0..034f991447ab 100644 --- a/crates/tauri-macros/src/lib.rs +++ b/crates/tauri-macros/src/lib.rs @@ -131,27 +131,27 @@ pub fn default_runtime(attributes: TokenStream, input: TokenStream) -> TokenStre /// let kind = ItemKind::Check; /// let resources_table = app.resources_table(); /// match kind { -/// ItemKind::Submenu => { -/// let i = resources_table.get::>(rid)?; -/// i.set_text(text) -/// } -/// ItemKind::MenuItem => { -/// let i = resources_table.get::>(rid)?; -/// i.set_text(text) -/// } -/// ItemKind::Predefined => { -/// let i = resources_table.get::>(rid)?; -/// i.set_text(text) -/// } -/// ItemKind::Check => { -/// let i = resources_table.get::>(rid)?; -/// i.set_text(text) -/// } -/// ItemKind::Icon => { -/// let i = resources_table.get::>(rid)?; -/// i.set_text(text) -/// } -/// _ => unreachable!(), +/// ItemKind::Submenu => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// ItemKind::MenuItem => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// ItemKind::Predefined => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// ItemKind::Check => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// ItemKind::Icon => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// _ => unreachable!(), /// } /// ``` #[proc_macro] diff --git a/crates/tauri/src/menu/plugin.rs b/crates/tauri/src/menu/plugin.rs index 7f6741d00315..658b5672679c 100644 --- a/crates/tauri/src/menu/plugin.rs +++ b/crates/tauri/src/menu/plugin.rs @@ -359,7 +359,6 @@ struct NewOptions { #[command(root = "crate")] fn new( - app: Webview, webview: Webview, kind: ItemKind, options: Option, @@ -367,11 +366,11 @@ fn new( handler: Channel, ) -> crate::Result<(ResourceId, MenuId)> { let options = options.unwrap_or_default(); - let mut resources_table = app.resources_table(); + let mut resources_table = webview.resources_table(); let (rid, id) = match kind { ItemKind::Menu => { - let mut builder = MenuBuilder::new(&app); + let mut builder = MenuBuilder::new(&webview); if let Some(id) = options.id { builder = builder.id(id); } diff --git a/examples/api/package.json b/examples/api/package.json index c0f15c0f1bac..0ba26f005ea4 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -15,10 +15,12 @@ "devDependencies": { "@iconify-json/codicon": "^1.2.49", "@iconify-json/ph": "^1.2.2", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@unocss/extractor-svelte": "^66.6.6", - "svelte": "^5.55.7", - "unocss": "^66.6.6", - "vite": "^8.0.16" + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "@types/sortablejs": "^1.15.9", + "@unocss/extractor-svelte": "^66.7.5", + "sortablejs": "^1.15.7", + "svelte": "^5.56.4", + "unocss": "^66.7.5", + "vite": "^8.1.4" } } diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index b569f25cb550..876978fdaecd 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -67,6 +67,7 @@ pub fn run_app) + Send + 'static>( #[allow(unused_mut)] let mut window_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) + .disable_drag_drop_handler() .on_document_title_changed(|_window, title| { println!("document title changed: {title}"); }); diff --git a/examples/api/src/components/MenuBuilder.svelte b/examples/api/src/components/MenuBuilder.svelte index 006afecf5201..e0a614328e41 100644 --- a/examples/api/src/components/MenuBuilder.svelte +++ b/examples/api/src/components/MenuBuilder.svelte @@ -1,91 +1,293 @@ -
- + onMount(() => { + const sourceSortable1 = makeSourceSortable(sourceSortableEl1) + const sourceSortable2 = makeSourceSortable(sourceSortableEl2) + const targetSortable = new Sortable(targetSortableEl, { + dataIdAttr: 'data-id', + group: { + name: 'shared' + }, + async onAdd(event) { + const item = event.item + const kind = item.dataset.kind as MenuItemComponentKind + const text = item.dataset.text + const iconPath = item.dataset.iconPath + const checked = item.dataset.checked + + const newItem = { + id: currentId, + kind, + text, + iconPath, + checked: checked !== undefined ? checked === 'true' : checked + } as Item + currentId += 1 + + // HACK: We can't track the element created by Sortable, + // just make a new one and delete the one from Sortable + item.remove() + + if (event.newIndex !== undefined) { + items.splice(event.newIndex, 0, newItem) + } else { + items.push(newItem) + } + + newItem.menu = await create(newItem) -
- {#each items as item} -
-
-

{itemToString(item)}

+ await onItemAdded?.(newItem) + }, + onUpdate(event) { + const [item] = items.splice(event.oldIndex!, 1) + // Sortable's DOM manipulation doesn't work with Svelte, + // so we recreate all the nodes on order changes... + // This must be `toSpliced` to trigger the `#key` block re-render + items = items.toSpliced(event.newIndex!, 0, item!) + + onItemMoved?.(item, event.newIndex!) + }, + delayOnTouchOnly: true, + delay: 500, + animation: 200 + }) + return () => { + targetSortable.destroy() + sourceSortable1.destroy() + sourceSortable2.destroy() + } + }) + + onDestroy(() => { + for (const item of items) { + item.menu?.close() + } + items = [] + }) + + +
+
+
+

Custom

+
+ + +
- {/each} +
+
+

Predefined

+
+ {#each predefinedOptions as predefinedOption} + + {/each} +
+
+
+
+
+ {#key items} + {#each items as item, i (item.id)} + { + item.menu?.setText(item.text ?? '') + } + : undefined} + onIconPathChange={item.iconPath !== undefined + ? () => { + ;(item.menu as IconMenuItem | undefined)?.setIcon( + item.iconPath! + ) + } + : undefined} + onCheckedChange={item.checked !== undefined + ? () => { + ;(item.menu as CheckMenuItem | undefined)?.setChecked( + item.checked! + ) + } + : undefined} + onRemove={async () => { + items.splice(i, 1) + await onItemRemoved?.(item).finally(() => { + item.menu?.close() + }) + }} + /> + {:else} +
+ Drag and drop the menu items above to start building +
+ {/each} + {/key}
diff --git a/examples/api/src/components/MenuItemBuilder.svelte b/examples/api/src/components/MenuItemBuilder.svelte deleted file mode 100644 index 47b34b9733b7..000000000000 --- a/examples/api/src/components/MenuItemBuilder.svelte +++ /dev/null @@ -1,163 +0,0 @@ - - -
-
- {#each itemKinds as itemKind, i} -
- (kind = itemKind)} - type="radio" - name="kind" - value={itemKinds[i]} - /> - -
- {/each} -
- -
- -
- {#if kind == 'Normal' || kind == 'Icon' || kind == 'Check'} - - {/if} - {#if kind == 'Icon'} - - {:else if kind == 'Check'} -
- - -
- {:else if kind == 'Predefined'} -
- {#each predefinedOptions as predefinedOption, i} -
- (predefinedItem = predefinedOption)} - type="radio" - name="predefinedKind" - value={predefinedOptions[i]} - /> - -
- {/each} -
- {/if} -
- -
- -
- -
-
diff --git a/examples/api/src/components/MenuItemComponent.svelte b/examples/api/src/components/MenuItemComponent.svelte new file mode 100644 index 000000000000..5b828714a9fb --- /dev/null +++ b/examples/api/src/components/MenuItemComponent.svelte @@ -0,0 +1,79 @@ + + + + + diff --git a/examples/api/src/views/Menu.svelte b/examples/api/src/views/Menu.svelte index 6613d3ab3210..aa9cef7ed69d 100644 --- a/examples/api/src/views/Menu.svelte +++ b/examples/api/src/views/Menu.svelte @@ -1,49 +1,60 @@ -
- +
+ { + if (item.menu) { + await submenu?.append(item.menu) + } + }} + onItemRemoved={async (item) => { + if (item.menu) { + await submenu?.remove(item.menu) + } + }} + onItemMoved={(item, toIndex) => { + if (submenu) { + reorderMenuItems(submenu, item, toIndex) + } + }} + />
+
+ {#if tray} + + {:else} + + {/if} +
diff --git a/examples/api/src/views/Window.svelte b/examples/api/src/views/Window.svelte index 96f02642ac07..481c927969c2 100644 --- a/examples/api/src/views/Window.svelte +++ b/examples/api/src/views/Window.svelte @@ -400,7 +400,7 @@ }) -
+
@@ -513,7 +513,7 @@ Set focusable to {!focusable}
-
+
-
-
+
+
-
+
-
+
-
+