diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index 16206db25..3c30c24e1 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -30,6 +30,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 */ ## [Unreleased] +## v3.0.0-alpha.73 - 2026-02-27 + +## Fixed +- Fix frameless window cannot be minimized on darwin (#4294) + ## v3.0.0-alpha.72 - 2026-02-16 ## Fixed diff --git a/docs/src/content/docs/concepts/architecture.mdx b/docs/src/content/docs/concepts/architecture.mdx index d72affe3b..6b81a15a8 100644 --- a/docs/src/content/docs/concepts/architecture.mdx +++ b/docs/src/content/docs/concepts/architecture.mdx @@ -269,9 +269,9 @@ app.Event.Emit("user-logged-in", user) ```javascript // JavaScript: Listen for event -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -On('user-logged-in', (user) => { +Events.On('user-logged-in', (user) => { console.log('User logged in:', user) }) ``` diff --git a/docs/src/content/docs/concepts/bridge.mdx b/docs/src/content/docs/concepts/bridge.mdx index 7593d896a..13a8ac4e8 100644 --- a/docs/src/content/docs/concepts/bridge.mdx +++ b/docs/src/content/docs/concepts/bridge.mdx @@ -549,11 +549,11 @@ func ProcessLargeFile(path string) error { ``` ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' import { ProcessLargeFile } from './bindings/FileService' // Listen for progress -On('file-progress', (data) => { +Events.On('file-progress', (data) => { console.log(`Line ${data.line}: ${data.text}`) }) diff --git a/docs/src/content/docs/contributing/binding-system.mdx b/docs/src/content/docs/contributing/binding-system.mdx index 63f672155..d8609b748 100644 --- a/docs/src/content/docs/contributing/binding-system.mdx +++ b/docs/src/content/docs/contributing/binding-system.mdx @@ -139,9 +139,9 @@ export namespace backend { ### JavaScript Side ```ts -import { invoke } from "@wailsio/runtime"; +import { System } from "@wailsio/runtime"; -await invoke(0x7a1201d3 /* ChatService.Send */, ["Hello"]); +await System.invoke(0x7a1201d3 /* ChatService.Send */, ["Hello"]); ``` Implementation: `runtime/desktop/@wailsio/runtime/invoke.ts` diff --git a/docs/src/content/docs/features/dialogs/custom.mdx b/docs/src/content/docs/features/dialogs/custom.mdx index 591d68db2..242527465 100644 --- a/docs/src/content/docs/features/dialogs/custom.mdx +++ b/docs/src/content/docs/features/dialogs/custom.mdx @@ -199,14 +199,14 @@ func ShowConfirmdialog(message string) bool { ``` @@ -389,17 +389,17 @@ func (ld *Logindialog) Cancel() { ``` diff --git a/docs/src/content/docs/features/events/system.mdx b/docs/src/content/docs/features/events/system.mdx index 678abff2f..6a4c49725 100644 --- a/docs/src/content/docs/features/events/system.mdx +++ b/docs/src/content/docs/features/events/system.mdx @@ -129,13 +129,13 @@ window.EmitEvent("window-specific-event", data) ### From JavaScript ```javascript -import { Emit } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Emit to Go -Emit("button-clicked", { buttonId: "submit" }) +Events.Emit("button-clicked", { buttonId: "submit" }) // Emit to all windows -Emit("broadcast-message", "Hello everyone") +Events.Emit("broadcast-message", "Hello everyone") ``` ## Listening to Events @@ -506,7 +506,7 @@ func main() { **JavaScript:** ```javascript -import { Events, Emit } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Listen for notifications Events.On("notification", (event) => { diff --git a/docs/src/content/docs/features/windows/frameless.mdx b/docs/src/content/docs/features/windows/frameless.mdx index 361cced18..b8075b92a 100644 --- a/docs/src/content/docs/features/windows/frameless.mdx +++ b/docs/src/content/docs/features/windows/frameless.mdx @@ -166,11 +166,11 @@ Use the `--wails-draggable` CSS property: **JavaScript for buttons:** ```javascript -import { WindowMinimise, WindowMaximise, WindowClose } from '@wailsio/runtime' +import { Window } from '@wailsio/runtime' -document.querySelector('.minimize').addEventListener('click', WindowMinimise) -document.querySelector('.maximize').addEventListener('click', WindowMaximise) -document.querySelector('.close').addEventListener('click', WindowClose) +document.querySelector('.minimize').addEventListener('click', () => Window.Minimise()) +document.querySelector('.maximize').addEventListener('click', () => Window.Maximise()) +document.querySelector('.close').addEventListener('click', () => Window.Close()) ``` ## System Buttons @@ -214,15 +214,11 @@ document.querySelector('.close').addEventListener('click', Close) **Or use runtime methods:** ```javascript -import { - WindowMinimise, - WindowMaximise, - WindowClose -} from '@wailsio/runtime' +import { Window } from '@wailsio/runtime' -document.querySelector('.minimize').addEventListener('click', WindowMinimise) -document.querySelector('.maximize').addEventListener('click', WindowMaximise) -document.querySelector('.close').addEventListener('click', WindowClose) +document.querySelector('.minimize').addEventListener('click', () => Window.Minimise()) +document.querySelector('.maximize').addEventListener('click', () => Window.Maximise()) +document.querySelector('.close').addEventListener('click', () => Window.Close()) ``` ### Toggle Maximise State @@ -230,22 +226,22 @@ document.querySelector('.close').addEventListener('click', WindowClose) Track maximise state for button icon: ```javascript -import { WindowIsMaximised, WindowMaximise, WindowUnMaximise } from '@wailsio/runtime' +import { Window } from '@wailsio/runtime' async function toggleMaximise() { - const isMaximised = await WindowIsMaximised() - + const isMaximised = await Window.IsMaximised() + if (isMaximised) { - await WindowUnMaximise() + await Window.Restore() } else { - await WindowMaximise() + await Window.Maximise() } - + updateMaximiseButton() } async function updateMaximiseButton() { - const isMaximised = await WindowIsMaximised() + const isMaximised = await Window.IsMaximised() const button = document.querySelector('.maximize') button.textContent = isMaximised ? '❐' : '□' } @@ -737,41 +733,35 @@ body { **JavaScript:** ```javascript -import { - WindowMinimise, - WindowMaximise, - WindowUnMaximise, - WindowIsMaximised, - WindowClose -} from '@wailsio/runtime' +import { Window } from '@wailsio/runtime' // Minimise button document.querySelector('.minimize').addEventListener('click', () => { - WindowMinimise() + Window.Minimise() }) // Maximise/restore button const maximiseBtn = document.querySelector('.maximize') maximiseBtn.addEventListener('click', async () => { - const isMaximised = await WindowIsMaximised() - + const isMaximised = await Window.IsMaximised() + if (isMaximised) { - await WindowUnMaximise() + await Window.Restore() } else { - await WindowMaximise() + await Window.Maximise() } - + updateMaximiseButton() }) // Close button document.querySelector('.close').addEventListener('click', () => { - WindowClose() + Window.Close() }) // Update maximise button icon async function updateMaximiseButton() { - const isMaximised = await WindowIsMaximised() + const isMaximised = await Window.IsMaximised() maximiseBtn.textContent = isMaximised ? '❐' : '□' maximiseBtn.title = isMaximised ? 'Restore' : 'Maximise' } diff --git a/docs/src/content/docs/features/windows/multiple.mdx b/docs/src/content/docs/features/windows/multiple.mdx index 41d5a18fe..60694abc9 100644 --- a/docs/src/content/docs/features/windows/multiple.mdx +++ b/docs/src/content/docs/features/windows/multiple.mdx @@ -288,7 +288,7 @@ func CreateToolPalette(app *application.Application) *application.WebviewWindow } ``` -### Pattern 4: Modal dialogs +### Pattern 4: Modal dialogs (macOS only) Child windows that block parent: @@ -303,14 +303,7 @@ func ShowModaldialog(parent *application.WebviewWindow, title string) { Resizable: false, }) - // Disable parent (platform-specific) - parent.SetEnabled(false) - - // Re-enable parent on close - dialog.OnDestroy(func() { - parent.SetEnabled(true) - parent.SetFocus() - }) + parent.AttachModal(dialog) } ``` @@ -359,51 +352,38 @@ func (e *EditorApp) TogglePreview() { ```go childWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Child Window", - Parent: parentWindow, }) + +parentWindow.AttachModal(childWindow) ``` **Behaviour:** -- Child closes when parent closes -- Child stays above parent (on some platforms) -- Child minimises with parent (on some platforms) +- Child stays above parent +- Child moves with parent +- Child blocks interaction to parent **Platform support:** -| Feature | macOS | Windows | Linux | -|---------|-------|---------|-------| -| Auto-close | ✅ | ✅ | ⚠️ Varies | -| Stay above | ✅ | ⚠️ Partial | ⚠️ Varies | -| Minimise together | ✅ | ❌ | ⚠️ Varies | +| macOS | Windows | Linux | +|-------|---------|-------| +| ✅ | ❌ | ❌ | ### Modal Behaviour Create modal-like behaviour: ```go -func ShowModal(parent *application.WebviewWindow) { - modal := app.Window.NewWithOptions(application.WebviewWindowOptions{ - Title: "Modal dialog", +func ShowModaldialog(parent *application.WebviewWindow, title string) { + dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: title, Width: 400, Height: 200, - Parent: parent, - AlwaysOnTop: true, }) - // Disable parent interaction - parent.SetEnabled(false) - - // Re-enable on close - modal.OnClose(func() bool { - parent.SetEnabled(true) - parent.SetFocus() - return true - }) + parent.AttachModal(dialog) } ``` -**Note:** True modal behaviour (blocking) varies by platform. - ## Window Lifecycle Management ### Creation Callbacks diff --git a/docs/src/content/docs/guides/events-reference.mdx b/docs/src/content/docs/guides/events-reference.mdx index afa61fc3e..27308a17e 100644 --- a/docs/src/content/docs/guides/events-reference.mdx +++ b/docs/src/content/docs/guides/events-reference.mdx @@ -518,13 +518,12 @@ useEffect(() => { Check platform availability when using platform-specific events: ```javascript -import { Platform, Events } from '@wailsio/runtime'; +import { Events } from '@wailsio/runtime'; -if (Platform.isWindows) { - Events.On('windows:APMSuspend', handleSuspend); -} else if (Platform.isMac) { - Events.On('mac:ApplicationWillTerminate', handleTerminate); -} +// Platform-specific events can be registered unconditionally; +// they will simply never fire on unsupported platforms. +Events.On('windows:APMSuspend', handleSuspend); +Events.On('mac:ApplicationWillTerminate', handleTerminate); ``` ### 4. Don't Overuse Events diff --git a/docs/src/content/docs/migration/v2-to-v3.mdx b/docs/src/content/docs/migration/v2-to-v3.mdx index 5eb94ca67..002b98663 100644 --- a/docs/src/content/docs/migration/v2-to-v3.mdx +++ b/docs/src/content/docs/migration/v2-to-v3.mdx @@ -456,13 +456,13 @@ EventsOn("update", (data) => { EventsEmit("action", data) // v3 -import { OnEvent, Emit } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -OnEvent("update", (data) => { +Events.On("update", (data) => { console.log(data) }) -Emit("action", data) +Events.Emit("action", data) ``` ### Step 6: Update Configuration diff --git a/docs/src/content/docs/reference/events.mdx b/docs/src/content/docs/reference/events.mdx index d6286018e..15e9ae0a4 100644 --- a/docs/src/content/docs/reference/events.mdx +++ b/docs/src/content/docs/reference/events.mdx @@ -97,9 +97,9 @@ app.Event.Emit("global-update", data) Listens for events from Go. ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -On(eventName, callback) +Events.On(eventName, callback) ``` **Parameters:** @@ -110,10 +110,10 @@ On(eventName, callback) **Example:** ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Listen for events -const cleanup = On('data-updated', (data) => { +const cleanup = Events.On('data-updated', (data) => { console.log('Count:', data.count) console.log('Status:', data.status) updateUI(data) @@ -128,17 +128,17 @@ cleanup() Listens for a single event occurrence. ```javascript -import { Once } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -Once(eventName, callback) +Events.Once(eventName, callback) ``` **Example:** ```javascript -import { Once } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Listen for first occurrence only -Once('initialization-complete', (data) => { +Events.Once('initialization-complete', (data) => { console.log('App initialized!', data) // This will only fire once }) @@ -149,24 +149,24 @@ Once('initialization-complete', (data) => { Removes an event listener. ```javascript -import { Off } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -Off(eventName, callback) +Events.Off(eventName, callback) ``` **Example:** ```javascript -import { On, Off } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' const handler = (data) => { console.log('Event received:', data) } // Start listening -On('my-event', handler) +Events.On('my-event', handler) // Stop listening -Off('my-event', handler) +Events.Off('my-event', handler) ``` ### OffAll() @@ -174,9 +174,9 @@ Off('my-event', handler) Removes all listeners for an event. ```javascript -import { OffAll } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -OffAll(eventName) +Events.OffAll(eventName) ``` **Example:** @@ -281,10 +281,10 @@ func (s *DataService) FetchData(query string) ([]Item, error) { ```javascript import { FetchData } from './bindings/DataService' -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Listen for completion event -On('data-fetched', (data) => { +Events.On('data-fetched', (data) => { console.log(`Fetched ${data.count} items for query: ${data.query}`) showNotification(`Found ${data.count} results`) }) @@ -325,16 +325,16 @@ func (s *Service) ProcessFiles(files []string) error { **JavaScript:** ```javascript -import { On, Once } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Update progress bar -On('progress', (data) => { +Events.On('progress', (data) => { progressBar.style.width = `${data.percent}%` statusText.textContent = `Processing ${data.file}... (${data.current}/${data.total})` }) // Handle completion -Once('processing-complete', () => { +Events.Once('processing-complete', () => { progressBar.style.width = '100%' statusText.textContent = 'Complete!' setTimeout(() => hideProgressBar(), 2000) @@ -364,10 +364,10 @@ window1.OnEvent("request-data", func(e *application.CustomEvent) { **JavaScript:** ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Listen in any window -On('theme-changed', (theme) => { +Events.On('theme-changed', (theme) => { document.body.className = theme }) ``` @@ -407,13 +407,13 @@ func (s *StateService) GetState(key string) interface{} { **JavaScript:** ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' import { GetState } from './bindings/StateService' // Keep local state in sync let localState = {} -On('state-updated', async (data) => { +Events.On('state-updated', async (data) => { localState[data.key] = data.value updateUI(data.key, data.value) }) @@ -459,10 +459,10 @@ func (s *NotificationService) Info(message string) { **JavaScript:** ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Unified notification handler -On('notification', (data) => { +Events.On('notification', (data) => { const toast = document.createElement('div') toast.className = `toast toast-${data.type}` toast.textContent = data.message @@ -568,34 +568,34 @@ func main() { **JavaScript:** ```javascript -import { On, Once } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' import { StartLongTask, BroadcastMessage } from './bindings/EventDemoService' // Task events -On('task-started', () => { +Events.On('task-started', () => { console.log('Task started...') document.getElementById('status').textContent = 'Running...' }) -On('task-progress', (data) => { +Events.On('task-progress', (data) => { const progressBar = document.getElementById('progress') progressBar.style.width = `${data.percent}%` console.log(`Step ${data.step} of ${data.total}`) }) -Once('task-completed', (data) => { +Events.Once('task-completed', (data) => { console.log('Task completed!', data.message) document.getElementById('status').textContent = data.message }) // Broadcast events -On('broadcast', (message) => { +Events.On('broadcast', (message) => { console.log('Broadcast:', message) alert(message) }) // Window state events -On('window-state', (state) => { +Events.On('window-state', (state) => { console.log('Window is now:', state) document.body.dataset.windowState = state }) @@ -784,12 +784,12 @@ func (s *Service) EmitWithDebounce(event string, data interface{}) { ### Throttling Events ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' let lastUpdate = 0 const throttleMs = 100 -On('high-frequency-event', (data) => { +Events.On('high-frequency-event', (data) => { const now = Date.now() if (now - lastUpdate < throttleMs) { return // Skip this update diff --git a/docs/src/content/docs/reference/frontend-runtime.mdx b/docs/src/content/docs/reference/frontend-runtime.mdx index 4d56667fc..1f02ececa 100644 --- a/docs/src/content/docs/reference/frontend-runtime.mdx +++ b/docs/src/content/docs/reference/frontend-runtime.mdx @@ -159,9 +159,7 @@ Events.Emit(UserUpdated({ The runtime is organized into modules, each providing specific functionality. Import only what you need: ```javascript -import { On, Emit } from '@wailsio/runtime/events' -import Window from '@wailsio/runtime/window' -import { SetText, Text } from '@wailsio/runtime/clipboard' +import { Events, Window, Clipboard } from '@wailsio/runtime' ``` ### Events @@ -182,17 +180,17 @@ function On(eventType: EventType, callback: (event: WailsEvent) => void **Example:** ```javascript -import { On } from '@wailsio/runtime/events' +import { Events } from '@wailsio/runtime' // Basic event listening -const unsubscribe = On('user-logged-in', (event) => { +const unsubscribe = Events.On('user-logged-in', (event) => { console.log('User:', event.data.username) }) // With typed events (TypeScript) import { UserLogin } from './bindings/events' -On(UserLogin, (event) => { +Events.On(UserLogin, (event) => { // event.data is typed as UserLoginData console.log('User:', event.data.username) }) @@ -212,9 +210,9 @@ function Once(eventType: EventType, callback: (event: WailsEvent) => vo **Example:** ```javascript -import { Once } from '@wailsio/runtime/events' +import { Events } from '@wailsio/runtime' -Once('app-ready', () => { +Events.Once('app-ready', () => { console.log('App initialized') }) ``` @@ -233,15 +231,15 @@ function Emit(event: Event): Promise **Example:** ```javascript -import { Emit } from '@wailsio/runtime/events' +import { Events } from '@wailsio/runtime' // Basic event emission -const wasCancelled = await Emit('button-clicked', { buttonId: 'submit' }) +const wasCancelled = await Events.Emit('button-clicked', { buttonId: 'submit' }) // With typed events (TypeScript) import { UserLogin } from './bindings/events' -const cancelled = await Emit(UserLogin({ +const cancelled = await Events.Emit(UserLogin({ UserID: "123", Username: "john_doe", LoginTime: new Date().toISOString() @@ -267,9 +265,9 @@ function Off(...eventNames: string[]): void **Example:** ```javascript -import { Off } from '@wailsio/runtime/events' +import { Events } from '@wailsio/runtime' -Off('user-logged-in', 'user-logged-out') +Events.Off('user-logged-in', 'user-logged-out') ``` #### OffAll() @@ -285,7 +283,7 @@ function OffAll(): void Window management methods. The default export is the current window. ```javascript -import Window from '@wailsio/runtime/window' +import { Window } from '@wailsio/runtime' // Current window await Window.SetTitle('New Title') @@ -351,7 +349,7 @@ function Center(): Promise **Example:** ```javascript -import Window from '@wailsio/runtime/window' +import { Window } from '@wailsio/runtime' // Resize and center await Window.SetSize(800, 600) @@ -514,7 +512,7 @@ function Print(): Promise **Example:** ```javascript -import Window from '@wailsio/runtime/window' +import { Window } from '@wailsio/runtime' // Open print dialog for current window await Window.Print() @@ -537,9 +535,9 @@ function SetText(text: string): Promise **Example:** ```javascript -import { SetText } from '@wailsio/runtime/clipboard' +import { Clipboard } from '@wailsio/runtime' -await SetText('Hello from Wails!') +await Clipboard.SetText('Hello from Wails!') ``` #### Text() @@ -553,9 +551,9 @@ function Text(): Promise **Example:** ```javascript -import { Text } from '@wailsio/runtime/clipboard' +import { Clipboard } from '@wailsio/runtime' -const clipboardText = await Text() +const clipboardText = await Clipboard.Text() console.log('Clipboard:', clipboardText) ``` @@ -620,11 +618,11 @@ function Quit(): Promise **Example:** ```javascript -import { Quit } from '@wailsio/runtime/application' +import { Application } from '@wailsio/runtime' // Add quit button document.getElementById('quit-btn').addEventListener('click', async () => { - await Quit() + await Application.Quit() }) ``` @@ -643,9 +641,9 @@ function OpenURL(url: string | URL): Promise **Example:** ```javascript -import { OpenURL } from '@wailsio/runtime/browser' +import { Browser } from '@wailsio/runtime' -await OpenURL('https://wails.io') +await Browser.OpenURL('https://wails.io') ``` ### Screens @@ -696,16 +694,16 @@ interface Screen { **Example:** ```javascript -import { GetAll, GetPrimary } from '@wailsio/runtime/screens' +import { Screens } from '@wailsio/runtime' // List all screens -const screens = await GetAll() +const screens = await Screens.GetAll() screens.forEach(screen => { console.log(`${screen.Name}: ${screen.Size.Width}x${screen.Size.Height}`) }) // Get primary screen -const primary = await GetPrimary() +const primary = await Screens.GetPrimary() console.log('Primary screen:', primary.Name) ``` @@ -724,9 +722,9 @@ function Info(options: MessageDialogOptions): Promise **Example:** ```javascript -import { Info } from '@wailsio/runtime/dialogs' +import { Dialogs } from '@wailsio/runtime' -await Info({ +await Dialogs.Info({ Title: 'Success', Message: 'Operation completed successfully!' }) @@ -759,9 +757,9 @@ function Question(options: QuestionDialogOptions): Promise **Example:** ```javascript -import { Question } from '@wailsio/runtime/dialogs' +import { Dialogs } from '@wailsio/runtime' -const result = await Question({ +const result = await Dialogs.Question({ Title: 'Confirm Delete', Message: 'Are you sure you want to delete this file?', Buttons: [ @@ -786,9 +784,9 @@ function OpenFile(options: OpenFileDialogOptions): Promise **Example:** ```javascript -import { OpenFile } from '@wailsio/runtime/dialogs' +import { Dialogs } from '@wailsio/runtime' -const file = await OpenFile({ +const file = await Dialogs.OpenFile({ Title: 'Select Image', Filters: [ { DisplayName: 'Images', Pattern: '*.png;*.jpg;*.jpeg' }, @@ -864,14 +862,10 @@ WML provides declarative attributes for common actions. Add attributes to HTML e ## Complete Example ```javascript -import { On, Emit } from '@wailsio/runtime/events' -import Window from '@wailsio/runtime/window' -import { SetText, Text } from '@wailsio/runtime/clipboard' -import { Question } from '@wailsio/runtime/dialogs' -import { GetAll } from '@wailsio/runtime/screens' +import { Events, Window, Clipboard, Dialogs, Screens } from '@wailsio/runtime' // Listen for events from Go -On('data-updated', (event) => { +Events.On('data-updated', (event) => { console.log('Data:', event.data) updateUI(event.data) }) @@ -892,12 +886,12 @@ document.getElementById('fullscreen-btn').addEventListener('click', async () => // Clipboard operations document.getElementById('copy-btn').addEventListener('click', async () => { - await SetText('Copied from Wails!') + await Clipboard.SetText('Copied from Wails!') }) // Dialog with confirmation document.getElementById('delete-btn').addEventListener('click', async () => { - const result = await Question({ + const result = await Dialogs.Question({ Title: 'Confirm', Message: 'Delete this item?', Buttons: [ @@ -907,12 +901,12 @@ document.getElementById('delete-btn').addEventListener('click', async () => { }) if (result === 'Delete') { - await Emit('delete-item', { id: currentItemId }) + await Events.Emit('delete-item', { id: currentItemId }) } }) // Screen information -const screens = await GetAll() +const screens = await Screens.GetAll() console.log(`Detected ${screens.length} screen(s)`) screens.forEach(screen => { console.log(`- ${screen.Name}: ${screen.Size.Width}x${screen.Size.Height}`) @@ -940,10 +934,9 @@ screens.forEach(screen => { The runtime includes full TypeScript definitions: ```typescript -import { On, WailsEvent } from '@wailsio/runtime/events' -import Window from '@wailsio/runtime/window' +import { Events, Window } from '@wailsio/runtime' -On('custom-event', (event: WailsEvent) => { +Events.On('custom-event', (event) => { // TypeScript knows event.data, event.name, event.sender console.log(event.data) }) diff --git a/docs/src/content/docs/reference/overview.mdx b/docs/src/content/docs/reference/overview.mdx index 8a3b77bc3..064edbf4a 100644 --- a/docs/src/content/docs/reference/overview.mdx +++ b/docs/src/content/docs/reference/overview.mdx @@ -146,8 +146,7 @@ import ( import { MyMethod } from './bindings/MyService' // Runtime APIs -import { On, Emit } from '@wailsio/runtime' -import { WindowSetTitle } from '@wailsio/runtime/window' +import { Events, Window } from '@wailsio/runtime' ``` ## Type Reference diff --git a/docs/src/content/docs/reference/window.mdx b/docs/src/content/docs/reference/window.mdx index 346a326e6..9d6dd5102 100644 --- a/docs/src/content/docs/reference/window.mdx +++ b/docs/src/content/docs/reference/window.mdx @@ -498,9 +498,9 @@ window.EmitEvent("data-updated", map[string]interface{}{ **Frontend (JavaScript):** ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -On('data-updated', (data) => { +Events.On('data-updated', (data) => { console.log('Count:', data.count) console.log('Status:', data.status) }) @@ -597,12 +597,27 @@ if err != nil { } ``` -**Platform support:** -- **macOS**: Full support -- **Windows**: Full support -- **Linux**: Full support +### AttachModal() -**Note:** This triggers the native OS print dialog, allowing the user to select printer settings and print the current window content. +Attaches a second Window as a sheet modal. + +```go +func (w *Window) AttachModal(modalWindow Window) +``` + +**Parameters:** +- `modalWindow` - The window to attach as a modal + +**Platform support:** +- **macOS**: Full support (presents as a sheet) +- **Windows**: No support +- **Linux**: No support + +**Example:** +```go +modalWindow := app.Window.New() +window.AttachModal(modalWindow) +``` ## Platform-Specific Options diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index e9af45432..35c356b07 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file ## Added +- Add support for modal sheets (macOS) ## Changed @@ -25,6 +26,7 @@ After processing, the content will be moved to the main changelog and this file - Fix system tray menu highlight state on macOS - icon now shows selected state when menu is open (#4910) - Fix system tray attached window appearing behind other windows on macOS - now uses proper popup window level (#4910) +- Fix incorrect `@wailsio/runtime` import examples across documentation (#4989) ## Deprecated diff --git a/v3/internal/version/version.txt b/v3/internal/version/version.txt index 080cb6498..c4b06870c 100644 --- a/v3/internal/version/version.txt +++ b/v3/internal/version/version.txt @@ -1 +1 @@ -v3.0.0-alpha.72 \ No newline at end of file +v3.0.0-alpha.73 \ No newline at end of file diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 6cfa8b821..91909e83e 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -113,6 +113,7 @@ type ( setMenu(menu *Menu) snapAssist() setContentProtection(enabled bool) + attachModal(modalWindow *WebviewWindow) } ) @@ -1284,6 +1285,22 @@ func (w *WebviewWindow) NativeWindow() unsafe.Pointer { return w.impl.nativeWindow() } +// AttachModal attaches a modal window to this window, presenting it as a sheet on macOS. +func (w *WebviewWindow) AttachModal(modalWindow Window) { + if w.impl == nil || w.isDestroyed() { + return + } + + modalWebviewWindow, ok := modalWindow.(*WebviewWindow) + if !ok || modalWebviewWindow == nil { + return + } + + InvokeSync(func() { + w.impl.attachModal(modalWebviewWindow) + }) +} + // shouldUnconditionallyClose returns whether the window should close unconditionally func (w *WebviewWindow) shouldUnconditionallyClose() bool { return atomic.LoadUint32(&w.unconditionallyClose) != 0 diff --git a/v3/pkg/application/webview_window_android.go b/v3/pkg/application/webview_window_android.go index ed41a5add..6e139f52c 100644 --- a/v3/pkg/application/webview_window_android.go +++ b/v3/pkg/application/webview_window_android.go @@ -296,6 +296,10 @@ func (w *androidWebviewWindow) nativeWindow() unsafe.Pointer { return nil } +func (w *androidWebviewWindow) attachModal(modalWindow *WebviewWindow) { + // Modal windows are not supported on Android +} + func (w *androidWebviewWindow) on(eventID uint) { // Android event handling } diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index ff49ad49b..14fef21da 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -27,7 +27,7 @@ extern void registerListener(unsigned int event); void* windowNew(unsigned int id, int width, int height, bool fraudulentWebsiteWarningEnabled, bool frameless, bool enableDragAndDrop, struct WebviewPreferences preferences) { NSWindowStyleMask styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; if (frameless) { - styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable; + styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable; } WebviewWindow* window = [[WebviewWindow alloc] initWithContentRect:NSMakeRect(0, 0, width-1, height-1) styleMask:styleMask @@ -331,6 +331,24 @@ void windowZoomOut(void* nsWindow) { } } +// createModalWindow presents a modal window as a sheet attached to the parent window +void createModalWindow(void* parentWindowPtr, void* modalWindowPtr) { + if (parentWindowPtr == NULL || modalWindowPtr == NULL) { + return; + } + + NSWindow* parentWindow = (NSWindow*)parentWindowPtr; + NSWindow* modalWindow = (NSWindow*)modalWindowPtr; + + // Present the modal window as a sheet attached to the parent window + // Must be dispatched to the main thread for UI thread safety + dispatch_async(dispatch_get_main_queue(), ^{ + [parentWindow beginSheet:modalWindow completionHandler:^(NSModalResponse returnCode) { + // Sheet was dismissed - window will be released automatically + }]; + }); +} + // set the window position relative to the screen void windowSetRelativePosition(void* nsWindow, int x, int y) { WebviewWindow* window = (WebviewWindow*)nsWindow; @@ -1551,6 +1569,17 @@ func (w *macosWebviewWindow) setContentProtection(enabled bool) { C.setContentProtection(w.nsWindow, C.bool(enabled)) } +func (w *macosWebviewWindow) attachModal(modalWindow *WebviewWindow) { + if modalWindow == nil || modalWindow.impl == nil || modalWindow.isDestroyed() { + return + } + modalNativeWindow := modalWindow.impl.nativeWindow() + if modalNativeWindow == nil { + return + } + C.createModalWindow(w.nsWindow, modalNativeWindow) +} + func (w *macosWebviewWindow) cut() { } diff --git a/v3/pkg/application/webview_window_ios.go b/v3/pkg/application/webview_window_ios.go index bc3087f2c..481788c87 100644 --- a/v3/pkg/application/webview_window_ios.go +++ b/v3/pkg/application/webview_window_ios.go @@ -330,6 +330,10 @@ func (w *iosWebviewWindow) nativeWindow() unsafe.Pointer { return w.nativeHandle } +func (w *iosWebviewWindow) attachModal(modalWindow *WebviewWindow) { + // Modal windows are not supported on iOS +} + func (w *iosWebviewWindow) on(eventID uint) { // iOS event handling } diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 9a5eb4131..4eddadbed 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -410,6 +410,10 @@ func (w *linuxWebviewWindow) nativeWindow() unsafe.Pointer { return unsafe.Pointer(w.window) } +func (w *linuxWebviewWindow) attachModal(modalWindow *WebviewWindow) { + // Modal windows are not supported on Linux +} + func (w *linuxWebviewWindow) print() error { w.execJS("window.print();") return nil diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 16ba08f0b..434723fb4 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -78,6 +78,9 @@ type windowsWebviewWindow struct { // menubarTheme is the theme for the menubar menubarTheme *w32.MenuBarTheme + + // Modal window tracking + parentHWND w32.HWND // Parent window HWND when this window is a modal } func (w *windowsWebviewWindow) setMenu(menu *Menu) { @@ -233,6 +236,37 @@ func (w *windowsWebviewWindow) startDrag() error { return nil } +func (w *windowsWebviewWindow) attachModal(modalWindow *WebviewWindow) { + if modalWindow == nil || modalWindow.impl == nil || modalWindow.isDestroyed() { + return + } + + // Get the modal window's Windows implementation + modalWindowsImpl, ok := modalWindow.impl.(*windowsWebviewWindow) + if !ok { + return + } + + parentHWND := w.hwnd + modalHWND := modalWindowsImpl.hwnd + + // Set parent-child relationship using GWLP_HWNDPARENT + // This ensures the modal stays above parent and moves with it + w32.SetWindowLongPtr(modalHWND, w32.GWLP_HWNDPARENT, uintptr(parentHWND)) + + // Track the parent HWND in the modal window for cleanup + modalWindowsImpl.parentHWND = parentHWND + + // Disable the parent window to block interaction (Microsoft's recommended approach) + // This follows Windows modal dialog best practices + w32.EnableWindow(parentHWND, false) + + // Ensure modal window is shown and brought to front + w32.ShowWindow(modalHWND, w32.SW_SHOW) + w32.SetForegroundWindow(modalHWND) + w32.BringWindowToTop(modalHWND) +} + func (w *windowsWebviewWindow) nativeWindow() unsafe.Pointer { return unsafe.Pointer(w.hwnd) } @@ -725,6 +759,12 @@ func (w *windowsWebviewWindow) setRelativePosition(x int, y int) { } func (w *windowsWebviewWindow) destroy() { + // Re-enable parent window if this was a modal window + if w.parentHWND != 0 { + w32.EnableWindow(w.parentHWND, true) + w.parentHWND = 0 + } + w.parent.markAsDestroyed() // destroy the window w32.DestroyWindow(w.hwnd) @@ -1420,6 +1460,12 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp } defer func() { + // Re-enable parent window if this was a modal window + if w.parentHWND != 0 { + w32.EnableWindow(w.parentHWND, true) + w.parentHWND = 0 + } + windowsApp := globalApplication.impl.(*windowsApp) windowsApp.unregisterWindow(w) diff --git a/v3/pkg/application/window.go b/v3/pkg/application/window.go index 3f4949b16..ec810b115 100644 --- a/v3/pkg/application/window.go +++ b/v3/pkg/application/window.go @@ -91,6 +91,7 @@ type Window interface { Flash(enabled bool) Print() error RegisterHook(eventType events.WindowEventType, callback func(event *WindowEvent)) func() + AttachModal(modalWindow Window) shouldUnconditionallyClose() bool // Editing methods