From fe9842c999ced38a4a6a0bd2c3c08fc3353b8c58 Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 08:01:33 -0600 Subject: [PATCH 01/10] add process and cache screen step macOS --- v3/pkg/application/screen_darwin.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/v3/pkg/application/screen_darwin.go b/v3/pkg/application/screen_darwin.go index ba9859ff0ec..58f8e8e7727 100644 --- a/v3/pkg/application/screen_darwin.go +++ b/v3/pkg/application/screen_darwin.go @@ -163,21 +163,24 @@ func cScreenToScreen(screen C.Screen) *Screen { } } -func (m *macosApp) getPrimaryScreen() (*Screen, error) { - cScreen := C.GetPrimaryScreen() - return cScreenToScreen(cScreen), nil -} - -func (m *macosApp) getScreens() ([]*Screen, error) { +func (m *macosApp) processAndCacheScreens() error { cScreens := C.getAllScreens() defer C.free(unsafe.Pointer(cScreens)) numScreens := int(C.GetNumScreens()) - displays := make([]*Screen, numScreens) + screens := make([]*Screen, numScreens) cScreenHeaders := (*[1 << 30]C.Screen)(unsafe.Pointer(cScreens))[:numScreens:numScreens] for i := 0; i < numScreens; i++ { - displays[i] = cScreenToScreen(cScreenHeaders[i]) + screens[i] = cScreenToScreen(cScreenHeaders[i]) } - return displays, nil + return m.parent.Screen.LayoutScreens(screens) +} + +func (m *macosApp) getPrimaryScreen() (*Screen, error) { + return m.parent.Screen.primaryScreen, nil +} + +func (m *macosApp) getScreens() ([]*Screen, error) { + return m.parent.Screen.screens, nil } func getScreenForWindow(window *macosWebviewWindow) (*Screen, error) { From 5c7718cbbd14348ec9e744bd0fb9cd019e2108de Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 08:01:44 -0600 Subject: [PATCH 02/10] screen 0 == primary on macos --- v3/pkg/application/screen_darwin.go | 1 + 1 file changed, 1 insertion(+) diff --git a/v3/pkg/application/screen_darwin.go b/v3/pkg/application/screen_darwin.go index 58f8e8e7727..4948dbd1185 100644 --- a/v3/pkg/application/screen_darwin.go +++ b/v3/pkg/application/screen_darwin.go @@ -89,6 +89,7 @@ Screen* getAllScreens() { for (int i = 0; i < screens.count; i++) { NSScreen* screen = [screens objectAtIndex:i]; returnScreens[i] = processScreen(screen); + returnScreens[i].isPrimary = (i == 0); } return returnScreens; } From 81c79a7655d62589e79aa30872b1b9bff9bd85a2 Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 08:01:58 -0600 Subject: [PATCH 03/10] process/cache screens on screen change event --- v3/pkg/application/application_darwin.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go index 287b66e84bd..6352be7c7e2 100644 --- a/v3/pkg/application/application_darwin.go +++ b/v3/pkg/application/application_darwin.go @@ -300,6 +300,18 @@ func (m *macosApp) run() error { ) C.setActivationPolicy(C.int(m.parent.options.Mac.ActivationPolicy)) C.activateIgnoringOtherApps() + if err := m.processAndCacheScreens(); err != nil { + m.parent.handleError(err) + } + }, + ) + // Refresh screen cache when display configuration changes + m.parent.Event.OnApplicationEvent( + events.Mac.ApplicationDidChangeScreenParameters, + func(*ApplicationEvent) { + if err := m.processAndCacheScreens(); err != nil { + m.parent.handleError(err) + } }, ) m.setupCommonEvents() From 76123eac5836fb507db500d4fd0c078a1b121d63 Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 08:22:31 -0600 Subject: [PATCH 04/10] SetScreen/set on startup per OS impl --- v3/pkg/application/webview_window.go | 20 ++++++++++++++++++++ v3/pkg/application/webview_window_darwin.go | 10 +++++++++- v3/pkg/application/webview_window_linux.go | 20 ++++++++++++++++++-- v3/pkg/application/webview_window_options.go | 6 ++++++ v3/pkg/application/webview_window_windows.go | 10 +++++++++- v3/pkg/application/window.go | 1 + 6 files changed, 63 insertions(+), 4 deletions(-) diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 91909e83edd..2b6a05a0f4c 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -950,6 +950,26 @@ func (w *WebviewWindow) SetPosition(x int, y int) { }) } +// SetScreen moves the window to the center of the given screen's WorkArea. +// If called before Run() (impl is nil), the screen is stored for deferred application. +func (w *WebviewWindow) SetScreen(screen *Screen) Window { + if screen == nil { + return w + } + w.options.Screen = screen + if w.impl == nil || w.isDestroyed() { + return w + } + InvokeSync(func() { + width, height := w.impl.size() + workArea := screen.WorkArea + x := workArea.X + (workArea.Width-width)/2 + y := workArea.Y + (workArea.Height-height)/2 + w.impl.setPosition(x, y) + }) + return w +} + // RelativePosition returns the position of the window relative to the screen WorkArea on which it is func (w *WebviewWindow) RelativePosition() (int, int) { if w.impl == nil || w.isDestroyed() { diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index 14fef21dafd..a3f9691a391 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -1368,7 +1368,15 @@ func (w *macosWebviewWindow) run() { w.fullscreen() case WindowStateNormal: } - if w.parent.options.InitialPosition == WindowCentered { + if options.Screen != nil { + workArea := options.Screen.WorkArea + if w.parent.options.InitialPosition == WindowCentered { + width, height := w.size() + w.setPosition(workArea.X+(workArea.Width-width)/2, workArea.Y+(workArea.Height-height)/2) + } else { + w.setPosition(workArea.X+options.X, workArea.Y+options.Y) + } + } else if w.parent.options.InitialPosition == WindowCentered { C.windowCenter(w.nsWindow) } else { w.setPosition(options.X, options.Y) diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 4eddadbeddb..5b61f86e0eb 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -339,7 +339,15 @@ func (w *linuxWebviewWindow) run() { w.setFrameless(w.parent.options.Frameless) - if w.parent.options.InitialPosition == WindowCentered { + if w.parent.options.Screen != nil { + workArea := w.parent.options.Screen.WorkArea + if w.parent.options.InitialPosition == WindowCentered { + width, height := w.size() + w.setPosition(workArea.X+(workArea.Width-width)/2, workArea.Y+(workArea.Height-height)/2) + } else { + w.setPosition(workArea.X+w.parent.options.X, workArea.Y+w.parent.options.Y) + } + } else if w.parent.options.InitialPosition == WindowCentered { w.center() } else { w.setPosition(w.parent.options.X, w.parent.options.Y) @@ -387,7 +395,15 @@ func (w *linuxWebviewWindow) run() { } if !w.parent.options.Hidden { w.show() - if w.parent.options.InitialPosition == WindowCentered { + if w.parent.options.Screen != nil { + workArea := w.parent.options.Screen.WorkArea + if w.parent.options.InitialPosition == WindowCentered { + width, height := w.size() + w.setPosition(workArea.X+(workArea.Width-width)/2, workArea.Y+(workArea.Height-height)/2) + } else { + w.setPosition(workArea.X+w.parent.options.X, workArea.Y+w.parent.options.Y) + } + } else if w.parent.options.InitialPosition == WindowCentered { w.center() } else { w.setRelativePosition(w.parent.options.X, w.parent.options.Y) diff --git a/v3/pkg/application/webview_window_options.go b/v3/pkg/application/webview_window_options.go index c1b1bf164c0..e6f6dae5d25 100644 --- a/v3/pkg/application/webview_window_options.go +++ b/v3/pkg/application/webview_window_options.go @@ -95,6 +95,12 @@ type WebviewWindowOptions struct { // Y is the starting Y position of the window. Y int + // Screen specifies the target screen for initial window placement. + // When set with WindowCentered, the window is centered on that screen's WorkArea. + // When set with WindowXY, X/Y are treated as relative to that screen's WorkArea origin. + // When nil, OS default behavior is used. + Screen *Screen + // Hidden will hide the window when it is first created. Hidden bool diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 434723fb4d2..0c8fc1851cb 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -551,7 +551,15 @@ func (w *windowsWebviewWindow) run() { w.setWindowMask(options.Windows.WindowMask) } - if options.InitialPosition == WindowCentered { + if options.Screen != nil { + workArea := options.Screen.WorkArea + if options.InitialPosition == WindowCentered { + width, height := w.size() + w.setPosition(workArea.X+(workArea.Width-width)/2, workArea.Y+(workArea.Height-height)/2) + } else { + w.setPosition(workArea.X+options.X, workArea.Y+options.Y) + } + } else if options.InitialPosition == WindowCentered { w.center() } else { w.setPosition(options.X, options.Y) diff --git a/v3/pkg/application/window.go b/v3/pkg/application/window.go index ec810b115bd..9ceffc18b91 100644 --- a/v3/pkg/application/window.go +++ b/v3/pkg/application/window.go @@ -58,6 +58,7 @@ type Window interface { SetMaxSize(maxWidth, maxHeight int) Window SetMinSize(minWidth, minHeight int) Window SetRelativePosition(x, y int) Window + SetScreen(screen *Screen) Window SetResizable(b bool) Window SetIgnoreMouseEvents(ignore bool) Window SetSize(width, height int) Window From f0c5331db9074de1fb11b1a17da528401ef2f078 Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 08:22:44 -0600 Subject: [PATCH 05/10] screen manager get helpers --- v3/pkg/application/screenmanager.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/v3/pkg/application/screenmanager.go b/v3/pkg/application/screenmanager.go index 6caa1cd3332..a29887ed2d2 100644 --- a/v3/pkg/application/screenmanager.go +++ b/v3/pkg/application/screenmanager.go @@ -391,6 +391,24 @@ func (m *ScreenManager) GetPrimary() *Screen { return m.primaryScreen } +// GetByID returns the screen with the given display ID, or nil if not found. +func (m *ScreenManager) GetByID(id string) *Screen { + for _, screen := range m.screens { + if screen.ID == id { + return screen + } + } + return nil +} + +// GetByIndex returns the screen at the given index in the screen list, or nil if out of range. +func (m *ScreenManager) GetByIndex(index int) *Screen { + if index < 0 || index >= len(m.screens) { + return nil + } + return m.screens[index] +} + // Reference: https://source.chromium.org/chromium/chromium/src/+/main:ui/display/win/screen_win.cc;l=317 func (m *ScreenManager) calculateScreensDipCoordinates() error { remainingScreens := []*Screen{} @@ -873,3 +891,11 @@ func ScreenNearestPhysicalRect(physicalRect Rect) *Screen { func ScreenNearestDipRect(dipRect Rect) *Screen { return globalApplication.Screen.ScreenNearestDipRect(dipRect) } + +func GetScreenByID(id string) *Screen { + return globalApplication.Screen.GetByID(id) +} + +func GetScreenByIndex(index int) *Screen { + return globalApplication.Screen.GetByIndex(index) +} From 12dbb24133c7afdd84102fc392f5f2c6b472e700 Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 08:22:58 -0600 Subject: [PATCH 06/10] Set screen runtime --- .../desktop/@wailsio/runtime/src/screens.ts | 22 ++++++++++++++++ .../desktop/@wailsio/runtime/src/window.ts | 10 +++++++ .../application/messageprocessor_screens.go | 26 +++++++++++++++++++ v3/pkg/application/messageprocessor_window.go | 13 ++++++++++ 4 files changed, 71 insertions(+) diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts index c0ecfd7be14..adb12d205e5 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts @@ -59,6 +59,8 @@ const call = newRuntimeCaller(objectNames.Screens); const getAll = 0; const getPrimary = 1; const getCurrent = 2; +const getByID = 3; +const getByIndex = 4; /** * Gets all screens. @@ -86,3 +88,23 @@ export function GetPrimary(): Promise { export function GetCurrent(): Promise { return call(getCurrent); } + +/** + * Gets a screen by its unique display ID. + * + * @param id - The unique identifier of the screen. + * @returns A promise that resolves to the matching Screen. + */ +export function GetByID(id: string): Promise { + return call(getByID, { id }); +} + +/** + * Gets a screen by its index in the screen list. + * + * @param index - The zero-based index of the screen. + * @returns A promise that resolves to the matching Screen. + */ +export function GetByIndex(index: number): Promise { + return call(getByIndex, { index }); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts index 7a80fc17a3f..2e6dcfe004e 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts @@ -68,6 +68,7 @@ const ZoomResetMethod = 48; const SnapAssistMethod = 49; const FilesDropped = 50; const PrintMethod = 51; +const SetScreenMethod = 52; /** * Finds the nearest drop target element by walking up the DOM tree. @@ -671,6 +672,15 @@ class Window { cleanupNativeDrag(); } + /** + * Moves the window to the center of the specified screen's work area. + * + * @param screenID - The ID of the target screen. + */ + SetScreen(screenID: string): Promise { + return this[callerSym](SetScreenMethod, { screenID }); + } + /* Triggers Windows 11 Snap Assist feature (Windows only). * This is equivalent to pressing Win+Z and shows snap layout options. */ diff --git a/v3/pkg/application/messageprocessor_screens.go b/v3/pkg/application/messageprocessor_screens.go index 6fbadad123f..b718c5e0e8c 100644 --- a/v3/pkg/application/messageprocessor_screens.go +++ b/v3/pkg/application/messageprocessor_screens.go @@ -8,15 +8,21 @@ const ( ScreensGetAll = 0 ScreensGetPrimary = 1 ScreensGetCurrent = 2 + ScreensGetByID = 3 + ScreensGetByIndex = 4 ) var screensMethodNames = map[int]string{ ScreensGetAll: "GetAll", ScreensGetPrimary: "GetPrimary", ScreensGetCurrent: "GetCurrent", + ScreensGetByID: "GetByID", + ScreensGetByIndex: "GetByIndex", } func (m *MessageProcessor) processScreensMethod(req *RuntimeRequest) (any, error) { + args := req.Args.AsMap() + switch req.Method { case ScreensGetAll: return globalApplication.Screen.GetAll(), nil @@ -28,6 +34,26 @@ func (m *MessageProcessor) processScreensMethod(req *RuntimeRequest) (any, error return nil, errs.WrapInvalidScreensCallErrorf(err, "Window.GetScreen failed") } return screen, nil + case ScreensGetByID: + id := args.String("id") + if id == nil { + return nil, errs.NewInvalidScreensCallErrorf("missing or invalid argument 'id'") + } + screen := globalApplication.Screen.GetByID(*id) + if screen == nil { + return nil, errs.NewInvalidScreensCallErrorf("screen not found: %s", *id) + } + return screen, nil + case ScreensGetByIndex: + index := args.Int("index") + if index == nil { + return nil, errs.NewInvalidScreensCallErrorf("missing or invalid argument 'index'") + } + screen := globalApplication.Screen.GetByIndex(*index) + if screen == nil { + return nil, errs.NewInvalidScreensCallErrorf("screen not found at index: %d", *index) + } + return screen, nil default: return nil, errs.NewInvalidScreensCallErrorf("Unknown method: %d", req.Method) } diff --git a/v3/pkg/application/messageprocessor_window.go b/v3/pkg/application/messageprocessor_window.go index 1baf57606da..374b938906c 100644 --- a/v3/pkg/application/messageprocessor_window.go +++ b/v3/pkg/application/messageprocessor_window.go @@ -59,6 +59,7 @@ const ( WindowSnapAssist = 49 WindowFilesDropped = 50 WindowPrint = 51 + WindowSetScreen = 52 ) var windowMethodNames = map[int]string{ @@ -114,6 +115,7 @@ var windowMethodNames = map[int]string{ WindowFilesDropped: "FilesDropped", WindowSnapAssist: "SnapAssist", WindowPrint: "Print", + WindowSetScreen: "SetScreen", } var unit = struct{}{} @@ -394,6 +396,17 @@ func (m *MessageProcessor) processWindowMethod( return nil, fmt.Errorf("Window.Print failed: %w", err) } return unit, nil + case WindowSetScreen: + screenID := args.String("screenID") + if screenID == nil { + return nil, errs.NewInvalidWindowCallErrorf("missing or invalid argument 'screenID'") + } + screen := globalApplication.Screen.GetByID(*screenID) + if screen == nil { + return nil, errs.NewInvalidWindowCallErrorf("screen not found: %s", *screenID) + } + window.SetScreen(screen) + return unit, nil default: return nil, errs.NewInvalidWindowCallErrorf("Unknown method %d", req.Method) } From a9076d7a1e53c1c530c73d8d2155a6d78aeb66f3 Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 08:35:52 -0600 Subject: [PATCH 07/10] processAndCacheScreens linux - all variants --- v3/pkg/application/application_linux.go | 4 +- v3/pkg/application/application_linux_gtk4.go | 5 + v3/pkg/application/linux_cgo.go | 133 +++++++++++++------ v3/pkg/application/linux_purego.go | 74 ++++++++--- v3/pkg/application/screen_linux.go | 26 ++-- 5 files changed, 166 insertions(+), 76 deletions(-) diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index adc21435043..289bd4890fc 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -174,7 +174,9 @@ func (a *linuxApp) run() error { } a.parent.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(evt *ApplicationEvent) { - // TODO: What should happen here? + if err := a.processAndCacheScreens(); err != nil { + a.parent.handleError(err) + } }) a.setupCommonEvents() a.monitorThemeChanges() diff --git a/v3/pkg/application/application_linux_gtk4.go b/v3/pkg/application/application_linux_gtk4.go index 031dedbf66a..a82f42afa60 100644 --- a/v3/pkg/application/application_linux_gtk4.go +++ b/v3/pkg/application/application_linux_gtk4.go @@ -90,6 +90,11 @@ func (a *linuxApp) name() string { } func (a *linuxApp) run() error { + a.parent.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(evt *ApplicationEvent) { + if err := a.processAndCacheScreens(); err != nil { + a.parent.handleError(err) + } + }) return appRun(a.application) } diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index ee7add76142..37e72827218 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -970,50 +970,71 @@ func menuRadioItemNew(group *GSList, label string) pointer { func getScreenByIndex(display *C.struct__GdkDisplay, index int) *Screen { monitor := C.gdk_display_get_monitor(display, C.int(index)) - // TODO: Do we need to update Screen to contain current info? - // currentMonitor := C.gdk_display_get_monitor_at_window(display, window) var geometry C.GdkRectangle C.gdk_monitor_get_geometry(monitor, &geometry) - primary := false - if C.gdk_monitor_is_primary(monitor) == 1 { - primary = true - } + + var workarea C.GdkRectangle + C.gdk_monitor_get_workarea(monitor, &workarea) + + scaleFactor := float32(C.gdk_monitor_get_scale_factor(monitor)) + primary := C.gdk_monitor_is_primary(monitor) == 1 name := C.gdk_monitor_get_model(monitor) + + x := int(geometry.x) + y := int(geometry.y) + width := int(geometry.width) + height := int(geometry.height) + + pX := int(float32(x) * scaleFactor) + pY := int(float32(y) * scaleFactor) + pWidth := int(float32(width) * scaleFactor) + pHeight := int(float32(height) * scaleFactor) + + waX := int(workarea.x) + waY := int(workarea.y) + waWidth := int(workarea.width) + waHeight := int(workarea.height) + + pwaX := int(float32(waX) * scaleFactor) + pwaY := int(float32(waY) * scaleFactor) + pwaWidth := int(float32(waWidth) * scaleFactor) + pwaHeight := int(float32(waHeight) * scaleFactor) + return &Screen{ ID: fmt.Sprintf("%d", index), Name: C.GoString(name), IsPrimary: primary, - ScaleFactor: float32(C.gdk_monitor_get_scale_factor(monitor)), - X: int(geometry.x), - Y: int(geometry.y), + ScaleFactor: scaleFactor, + X: x, + Y: y, Size: Size{ - Height: int(geometry.height), - Width: int(geometry.width), + Height: height, + Width: width, }, Bounds: Rect{ - X: int(geometry.x), - Y: int(geometry.y), - Height: int(geometry.height), - Width: int(geometry.width), + X: x, + Y: y, + Height: height, + Width: width, }, PhysicalBounds: Rect{ - X: int(geometry.x), - Y: int(geometry.y), - Height: int(geometry.height), - Width: int(geometry.width), + X: pX, + Y: pY, + Height: pHeight, + Width: pWidth, }, WorkArea: Rect{ - X: int(geometry.x), - Y: int(geometry.y), - Height: int(geometry.height), - Width: int(geometry.width), + X: waX, + Y: waY, + Height: waHeight, + Width: waWidth, }, PhysicalWorkArea: Rect{ - X: int(geometry.x), - Y: int(geometry.y), - Height: int(geometry.height), - Width: int(geometry.width), + X: pwaX, + Y: pwaY, + Height: pwaHeight, + Width: pwaWidth, }, Rotation: 0.0, } @@ -1490,28 +1511,56 @@ func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { func getPrimaryScreen() (*Screen, error) { display := C.gdk_display_get_default() monitor := C.gdk_display_get_primary_monitor(display) - geometry := C.GdkRectangle{} + + var geometry C.GdkRectangle C.gdk_monitor_get_geometry(monitor, &geometry) - scaleFactor := int(C.gdk_monitor_get_scale_factor(monitor)) - // get the name for the screen + + var workarea C.GdkRectangle + C.gdk_monitor_get_workarea(monitor, &workarea) + + scaleFactor := float32(C.gdk_monitor_get_scale_factor(monitor)) name := C.gdk_monitor_get_model(monitor) + + x := int(geometry.x) + y := int(geometry.y) + width := int(geometry.width) + height := int(geometry.height) + return &Screen{ - ID: "0", - Name: C.GoString(name), - IsPrimary: true, - X: int(geometry.x), - Y: int(geometry.y), + ID: "0", + Name: C.GoString(name), + IsPrimary: true, + ScaleFactor: scaleFactor, + X: x, + Y: y, Size: Size{ - Height: int(geometry.height), - Width: int(geometry.width), + Height: height, + Width: width, }, Bounds: Rect{ - X: int(geometry.x), - Y: int(geometry.y), - Height: int(geometry.height), - Width: int(geometry.width), + X: x, + Y: y, + Height: height, + Width: width, + }, + PhysicalBounds: Rect{ + X: int(float32(x) * scaleFactor), + Y: int(float32(y) * scaleFactor), + Height: int(float32(height) * scaleFactor), + Width: int(float32(width) * scaleFactor), + }, + WorkArea: Rect{ + X: int(workarea.x), + Y: int(workarea.y), + Height: int(workarea.height), + Width: int(workarea.width), + }, + PhysicalWorkArea: Rect{ + X: int(float32(workarea.x) * scaleFactor), + Y: int(float32(workarea.y) * scaleFactor), + Height: int(float32(workarea.height) * scaleFactor), + Width: int(float32(workarea.width) * scaleFactor), }, - ScaleFactor: float32(scaleFactor), }, nil } diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index 7b2e17235f7..3c0b7ff5321 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -120,6 +120,7 @@ var ( gdkDisplayGetMonitorAtWindow func(pointer, pointer) pointer gdkDisplayGetNMonitors func(pointer) int gdkMonitorGetGeometry func(pointer, pointer) pointer + gdkMonitorGetWorkarea func(pointer, pointer) pointer gdkMonitorGetScaleFactor func(pointer) int gdkMonitorIsPrimary func(pointer) int gdkPixbufNewFromBytes func(uintptr, int, int, int, int, int, int) pointer @@ -279,6 +280,7 @@ func init() { purego.RegisterLibFunc(&gdkDisplayGetMonitorAtWindow, gtk, "gdk_display_get_monitor_at_window") purego.RegisterLibFunc(&gdkDisplayGetNMonitors, gtk, "gdk_display_get_n_monitors") purego.RegisterLibFunc(&gdkMonitorGetGeometry, gtk, "gdk_monitor_get_geometry") + purego.RegisterLibFunc(&gdkMonitorGetWorkarea, gtk, "gdk_monitor_get_workarea") purego.RegisterLibFunc(&gdkMonitorGetScaleFactor, gtk, "gdk_monitor_get_scale_factor") purego.RegisterLibFunc(&gdkMonitorIsPrimary, gtk, "gdk_monitor_is_primary") purego.RegisterLibFunc(&gdkPixbufNewFromBytes, gtk, "gdk_pixbuf_new_from_bytes") @@ -632,32 +634,66 @@ func menuRadioItemNew(group GSListPointer, label string) pointer { func getScreenByIndex(display pointer, index int) *Screen { monitor := gdkDisplayGetMonitor(display, index) - // TODO: Do we need to update Screen to contain current info? - // currentMonitor := C.gdk_display_get_monitor_at_window(display, window) - geometry := struct { - x int32 - y int32 - width int32 - height int32 - }{} - result := pointer(unsafe.Pointer(&geometry)) - gdkMonitorGetGeometry(monitor, result) - - primary := false - if gdkMonitorIsPrimary(monitor) == 1 { - primary = true + type gdkRect struct { + x, y, width, height int32 } + var geometry gdkRect + gdkMonitorGetGeometry(monitor, pointer(unsafe.Pointer(&geometry))) + + var workarea gdkRect + gdkMonitorGetWorkarea(monitor, pointer(unsafe.Pointer(&workarea))) + + scaleFactor := float32(gdkMonitorGetScaleFactor(monitor)) + primary := gdkMonitorIsPrimary(monitor) == 1 + + x := int(geometry.x) + y := int(geometry.y) + width := int(geometry.width) + height := int(geometry.height) + + waX := int(workarea.x) + waY := int(workarea.y) + waWidth := int(workarea.width) + waHeight := int(workarea.height) + return &Screen{ + ID: fmt.Sprintf("%d", index), + Name: "", IsPrimary: primary, - ScaleFactor: 1.0, - X: int(geometry.x), - Y: int(geometry.y), + ScaleFactor: scaleFactor, + X: x, + Y: y, Size: Size{ - Height: int(geometry.height), - Width: int(geometry.width), + Height: height, + Width: width, + }, + Bounds: Rect{ + X: x, + Y: y, + Height: height, + Width: width, + }, + PhysicalBounds: Rect{ + X: int(float32(x) * scaleFactor), + Y: int(float32(y) * scaleFactor), + Height: int(float32(height) * scaleFactor), + Width: int(float32(width) * scaleFactor), + }, + WorkArea: Rect{ + X: waX, + Y: waY, + Height: waHeight, + Width: waWidth, + }, + PhysicalWorkArea: Rect{ + X: int(float32(waX) * scaleFactor), + Y: int(float32(waY) * scaleFactor), + Height: int(float32(waHeight) * scaleFactor), + Width: int(float32(waWidth) * scaleFactor), }, + Rotation: 0.0, } } diff --git a/v3/pkg/application/screen_linux.go b/v3/pkg/application/screen_linux.go index 60507f5541e..a72aeb6a4b4 100644 --- a/v3/pkg/application/screen_linux.go +++ b/v3/pkg/application/screen_linux.go @@ -6,30 +6,28 @@ import ( "sync" ) -func (a *linuxApp) getPrimaryScreen() (*Screen, error) { +func (a *linuxApp) processAndCacheScreens() error { var wg sync.WaitGroup - var screen *Screen + var screens []*Screen var err error wg.Add(1) InvokeSync(func() { - screen, err = getPrimaryScreen() + screens, err = getScreens(a.application) wg.Done() }) wg.Wait() - return screen, err + if err != nil { + return err + } + return a.parent.Screen.LayoutScreens(screens) +} + +func (a *linuxApp) getPrimaryScreen() (*Screen, error) { + return a.parent.Screen.primaryScreen, nil } func (a *linuxApp) getScreens() ([]*Screen, error) { - var wg sync.WaitGroup - var screens []*Screen - var err error - wg.Add(1) - InvokeSync(func() { - screens, err = getScreens(a.application) - wg.Done() - }) - wg.Wait() - return screens, err + return a.parent.Screen.screens, nil } func getScreenForWindow(window *linuxWebviewWindow) (*Screen, error) { From a7d115bd54b8ad91f73cd16aa10803816bf4a286 Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 08:37:46 -0600 Subject: [PATCH 08/10] fix for wayland --- v3/pkg/application/screen_linux.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/v3/pkg/application/screen_linux.go b/v3/pkg/application/screen_linux.go index a72aeb6a4b4..8bfe9a9946c 100644 --- a/v3/pkg/application/screen_linux.go +++ b/v3/pkg/application/screen_linux.go @@ -19,6 +19,18 @@ func (a *linuxApp) processAndCacheScreens() error { if err != nil { return err } + // gdk_monitor_is_primary is unreliable on Wayland (always returns false). + // If no screen reports as primary, default to index 0. + hasPrimary := false + for _, s := range screens { + if s.IsPrimary { + hasPrimary = true + break + } + } + if !hasPrimary && len(screens) > 0 { + screens[0].IsPrimary = true + } return a.parent.Screen.LayoutScreens(screens) } From 0a288e639feb02626544e45f7dccc6a4461e11c2 Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 08:53:42 -0600 Subject: [PATCH 09/10] changelog --- website/src/pages/changelog.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index c5f51220e3d..fd1699908bb 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -14,7 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added `SetScreen` api for MacOS, Windows, Linux in [PR](https://github.com/wailsapp/wails/pull/5067) by @atterpac + ### Fixed +- Fixed `app.Screen` population on startup in Linux and MacOS in [PR](https://github.com/wailsapp/wails/pull/5067) by @atterpac - Fixed locking issue on Windows when multiselect dialog returns an error. Fixed in [PR](https://github.com/wailsapp/wails/pull/4156) by @johannes-luebke ## v2.9.1 - 2024-06-18 From 81d87b83eca5ee1543d30c899019375077772ac2 Mon Sep 17 00:00:00 2001 From: atterpac Date: Fri, 20 Mar 2026 09:55:38 -0600 Subject: [PATCH 10/10] appease the rabbit adds centerOnScreen helper to avoid placement logic in the shared layer --- v3/pkg/application/application_server.go | 1 + v3/pkg/application/linux_cgo.go | 22 +++++-- v3/pkg/application/linux_purego.go | 6 -- v3/pkg/application/screen_darwin.go | 10 +++ v3/pkg/application/screenmanager.go | 10 ++- v3/pkg/application/webview_window.go | 7 +-- v3/pkg/application/webview_window_android.go | 4 ++ v3/pkg/application/webview_window_darwin.go | 66 ++++++++++++++++++-- v3/pkg/application/webview_window_ios.go | 4 ++ v3/pkg/application/webview_window_linux.go | 52 +++++++-------- v3/pkg/application/webview_window_windows.go | 13 +++- website/src/pages/changelog.mdx | 4 +- 12 files changed, 146 insertions(+), 53 deletions(-) diff --git a/v3/pkg/application/application_server.go b/v3/pkg/application/application_server.go index 41059242f3c..6e58c4f6a57 100644 --- a/v3/pkg/application/application_server.go +++ b/v3/pkg/application/application_server.go @@ -529,6 +529,7 @@ func (w *serverWebviewWindow) bounds() Rect { retu func (w *serverWebviewWindow) setBounds(bounds Rect) {} func (w *serverWebviewWindow) position() (int, int) { return 0, 0 } func (w *serverWebviewWindow) setPosition(x int, y int) {} +func (w *serverWebviewWindow) centerOnScreen(_ *Screen) {} func (w *serverWebviewWindow) relativePosition() (int, int) { return 0, 0 } func (w *serverWebviewWindow) setRelativePosition(x int, y int) {} func (w *serverWebviewWindow) flash(enabled bool) {} diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 37e72827218..1b40de1b3f0 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -1510,16 +1510,26 @@ func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { func getPrimaryScreen() (*Screen, error) { display := C.gdk_display_get_default() - monitor := C.gdk_display_get_primary_monitor(display) + primaryMonitor := C.gdk_display_get_primary_monitor(display) + + // Find the index of the primary monitor so the ID matches getScreenByIndex's contract. + primaryIndex := 0 + count := int(C.gdk_display_get_n_monitors(display)) + for i := 0; i < count; i++ { + if C.gdk_display_get_monitor(display, C.int(i)) == primaryMonitor { + primaryIndex = i + break + } + } var geometry C.GdkRectangle - C.gdk_monitor_get_geometry(monitor, &geometry) + C.gdk_monitor_get_geometry(primaryMonitor, &geometry) var workarea C.GdkRectangle - C.gdk_monitor_get_workarea(monitor, &workarea) + C.gdk_monitor_get_workarea(primaryMonitor, &workarea) - scaleFactor := float32(C.gdk_monitor_get_scale_factor(monitor)) - name := C.gdk_monitor_get_model(monitor) + scaleFactor := float32(C.gdk_monitor_get_scale_factor(primaryMonitor)) + name := C.gdk_monitor_get_model(primaryMonitor) x := int(geometry.x) y := int(geometry.y) @@ -1527,7 +1537,7 @@ func getPrimaryScreen() (*Screen, error) { height := int(geometry.height) return &Screen{ - ID: "0", + ID: fmt.Sprintf("%d", primaryIndex), Name: C.GoString(name), IsPrimary: true, ScaleFactor: scaleFactor, diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index 3c0b7ff5321..24f901b0460 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -768,12 +768,6 @@ func windowFullscreen(window pointer) { gtkWindowFullScreen(window) } -func windowGetPosition(window pointer) (int, int) { - var x, y int - gtkWindowGetPosition(window, &x, &y) - return x, y -} - func windowGetCurrentMonitor(window pointer) pointer { // Get the monitor that the window is currently on display := gtkWidgetGetDisplay(window) diff --git a/v3/pkg/application/screen_darwin.go b/v3/pkg/application/screen_darwin.go index 4948dbd1185..06aefe8a61e 100644 --- a/v3/pkg/application/screen_darwin.go +++ b/v3/pkg/application/screen_darwin.go @@ -177,10 +177,20 @@ func (m *macosApp) processAndCacheScreens() error { } func (m *macosApp) getPrimaryScreen() (*Screen, error) { + if m.parent.Screen.primaryScreen == nil { + if err := m.processAndCacheScreens(); err != nil { + return nil, err + } + } return m.parent.Screen.primaryScreen, nil } func (m *macosApp) getScreens() ([]*Screen, error) { + if len(m.parent.Screen.screens) == 0 { + if err := m.processAndCacheScreens(); err != nil { + return nil, err + } + } return m.parent.Screen.screens, nil } diff --git a/v3/pkg/application/screenmanager.go b/v3/pkg/application/screenmanager.go index a29887ed2d2..cfaae65b9dc 100644 --- a/v3/pkg/application/screenmanager.go +++ b/v3/pkg/application/screenmanager.go @@ -370,13 +370,21 @@ func (s *Screen) physicalToDipRect(physicalRect Rect) Rect { // Layout screens in the virtual space with DIP calculations and cache the screens // for future coordinate transformation between the physical and logical (DIP) space func (m *ScreenManager) LayoutScreens(screens []*Screen) error { - if screens == nil || len(screens) == 0 { + if len(screens) == 0 { return errors.New("screens parameter is nil or empty") } + + // Store screens before DIP calculation so calculateScreensDipCoordinates + // can find the primary and mutate the slice in-place. + oldScreens := m.screens + oldPrimary := m.primaryScreen m.screens = screens err := m.calculateScreensDipCoordinates() if err != nil { + // Restore previous state on failure so callers never see partial data. + m.screens = oldScreens + m.primaryScreen = oldPrimary return err } diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 2b6a05a0f4c..aec674db830 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -90,6 +90,7 @@ type ( setBounds(bounds Rect) position() (int, int) setPosition(x int, y int) + centerOnScreen(screen *Screen) relativePosition() (int, int) setRelativePosition(x int, y int) flash(enabled bool) @@ -961,11 +962,7 @@ func (w *WebviewWindow) SetScreen(screen *Screen) Window { return w } InvokeSync(func() { - width, height := w.impl.size() - workArea := screen.WorkArea - x := workArea.X + (workArea.Width-width)/2 - y := workArea.Y + (workArea.Height-height)/2 - w.impl.setPosition(x, y) + w.impl.centerOnScreen(screen) }) return w } diff --git a/v3/pkg/application/webview_window_android.go b/v3/pkg/application/webview_window_android.go index 6e139f52c14..a03646c2e42 100644 --- a/v3/pkg/application/webview_window_android.go +++ b/v3/pkg/application/webview_window_android.go @@ -354,6 +354,10 @@ func (w *androidWebviewWindow) setPosition(_ int, _ int) { // Android doesn't support window positioning - apps are fullscreen } +func (w *androidWebviewWindow) centerOnScreen(_ *Screen) { + // Android doesn't support window positioning +} + func (w *androidWebviewWindow) setURL(url string) { // TODO: Implement via JNI androidLogf("debug", "setURL: %s", url) diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index a3f9691a391..5e0968c1769 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -641,6 +641,58 @@ void windowSetPosition(void* nsWindow, int x, int y) { } +// Center window on a specific screen identified by display ID +void windowCenterOnScreen(void* nsWindow, const char* screenID) { + WebviewWindow* window = (WebviewWindow*)nsWindow; + NSString* targetID = [NSString stringWithUTF8String:screenID]; + NSScreen* targetScreen = nil; + for (NSScreen* s in [NSScreen screens]) { + NSDictionary* desc = [s deviceDescription]; + NSNumber* num = [desc objectForKey:@"NSScreenNumber"]; + CGDirectDisplayID displayID = [num unsignedIntValue]; + NSString* sid = [NSString stringWithFormat:@"%d", displayID]; + if ([sid isEqualToString:targetID]) { + targetScreen = s; + break; + } + } + if (targetScreen == nil) { + targetScreen = [NSScreen mainScreen]; + } + NSRect visibleFrame = [targetScreen visibleFrame]; + NSRect windowFrame = [window frame]; + CGFloat x = visibleFrame.origin.x + (visibleFrame.size.width - windowFrame.size.width) / 2; + CGFloat y = visibleFrame.origin.y + (visibleFrame.size.height - windowFrame.size.height) / 2; + [window setFrameOrigin:NSMakePoint(x, y)]; +} + +// Position window relative to a specific screen's visible frame +void windowSetPositionOnScreen(void* nsWindow, int x, int y, const char* screenID) { + WebviewWindow* window = (WebviewWindow*)nsWindow; + NSString* targetID = [NSString stringWithUTF8String:screenID]; + NSScreen* targetScreen = nil; + for (NSScreen* s in [NSScreen screens]) { + NSDictionary* desc = [s deviceDescription]; + NSNumber* num = [desc objectForKey:@"NSScreenNumber"]; + CGDirectDisplayID displayID = [num unsignedIntValue]; + NSString* sid = [NSString stringWithFormat:@"%d", displayID]; + if ([sid isEqualToString:targetID]) { + targetScreen = s; + break; + } + } + if (targetScreen == nil) { + targetScreen = [NSScreen mainScreen]; + } + CGFloat scale = [targetScreen backingScaleFactor]; + NSRect visibleFrame = [targetScreen visibleFrame]; + NSRect windowFrame = [window frame]; + // x,y are in DIP top-origin coords relative to the screen's work area + CGFloat newX = visibleFrame.origin.x + (x / scale); + CGFloat newY = visibleFrame.origin.y + visibleFrame.size.height - windowFrame.size.height - (y / scale); + [window setFrameOrigin:NSMakePoint(newX, newY)]; +} + // Destroy window void windowDestroy(void* nsWindow) { [(WebviewWindow*)nsWindow close]; @@ -918,6 +970,12 @@ func (w *macosWebviewWindow) setPosition(x int, y int) { C.windowSetPosition(w.nsWindow, C.int(x), C.int(y)) } +func (w *macosWebviewWindow) centerOnScreen(screen *Screen) { + cID := C.CString(screen.ID) + defer C.free(unsafe.Pointer(cID)) + C.windowCenterOnScreen(w.nsWindow, cID) +} + func (w *macosWebviewWindow) print() error { C.windowPrint(w.nsWindow) return nil @@ -1369,13 +1427,13 @@ func (w *macosWebviewWindow) run() { case WindowStateNormal: } if options.Screen != nil { - workArea := options.Screen.WorkArea + cID := C.CString(options.Screen.ID) if w.parent.options.InitialPosition == WindowCentered { - width, height := w.size() - w.setPosition(workArea.X+(workArea.Width-width)/2, workArea.Y+(workArea.Height-height)/2) + C.windowCenterOnScreen(w.nsWindow, cID) } else { - w.setPosition(workArea.X+options.X, workArea.Y+options.Y) + C.windowSetPositionOnScreen(w.nsWindow, C.int(options.X), C.int(options.Y), cID) } + C.free(unsafe.Pointer(cID)) } else if w.parent.options.InitialPosition == WindowCentered { C.windowCenter(w.nsWindow) } else { diff --git a/v3/pkg/application/webview_window_ios.go b/v3/pkg/application/webview_window_ios.go index 481788c87e3..b2e9e13b7e5 100644 --- a/v3/pkg/application/webview_window_ios.go +++ b/v3/pkg/application/webview_window_ios.go @@ -392,6 +392,10 @@ func (w *iosWebviewWindow) setPosition(_ int, _ int) { // iOS doesn't support window positioning - apps are fullscreen } +func (w *iosWebviewWindow) centerOnScreen(_ *Screen) { + // iOS doesn't support window positioning +} + func (w *iosWebviewWindow) setURL(url string) { if w.nativeHandle == nil || url == "" { return diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 5b61f86e0eb..279a4126eb1 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -216,6 +216,30 @@ func (w *linuxWebviewWindow) setPosition(x int, y int) { w.move(x, y) } +func (w *linuxWebviewWindow) applyScreenPlacement() { + opts := w.parent.options + if opts.Screen != nil { + if opts.InitialPosition == WindowCentered { + w.centerOnScreen(opts.Screen) + } else { + workArea := opts.Screen.WorkArea + w.setPosition(workArea.X+opts.X, workArea.Y+opts.Y) + } + } else if opts.InitialPosition == WindowCentered { + w.center() + } else { + w.setRelativePosition(opts.X, opts.Y) + } +} + +func (w *linuxWebviewWindow) centerOnScreen(screen *Screen) { + workArea := screen.WorkArea + width, height := w.size() + x := workArea.X + (workArea.Width-width)/2 + y := workArea.Y + (workArea.Height-height)/2 + w.setPosition(x, y) +} + func (w *linuxWebviewWindow) bounds() Rect { // DOTO: do it in a single step + proper DPI scaling x, y := w.position() @@ -339,19 +363,7 @@ func (w *linuxWebviewWindow) run() { w.setFrameless(w.parent.options.Frameless) - if w.parent.options.Screen != nil { - workArea := w.parent.options.Screen.WorkArea - if w.parent.options.InitialPosition == WindowCentered { - width, height := w.size() - w.setPosition(workArea.X+(workArea.Width-width)/2, workArea.Y+(workArea.Height-height)/2) - } else { - w.setPosition(workArea.X+w.parent.options.X, workArea.Y+w.parent.options.Y) - } - } else if w.parent.options.InitialPosition == WindowCentered { - w.center() - } else { - w.setPosition(w.parent.options.X, w.parent.options.Y) - } + w.applyScreenPlacement() switch w.parent.options.StartState { case WindowStateMaximised: @@ -395,19 +407,7 @@ func (w *linuxWebviewWindow) run() { } if !w.parent.options.Hidden { w.show() - if w.parent.options.Screen != nil { - workArea := w.parent.options.Screen.WorkArea - if w.parent.options.InitialPosition == WindowCentered { - width, height := w.size() - w.setPosition(workArea.X+(workArea.Width-width)/2, workArea.Y+(workArea.Height-height)/2) - } else { - w.setPosition(workArea.X+w.parent.options.X, workArea.Y+w.parent.options.Y) - } - } else if w.parent.options.InitialPosition == WindowCentered { - w.center() - } else { - w.setRelativePosition(w.parent.options.X, w.parent.options.Y) - } + w.applyScreenPlacement() } if w.parent.options.DevToolsEnabled || globalApplication.isDebugMode { w.enableDevTools() diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 0c8fc1851cb..71a47f43714 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -552,11 +552,10 @@ func (w *windowsWebviewWindow) run() { } if options.Screen != nil { - workArea := options.Screen.WorkArea if options.InitialPosition == WindowCentered { - width, height := w.size() - w.setPosition(workArea.X+(workArea.Width-width)/2, workArea.Y+(workArea.Height-height)/2) + w.centerOnScreen(options.Screen) } else { + workArea := options.Screen.WorkArea w.setPosition(workArea.X+options.X, workArea.Y+options.Y) } } else if options.InitialPosition == WindowCentered { @@ -746,6 +745,14 @@ func (w *windowsWebviewWindow) setPosition(x int, y int) { w.setBounds(bounds) } +func (w *windowsWebviewWindow) centerOnScreen(screen *Screen) { + workArea := screen.WorkArea + width, height := w.size() + x := workArea.X + (workArea.Width-width)/2 + y := workArea.Y + (workArea.Height-height)/2 + w.setPosition(x, y) +} + // Get window position relative to the screen WorkArea on which it is func (w *windowsWebviewWindow) relativePosition() (int, int) { screen, _ := w.getScreen() diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index fd1699908bb..cc612c5f38e 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -15,10 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Added `SetScreen` api for MacOS, Windows, Linux in [PR](https://github.com/wailsapp/wails/pull/5067) by @atterpac +- Added `SetScreen` api for macOS, Windows, Linux in [PR](https://github.com/wailsapp/wails/pull/5067) by @atterpac ### Fixed -- Fixed `app.Screen` population on startup in Linux and MacOS in [PR](https://github.com/wailsapp/wails/pull/5067) by @atterpac +- Fixed `app.Screen` population on startup in Linux and macOS in [PR](https://github.com/wailsapp/wails/pull/5067) by @atterpac - Fixed locking issue on Windows when multiselect dialog returns an error. Fixed in [PR](https://github.com/wailsapp/wails/pull/4156) by @johannes-luebke ## v2.9.1 - 2024-06-18