From 7d56e4335e8be674d9691182ce466adb04e814b3 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 16 Dec 2019 10:52:53 +0900 Subject: [PATCH] ui: Add RunGame, WindowSize and SetWindowSize This change introduces the new APIs RunGame, WindowSize and SetWindowSize. These new APIs hides the notion of 'scale', and is more flexible with the outside size change. This means that we can introduce a resizable window. This change also adds -legacy flag to examples/windowsize. If the flag is off, the new APIs are used. This change deprecates these functions since the notion of 'scale' is deprecated: * ScreenScale * ScreenSizeInFullscreen * SetScreenScale * SetScreenSize Fixes #943, #571 Updates #320 --- doc.go | 38 ++++++++- examples/windowsize/main.go | 82 ++++++++++++++++--- internal/driver/ui.go | 1 + internal/uidriver/glfw/ui.go | 57 ++++++++++--- internal/uidriver/js/ui.go | 4 + internal/uidriver/mobile/ui.go | 4 + run.go | 141 ++++++++++++++++----------------- uicontext.go | 12 ++- window.go | 20 ++++- 9 files changed, 255 insertions(+), 104 deletions(-) diff --git a/doc.go b/doc.go index ab5ef315d..e5a4e16cd 100644 --- a/doc.go +++ b/doc.go @@ -14,7 +14,43 @@ // Package ebiten provides graphics and input API to develop a 2D game. // -// You can start the game by calling the function Run. +// You can start the game by calling the function RunGame. +// +// type Game struct{} +// +// // Update is called every frame (1/60 [s]). +// func (g *Game) Update(screen *ebiten.Image) error { +// +// // Write your game's logical update. +// +// if ebiten.IsDrawingSkipped() { +// // When the game is running slowly, the rendering result +// // will not be adopted. +// return nil +// } +// +// // Write your game's rendering. +// +// return nil +// } +// +// // Layout takes the outside size (e.g., the window size) and returns the (logical) screen size. +// // If you don't have to adjust the screen size with the outside size, just return a fixed size. +// func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { +// return 320, 240 +// } +// +// func main() { +// // Sepcify the window size as you like. Here, a doulbed size is specified. +// ebiten.SetWindowSize(640, 480) +// ebiten.SetTitle("Your game's title") +// // Call ebiten.RunGame to start your game loop. +// if err := ebiten.Run(game); err != nil { +// log.Fatal(err) +// } +// } +// +// For backward compatibility, you can use a shorthand style Run. // // // update is called every frame (1/60 [s]). // func update(screen *ebiten.Image) error { diff --git a/examples/windowsize/main.go b/examples/windowsize/main.go index b5d8ab31a..3b1ec74a0 100644 --- a/examples/windowsize/main.go +++ b/examples/windowsize/main.go @@ -37,6 +37,22 @@ import ( ) var ( + // flagLegacy represents whether the legacy APIs are used or not. + // If flagLegacy is true, these legacy APIs are used: + // + // * ebiten.Run + // * ebiten.ScreenScale + // * ebiten.SetScreenScale + // * ebiten.SetScreenSize + // + // If flagLegacy is false, these APIs are used: + // + // * ebiten.RunGame + // * ebiten.SetWindowSize + // * ebiten.WindowSize + flagLegacy = flag.Bool("legacy", false, "use the legacy API") + + flagFullscreen = flag.Bool("fullscreen", false, "fullscreen") flagWindowPosition = flag.String("windowposition", "", "window position (e.g., 100,200)") flagScreenTransparent = flag.Bool("screentransparent", false, "screen transparent") ) @@ -76,10 +92,32 @@ func createRandomIconImage() image.Image { return img } -func update(screen *ebiten.Image) error { - screenScale := ebiten.ScreenScale() - const d = 16 - screenWidth, screenHeight := screen.Size() +type game struct { + width int + height int +} + +func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) { + // Ignore the outside size. This means that the offscreen is not adjusted with the outside world. + return g.width, g.height +} + +func (g *game) Update(screen *ebiten.Image) error { + var ( + screenWidth int + screenHeight int + screenScale float64 + ) + if *flagLegacy { + screenWidth, screenHeight = screen.Size() + screenScale = ebiten.ScreenScale() + } else { + screenWidth = g.width + screenHeight = g.height + ww, _ := ebiten.WindowSize() + screenScale = float64(ww) / float64(g.width) + } + fullscreen := ebiten.IsFullscreen() runnableInBackground := ebiten.IsRunnableInBackground() cursorVisible := ebiten.IsCursorVisible() @@ -89,6 +127,7 @@ func update(screen *ebiten.Image) error { positionX, positionY := ebiten.WindowPosition() transparent := ebiten.IsScreenTransparent() + const d = 16 if ebiten.IsKeyPressed(ebiten.KeyShift) { if inpututil.IsKeyJustPressed(ebiten.KeyUp) { screenHeight += d @@ -164,9 +203,14 @@ func update(screen *ebiten.Image) error { decorated = !decorated } - ebiten.SetScreenSize(screenWidth, screenHeight) - // TODO: Add a flag for compatibility mode and call SetScreenScale only when the flag is on. - ebiten.SetScreenScale(screenScale) + if *flagLegacy { + ebiten.SetScreenSize(screenWidth, screenHeight) + ebiten.SetScreenScale(screenScale) + } else { + g.width = screenWidth + g.height = screenHeight + ebiten.SetWindowSize(int(float64(screenWidth)*screenScale), int(float64(screenHeight)*screenScale)) + } ebiten.SetFullscreen(fullscreen) ebiten.SetRunnableInBackground(runnableInBackground) ebiten.SetCursorVisible(cursorVisible) @@ -268,7 +312,27 @@ func main() { } ebiten.SetScreenTransparent(*flagScreenTransparent) - if err := ebiten.Run(update, initScreenWidth, initScreenHeight, initScreenScale, "Window Size (Ebiten Demo)"); err != nil { - log.Fatal(err) + g := &game{ + width: initScreenWidth, + height: initScreenHeight, + } + + if *flagFullscreen { + ebiten.SetFullscreen(true) + } + + const title = "Window Size (Ebiten Demo)" + if *flagLegacy { + if err := ebiten.Run(g.Update, g.width, g.height, initScreenScale, title); err != nil { + log.Fatal(err) + } + } else { + w := int(float64(g.width) * initScreenScale) + h := int(float64(g.height) * initScreenScale) + ebiten.SetWindowSize(w, h) + ebiten.SetWindowTitle(title) + if err := ebiten.RunGame(g); err != nil { + log.Fatal(err) + } } } diff --git a/internal/driver/ui.go b/internal/driver/ui.go index 6287cb5bd..03f2732c5 100644 --- a/internal/driver/ui.go +++ b/internal/driver/ui.go @@ -43,6 +43,7 @@ type UI interface { IsWindowResizable() bool ScreenSizeInFullscreen() (int, int) WindowPosition() (int, int) + WindowSize() (int, int) IsScreenTransparent() bool MonitorPosition() (int, int) CanHaveWindow() bool // TODO: Create a 'Widnow' interface. diff --git a/internal/uidriver/glfw/ui.go b/internal/uidriver/glfw/ui.go index edc8983b3..f715e28a0 100644 --- a/internal/uidriver/glfw/ui.go +++ b/internal/uidriver/glfw/ui.go @@ -64,6 +64,8 @@ type UserInterface struct { initWindowResizable bool initWindowPositionXInDP int initWindowPositionYInDP int + initWindowWidthInDP int + initWindowHeightInDP int initScreenTransparent bool initIconImages []image.Image @@ -80,17 +82,19 @@ type UserInterface struct { const ( maxInt = int(^uint(0) >> 1) minInt = -maxInt - 1 - invalidPos = minInt + invalidVal = minInt ) var ( theUI = &UserInterface{ - origPosX: invalidPos, - origPosY: invalidPos, + origPosX: invalidVal, + origPosY: invalidVal, initCursorMode: driver.CursorModeVisible, initWindowDecorated: true, - initWindowPositionXInDP: invalidPos, - initWindowPositionYInDP: invalidPos, + initWindowPositionXInDP: invalidVal, + initWindowPositionYInDP: invalidVal, + initWindowWidthInDP: invalidVal, + initWindowHeightInDP: invalidVal, vsync: true, } ) @@ -302,10 +306,10 @@ func (u *UserInterface) setInitIconImages(iconImages []image.Image) { func (u *UserInterface) getInitWindowPosition() (int, int) { u.m.RLock() defer u.m.RUnlock() - if u.initWindowPositionXInDP != invalidPos && u.initWindowPositionYInDP != invalidPos { + if u.initWindowPositionXInDP != invalidVal && u.initWindowPositionYInDP != invalidVal { return u.initWindowPositionXInDP, u.initWindowPositionYInDP } - return invalidPos, invalidPos + return invalidVal, invalidVal } func (u *UserInterface) setInitWindowPosition(x, y int) { @@ -316,6 +320,19 @@ func (u *UserInterface) setInitWindowPosition(x, y int) { u.initWindowPositionYInDP = y } +func (u *UserInterface) getInitWindowSize() (int, int) { + u.m.Lock() + w, h := u.initWindowWidthInDP, u.initWindowHeightInDP + u.m.Unlock() + return w, h +} + +func (u *UserInterface) setInitWindowSize(width, height int) { + u.m.Lock() + u.initWindowWidthInDP, u.initWindowHeightInDP = width, height + u.m.Unlock() +} + // toDeviceIndependentPixel must be called from the main thread. func (u *UserInterface) toDeviceIndependentPixel(x float64) float64 { return x / u.glfwScale() @@ -695,12 +712,16 @@ func (u *UserInterface) run(context driver.UIContext) error { } u.SetWindowPosition(u.getInitWindowPosition()) + if w, h := u.getInitWindowSize(); w != invalidVal && h != invalidVal { + w = int(u.toDeviceDependentPixel(float64(w))) + h = int(u.toDeviceDependentPixel(float64(h))) + u.setWindowSize(w, h, u.isFullscreen(), u.vsync) + } _ = u.t.Call(func() error { u.title = u.getInitTitle() u.window.SetTitle(u.title) u.window.Show() - return nil }) @@ -892,7 +913,7 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool, vsync u.swapBuffers() if fullscreen { - if u.origPosX == invalidPos || u.origPosY == invalidPos { + if u.origPosX == invalidVal || u.origPosY == invalidVal { u.origPosX, u.origPosY = u.window.GetPos() } m := u.currentMonitor() @@ -941,7 +962,7 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool, vsync width = minWindowWidth } - if u.origPosX != invalidPos && u.origPosY != invalidPos { + if u.origPosX != invalidVal && u.origPosY != invalidVal { x := u.origPosX y := u.origPosY u.window.SetPos(x, y) @@ -951,8 +972,8 @@ func (u *UserInterface) setWindowSize(width, height int, fullscreen bool, vsync u.window.SetPos(x+1, y) u.window.SetPos(x, y) } - u.origPosX = invalidPos - u.origPosY = invalidPos + u.origPosX = invalidVal + u.origPosY = invalidVal } // Set the window size after the position. The order matters. @@ -1083,9 +1104,19 @@ func (u *UserInterface) IsScreenTransparent() bool { return val } +func (u *UserInterface) WindowSize() (int, int) { + if !u.isRunning() { + return u.getInitWindowSize() + } + w := int(u.toDeviceIndependentPixel(float64(u.windowWidth))) + h := int(u.toDeviceIndependentPixel(float64(u.windowHeight))) + return w, h +} + func (u *UserInterface) SetWindowSize(width, height int) { if !u.isRunning() { - panic("glfw: SetWindowSize can't be called before the main loop starts") + u.setInitWindowSize(width, height) + return } w := int(u.toDeviceDependentPixel(float64(width))) h := int(u.toDeviceDependentPixel(float64(height))) diff --git a/internal/uidriver/js/ui.go b/internal/uidriver/js/ui.go index be26e740a..3ac0ccfd8 100644 --- a/internal/uidriver/js/ui.go +++ b/internal/uidriver/js/ui.go @@ -142,6 +142,10 @@ func (u *UserInterface) SetWindowResizable(decorated bool) { // Do nothing } +func (u *UserInterface) WindowSize() (int, int) { + return 0, 0 +} + func (u *UserInterface) SetWindowSize(width, height int) { // TODO: This is too tricky: Even though browsers don't have windows, SetWindowSize is called whenever the // screen size is changed. Fix this hack. diff --git a/internal/uidriver/mobile/ui.go b/internal/uidriver/mobile/ui.go index 22b29b993..34636003f 100644 --- a/internal/uidriver/mobile/ui.go +++ b/internal/uidriver/mobile/ui.go @@ -433,6 +433,10 @@ func (u *UserInterface) SetWindowSize(width, height int) { // Do nothing } +func (u *UserInterface) WindowSize() (int, int) { + return 0, 0 +} + func (u *UserInterface) CanHaveWindow() bool { return false } diff --git a/run.go b/run.go index ef5060424..2bd2c7c63 100644 --- a/run.go +++ b/run.go @@ -24,14 +24,12 @@ import ( var _ = __EBITEN_REQUIRES_GO_VERSION_1_12_OR_LATER__ // Game defines necessary functions for a game. -// -// Note: This interface is not used anywhere yet. type Game interface { // Update updates a game by one frame. Update(*Image) error - // Layout accepts a native outside size in DP (device-independent pixels) and returns the game's logical - // screen size. + // Layout accepts a native outside size in device-independent pixels and returns the game's logical screen + // size. // // The screen scale is automatically adjusted to fit the outside. // @@ -104,11 +102,14 @@ func IsRunningSlowly() bool { return IsDrawingSkipped() } -// Run runs the game. +// Run starts the main loop and runs the game. // f is a function which is called at every frame. // The argument (*Image) is the render target that represents the screen. // The screen size is based on the given values (width and height). // +// Run is a shorthand for RunGame, but there are some restrictions. +// If you want to resize the window by dragging, use RunGame instead. +// // A window size is based on the given values (width, height and scale). // // scale is used to enlarge the screen on desktops. @@ -156,7 +157,9 @@ func Run(f func(*Image) error, width, height int, scale float64, title string) e } theUIContext = newUIContext(game, scale) - fixWindowPosition(int(float64(width)*scale), int(float64(height)*scale)) + ww, wh := int(float64(width)*scale), int(float64(height)*scale) + fixWindowPosition(ww, wh) + SetWindowSize(ww, wh) SetWindowTitle(title) if err := uiDriver().Run(theUIContext, graphicsDriver()); err != nil { if err == driver.RegularTermination { @@ -168,6 +171,58 @@ func Run(f func(*Image) error, width, height int, scale float64, title string) e return nil } +// RunGame starts the main loop and runs the game. +// game's Update function is called every frame. +// game's Layout function is called when necessary, and you can specify the logical screen size in the function. +// +// RunGame is a more flexibile form of Run due to 'Layout' function. +// The window is resizable if you use RunGame, while you cannot if you use Run. +// RunGame is more sophisticated way than Run and hides the notion of 'scale'. +// +// On desktops, SetWindowSize must be called before RunGame is called. +// +// A window size is based on the given values (width, height and scale). +// +// RunGame must be called on the main thread. +// Note that Ebiten bounds the main goroutine to the main OS thread by runtime.LockOSThread. +// +// Ebiten tries to call game's Update function 60 times a second by default. In other words, +// TPS (ticks per second) is 60 by default. +// This is not related to framerate (display's refresh rate). +// +// game's Update is not called when the window is in background by default. +// This setting is configurable with SetRunnableInBackground. +// +// The given scale is ignored on fullscreen mode or gomobile-build mode. +// +// On non-GopherJS environments, RunGame returns error when 1) OpenGL error happens, 2) audio error happens or +// 3) f returns error. In the case of 3), RunGame returns the same error. +// +// On GopherJS, RunGame returns immediately. +// It is because the 'main' goroutine cannot be blocked on GopherJS due to the bug (gopherjs/gopherjs#826). +// When an error happens, this is shown as an error on the console. +// +// The size unit is device-independent pixel. +// +// Don't call RunGame twice or more in one process. +func RunGame(game Game) error { + if uiDriver().CanHaveWindow() { + w, h := WindowSize() + if w < 0 || h < 0 { + panic("ebiten: SetWindowSize must be called before RunGame on desktops") + } + fixWindowPosition(w, h) + } + theUIContext = newUIContext(game, 1) + if err := uiDriver().Run(theUIContext, graphicsDriver()); err != nil { + if err == driver.RegularTermination { + return nil + } + return err + } + return nil +} + // RunWithoutMainLoop runs the game, but don't call the loop on the main (UI) thread. // Different from Run, RunWithoutMainLoop returns immediately. // @@ -184,49 +239,17 @@ func RunWithoutMainLoop(f func(*Image) error, width, height int, scale float64, return uiDriver().RunWithoutMainLoop(width, height, scale, title, theUIContext, graphicsDriver()) } -// ScreenSizeInFullscreen returns the size in device-independent pixels when the game is fullscreen. -// The adopted monitor is the 'current' monitor which the window belongs to. -// The returned value can be given to Run or SetSize function if the perfectly fit fullscreen is needed. -// -// On browsers, ScreenSizeInFullscreen returns the 'window' (global object) size, not 'screen' size since an Ebiten game -// should not know the outside of the window object. -// For more details, see SetFullscreen API comment. -// -// On mobiles, ScreenSizeInFullscreen returns (0, 0) so far. -// -// If you use this for screen size with SetFullscreen(true), you can get the fullscreen mode -// which size is well adjusted with the monitor. -// -// w, h := ScreenSizeInFullscreen() -// ebiten.SetFullscreen(true) -// ebiten.Run(update, w, h, 1, "title") -// -// Furthermore, you can use them with DeviceScaleFactor(), you can get the finest -// fullscreen mode. -// -// s := ebiten.DeviceScaleFactor() -// w, h := ScreenSizeInFullscreen() -// ebiten.SetFullscreen(true) -// ebiten.Run(update, int(float64(w) * s), int(float64(h) * s), 1/s, "title") -// -// For actual example, see examples/fullscreen -// -// ScreenSizeInFullscreen must be called on the main thread before ebiten.Run, and is concurrent-safe after ebiten.Run. +// ScreenSizeInFullscreen is deprecated as of 1.11.0-alpha. func ScreenSizeInFullscreen() (int, int) { return uiDriver().ScreenSizeInFullscreen() } -// MonitorSize is deprecated as of 1.8.0-alpha. Use ScreenSizeInFullscreen instead. +// MonitorSize is deprecated as of 1.8.0-alpha. func MonitorSize() (int, int) { return ScreenSizeInFullscreen() } -// SetScreenSize changes the (logical) size of the screen. -// SetScreenSize adjusts the window size on desktops without changing its scale. -// -// The unit is device-independent pixel. -// -// SetScreenSize is concurrent-safe. +// SetScreenSize is deprecated as of 1.11.0-alpha. Use SetWindowSize and RunGame (Game's Layout) instead. func SetScreenSize(width, height int) { if width <= 0 || height <= 0 { panic("ebiten: width and height must be positive") @@ -237,27 +260,7 @@ func SetScreenSize(width, height int) { theUIContext.SetScreenSize(width, height) } -// SetScreenScale changes the scale of the screen on desktops. -// -// Note that the actual screen is multiplied not only by the given scale but also -// by the device scale on high-DPI display. -// If you pass inverse of the device scale, -// you can disable this automatical device scaling as a result. -// You can get the device scale by DeviceScaleFactor function. -// -// On browsers, SetScreenScale saves the given value and affects the returned value of ScreenScale, -// but does not affect actual rendering. -// SetScreenScale works as this as of 1.10.0-alpha. -// Before that, SetScreenScale affected the rendering scale. -// -// On mobiles, SetScreenScale works, but usually the user doesn't have to call this. -// Instead, ebitenmobile calls this automatically. -// -// SetScreenScale panics if scale is not a positive number. -// -// SetScreenScale is concurrent-safe. -// -// TODO: Deprecate this function. +// SetScreenScale is deprecated as of 1.11.0-alpha. Use SetWindowSize instead. func SetScreenScale(scale float64) { if scale <= 0 { panic("ebiten: scale must be positive") @@ -268,15 +271,7 @@ func SetScreenScale(scale float64) { theUIContext.setScaleForWindow(scale) } -// ScreenScale returns the current screen scale. -// -// On browsers, this value does not affect actual rendering. -// -// If Run is not called, this returns 0. -// -// ScreenScale is concurrent-safe. -// -// TODO: Deprecate this function. +// ScreenScale is deprecated as of 1.11.0-alpha. Use WindowSize instead. func ScreenScale() float64 { if theUIContext == nil { panic("ebiten: ScreenScale can't be called before the main loop starts") @@ -391,7 +386,7 @@ func SetRunnableInBackground(runnableInBackground bool) { // DeviceScaleFactor might panic on init function on some devices like Android. // Then, it is not recommended to call DeviceScaleFactor from init functions. // -// DeviceScaleFactor must be called on the main thread before ebiten.Run, and is concurrent-safe after ebiten.Run. +// DeviceScaleFactor must be called on the main thread before the main loop, and is concurrent-safe after the main loop. func DeviceScaleFactor() float64 { return uiDriver().DeviceScaleFactor() } @@ -462,7 +457,7 @@ func IsScreenTransparent() bool { // SetScreenTransparent sets the state if the window is transparent. // -// SetScreenTransparent panics if SetScreenTransparent is called after Run. +// SetScreenTransparent panics if SetScreenTransparent is called after the main loop. // // SetScreenTransparent does nothing on mobiles. func SetScreenTransparent(transparent bool) { diff --git a/uicontext.go b/uicontext.go index 49e60c8cd..a2e6d04ec 100644 --- a/uicontext.go +++ b/uicontext.go @@ -86,7 +86,7 @@ var theUIContext *uiContext func (c *uiContext) setScaleForWindow(scale float64) { g, ok := c.game.(*defaultGame) if !ok { - panic("ebiten: setScaleForWindow must be called when Run is used") + panic("ebiten: setScaleForWindow can be called only when Run is used") } c.m.Lock() @@ -99,7 +99,7 @@ func (c *uiContext) setScaleForWindow(scale float64) { func (c *uiContext) getScaleForWindow() float64 { if _, ok := c.game.(*defaultGame); !ok { - panic("ebiten: getScaleForWindow must be called when Run is used") + panic("ebiten: getScaleForWindow can be called only when Run is used") } c.m.Lock() @@ -115,7 +115,7 @@ func (c *uiContext) getScaleForWindow() float64 { func (c *uiContext) SetScreenSize(width, height int) { g, ok := c.game.(*defaultGame) if !ok { - panic("ebiten: SetScreenSize must be called when Run is used") + panic("ebiten: SetScreenSize can be called only when Run is used") } c.m.Lock() @@ -157,7 +157,11 @@ func (c *uiContext) updateOffscreen() { if c.offscreen == nil { c.offscreen = newImage(sw, sh, FilterDefault, true) } - c.SetScreenSize(sw, sh) + + // The window size is automatically adjusted when Run is used. + if _, ok := c.game.(*defaultGame); ok { + c.SetScreenSize(sw, sh) + } // TODO: This is duplicated with mobile/ebitenmobileview/funcs.go. Refactor this. d := uiDriver().DeviceScaleFactor() diff --git a/window.go b/window.go index 7246657dc..e2205447e 100644 --- a/window.go +++ b/window.go @@ -55,7 +55,7 @@ func IsWindowDecorated() bool { // setWindowResizable works only on desktops. // setWindowResizable does nothing on other platforms. // -// setWindowResizable panics if setWindowResizable is called after Run. +// setWindowResizable panics if setWindowResizable is called after the main loop. // // setWindowResizable is concurrent-safe. func setWindowResizable(resizable bool) { @@ -104,7 +104,7 @@ func SetWindowIcon(iconImages []image.Image) { // WindowPosition returns the window position. // -// WindowPosition panics before Run is called. +// WindowPosition panics if the main loop does not start yet. // // WindowPosition returns the last window position on fullscreen mode. // @@ -120,8 +120,6 @@ func WindowPosition() (x, y int) { // SetWindowPosition sets the window position. // -// SetWindowPosition works before and after Run is called. -// // SetWindowPosition does nothing on fullscreen mode. // // SetWindowPosition does nothing on browsers and mobiles. @@ -185,3 +183,17 @@ func fixWindowPosition(width, height int) { uiDriver().SetWindowPosition(initWindowPositionX, initWindowPositionY) } } + +// WindowSize returns the window size. On fullscreen mode, WindowSize returns the original window size. +// +// WindowSize is concurrent-safe. +func WindowSize() (int, int) { + return uiDriver().WindowSize() +} + +// SetWindowSize sets the window size. On fullscreen mode, SetWindowSize sets the original window size. +// +// SetWindowSize is concurrent-safe. +func SetWindowSize(width, height int) { + uiDriver().SetWindowSize(width, height) +}