diff --git a/examples/windowsize/main.go b/examples/windowsize/main.go index 7391cd7df..7dc008c93 100644 --- a/examples/windowsize/main.go +++ b/examples/windowsize/main.go @@ -165,6 +165,7 @@ func update(screen *ebiten.Image) error { } ebiten.SetScreenSize(screenWidth, screenHeight) + // TODO: Add a flag for compatibility mode and call SetScreenScale only when the flag is on. ebiten.SetScreenScale(screenScale) ebiten.SetFullscreen(fullscreen) ebiten.SetRunnableInBackground(runnableInBackground) diff --git a/internal/driver/ui.go b/internal/driver/ui.go index 8d9055a29..1e2f1cb6d 100644 --- a/internal/driver/ui.go +++ b/internal/driver/ui.go @@ -20,8 +20,9 @@ import ( ) type UIContext interface { - SetSize(width, height int, scale float64) Update(afterFrameUpdate func()) error + Layout(outsideWidth, outsideHeight float64) + AdjustPosition(x, y float64) (float64, float64) } // RegularTermination represents a regular termination. @@ -40,23 +41,21 @@ type UI interface { IsVsyncEnabled() bool IsWindowDecorated() bool IsWindowResizable() bool - ScreenPadding() (x0, y0, x1, y1 float64) - ScreenScale() float64 ScreenSizeInFullscreen() (int, int) WindowPosition() (int, int) IsScreenTransparent() bool + CanHaveWindow() bool // TODO: Create a 'Widnow' interface. SetCursorMode(mode CursorMode) SetFullscreen(fullscreen bool) SetRunnableInBackground(runnableInBackground bool) - SetScreenScale(scale float64) - SetScreenSize(width, height int) SetVsyncEnabled(enabled bool) SetWindowDecorated(decorated bool) SetWindowIcon(iconImages []image.Image) SetWindowResizable(resizable bool) SetWindowTitle(title string) SetWindowPosition(x, y int) + SetWindowSize(width, height int) SetScreenTransparent(transparent bool) Input() Input diff --git a/internal/uidriver/glfw/input.go b/internal/uidriver/glfw/input.go index 37295685b..20b2bd634 100644 --- a/internal/uidriver/glfw/input.go +++ b/internal/uidriver/glfw/input.go @@ -63,7 +63,7 @@ func (i *Input) CursorPosition() (x, y int) { cx, cy = i.cursorX, i.cursorY return nil }) - return i.ui.adjustPosition(cx, cy) + return cx, cy } func (i *Input) GamepadIDs() []int { @@ -181,7 +181,7 @@ func (i *Input) TouchPosition(id int) (x, y int) { if !found { return 0, 0 } - return i.ui.adjustPosition(p.X, p.Y) + return p.X, p.Y } func (i *Input) RuneBuffer() []rune { @@ -284,7 +284,8 @@ func (i *Input) setWheel(xoff, yoff float64) { i.scrollY = yoff } -func (i *Input) update(window *glfw.Window) { +func (i *Input) update(window *glfw.Window, context driver.UIContext) { + var cx, cy float64 _ = i.ui.t.Call(func() error { i.onceCallback.Do(func() { window.SetCharModsCallback(func(w *glfw.Window, char rune, mods glfw.ModifierKey) { @@ -306,9 +307,17 @@ func (i *Input) update(window *glfw.Window) { for gb := range glfwMouseButtonToMouseButton { i.mouseButtonPressed[gb] = window.GetMouseButton(gb) == glfw.Press } - x, y := window.GetCursorPos() - i.cursorX = int(i.ui.toDeviceIndependentPixel(x) / i.ui.getScale()) - i.cursorY = int(i.ui.toDeviceIndependentPixel(y) / i.ui.getScale()) + cx, cy = window.GetCursorPos() + cx = i.ui.toDeviceIndependentPixel(cx) + cy = i.ui.toDeviceIndependentPixel(cy) + return nil + }) + + cx, cy = context.AdjustPosition(cx, cy) + + _ = i.ui.t.Call(func() error { + i.cursorX, i.cursorY = int(cx), int(cy) + for id := glfw.Joystick(0); id < glfw.Joystick(len(i.gamepads)); id++ { i.gamepads[id].valid = false if !id.Present() { diff --git a/internal/uidriver/glfw/ui.go b/internal/uidriver/glfw/ui.go index 722c98558..81715b7a7 100644 --- a/internal/uidriver/glfw/ui.go +++ b/internal/uidriver/glfw/ui.go @@ -23,7 +23,6 @@ import ( "context" "fmt" "image" - "math" "os" "runtime" "sync" @@ -38,12 +37,13 @@ import ( ) type UserInterface struct { - title string - window *glfw.Window - screenWidthInDP int - screenHeightInDP int - scale float64 - fullscreenScale float64 + title string + window *glfw.Window + + // windowWidth and windowHeight represents a window size. + // The unit is device-dependent pixels. + windowWidth int + windowHeight int running bool toChangeSize bool @@ -329,32 +329,6 @@ func (u *UserInterface) ScreenSizeInFullscreen() (int, int) { return w, h } -func (u *UserInterface) SetScreenSize(width, height int) { - if !u.isRunning() { - panic("glfw: SetScreenSize can't be called before the main loop starts") - } - u.setScreenSize(width, height, u.scale, u.isFullscreen(), u.vsync) -} - -func (u *UserInterface) SetScreenScale(scale float64) { - if !u.isRunning() { - panic("glfw: SetScreenScale can't be called before the main loop starts") - } - u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, scale, u.isFullscreen(), u.vsync) -} - -func (u *UserInterface) ScreenScale() float64 { - if !u.isRunning() { - return 0 - } - s := 0.0 - _ = u.t.Call(func() error { - s = u.scale - return nil - }) - return s -} - // isFullscreen must be called from the main thread. func (u *UserInterface) isFullscreen() bool { if !u.isRunning() { @@ -380,7 +354,22 @@ func (u *UserInterface) SetFullscreen(fullscreen bool) { u.setInitFullscreen(fullscreen) return } - u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, u.scale, fullscreen, u.vsync) + + var update bool + _ = u.t.Call(func() error { + update = u.isFullscreen() != fullscreen + return nil + }) + if !update { + return + } + + var w, h int + _ = u.t.Call(func() error { + w, h = u.windowWidth, u.windowHeight + return nil + }) + u.setWindowSize(w, h, fullscreen, u.vsync) } func (u *UserInterface) SetRunnableInBackground(runnableInBackground bool) { @@ -394,15 +383,20 @@ func (u *UserInterface) IsRunnableInBackground() bool { func (u *UserInterface) SetVsyncEnabled(enabled bool) { if !u.isRunning() { // In general, m is used for locking init* values. - // m is not used for updating vsync in setScreenSize so far, but + // m is not used for updating vsync in setWindowSize so far, but // it should be OK since any goroutines can't reach here when - // the game already starts and setScreenSize can be called. + // the game already starts and setWindowSize can be called. u.m.Lock() u.vsync = enabled u.m.Unlock() return } - u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, u.scale, u.isFullscreen(), enabled) + var w, h int + _ = u.t.Call(func() error { + w, h = u.windowWidth, u.windowHeight + return nil + }) + u.setWindowSize(w, h, u.isFullscreen(), enabled) } func (u *UserInterface) IsVsyncEnabled() bool { @@ -434,59 +428,6 @@ func (u *UserInterface) SetWindowIcon(iconImages []image.Image) { }) } -func (u *UserInterface) ScreenPadding() (x0, y0, x1, y1 float64) { - if !u.isRunning() { - return 0, 0, 0, 0 - } - if !u.IsFullscreen() { - w, _ := u.window.GetSize() - wf := u.toDeviceIndependentPixel(float64(w)) / u.getScale() - if u.screenWidthInDP == int(wf) { - return 0, 0, 0, 0 - } - // The window width can be bigger than the game screen width (#444). - ox := 0.0 - _ = u.t.Call(func() error { - ox = (wf*u.actualScreenScale() - float64(u.screenWidthInDP)*u.actualScreenScale()) / 2 - return nil - }) - return ox, 0, ox, 0 - } - - d := 0.0 - sx := 0.0 - sy := 0.0 - mx := 0.0 - my := 0.0 - _ = u.t.Call(func() error { - sx = float64(u.screenWidthInDP) * u.actualScreenScale() - sy = float64(u.screenHeightInDP) * u.actualScreenScale() - - v := u.currentMonitor().GetVideoMode() - d = u.deviceScaleFactor() - mx = u.toDeviceIndependentPixel(float64(v.Width)) * d - my = u.toDeviceIndependentPixel(float64(v.Height)) * d - return nil - }) - - ox := (mx - sx) / 2 - oy := (my - sy) / 2 - return ox, oy, (mx - sx) - ox, (my - sy) - oy -} - -func (u *UserInterface) adjustPosition(x, y int) (int, int) { - if !u.isRunning() { - return x, y - } - ox, oy, _, _ := u.ScreenPadding() - s := 0.0 - _ = u.t.Call(func() error { - s = u.actualScreenScale() - return nil - }) - return x - int(ox/s), y - int(oy/s) -} - func (u *UserInterface) CursorMode() driver.CursorMode { if !u.isRunning() { return u.getInitCursorMode() @@ -684,11 +625,8 @@ func (u *UserInterface) createWindow() error { if u.isFullscreen() { return } - - w := int(u.toDeviceIndependentPixel(float64(width)) / u.scale) - h := int(u.toDeviceIndependentPixel(float64(height)) / u.scale) - u.reqWidth = w - u.reqHeight = h + u.reqWidth = width + u.reqHeight = height }) return nil @@ -699,6 +637,7 @@ func (u *UserInterface) run(width, height int, scale float64, title string, cont m *glfw.Monitor mx, my int v *glfw.VidMode + ww, wh int ) if err := u.t.Call(func() error { @@ -757,6 +696,9 @@ func (u *UserInterface) run(width, height int, scale float64, title string, cont mx, my = m.GetPos() v = m.GetVideoMode() + ww = int(u.toDeviceDependentPixel(float64(width) * scale)) + wh = int(u.toDeviceDependentPixel(float64(height) * scale)) + return nil }); err != nil { return err @@ -764,7 +706,7 @@ func (u *UserInterface) run(width, height int, scale float64, title string, cont // The game is in window mode (not fullscreen mode) at the first state. // Don't refer u.initFullscreen here to avoid some GLFW problems. - u.setScreenSize(width, height, scale, false, u.vsync) + u.setWindowSize(ww, wh, false, u.vsync) _ = u.t.Call(func() error { // Get the window size before showing it. Showing the window might change the current monitor which @@ -798,31 +740,13 @@ func (u *UserInterface) run(width, height int, scale float64, title string, cont return u.loop(context) } -// getScale must be called from the main thread. -func (u *UserInterface) getScale() float64 { - if !u.isFullscreen() { - return u.scale - } - if u.fullscreenScale == 0 { - v := u.currentMonitor().GetVideoMode() - sw := u.toDeviceIndependentPixel(float64(v.Width)) / float64(u.screenWidthInDP) - sh := u.toDeviceIndependentPixel(float64(v.Height)) / float64(u.screenHeightInDP) - s := sw - if s > sh { - s = sh - } - u.fullscreenScale = s - } - return u.fullscreenScale -} - -// actualScreenScale must be called from the main thread. -func (u *UserInterface) actualScreenScale() float64 { - return u.getScale() * u.deviceScaleFactor() -} - func (u *UserInterface) updateSize(context driver.UIContext) { - u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, u.scale, u.isFullscreen(), u.vsync) + var w, h int + _ = u.t.Call(func() error { + w, h = u.windowWidth, u.windowHeight + return nil + }) + u.setWindowSize(w, h, u.isFullscreen(), u.vsync) sizeChanged := false _ = u.t.Call(func() error { @@ -835,12 +759,21 @@ func (u *UserInterface) updateSize(context driver.UIContext) { return nil }) if sizeChanged { - actualScale := 0.0 + var w, h float64 _ = u.t.Call(func() error { - actualScale = u.actualScreenScale() + var ww, wh int + if u.isFullscreen() { + v := u.currentMonitor().GetVideoMode() + ww = v.Width + wh = v.Height + } else { + ww, wh = u.windowWidth, u.windowHeight + } + w = u.toDeviceIndependentPixel(float64(ww)) + h = u.toDeviceIndependentPixel(float64(wh)) return nil }) - context.SetSize(u.screenWidthInDP, u.screenHeightInDP, actualScale) + context.Layout(w, h) } } @@ -855,7 +788,12 @@ func (u *UserInterface) update(context driver.UIContext) error { } if u.isInitFullscreen() { - u.setScreenSize(u.screenWidthInDP, u.screenHeightInDP, u.scale, true, u.vsync) + var w, h int + _ = u.t.Call(func() error { + w, h = u.window.GetSize() + return nil + }) + u.setWindowSize(w, h, true, u.vsync) u.setInitFullscreen(false) } @@ -866,7 +804,7 @@ func (u *UserInterface) update(context driver.UIContext) error { glfw.PollEvents() return nil }) - u.input.update(u.window) + u.input.update(u.window, context) _ = u.t.Call(func() error { defer hooks.ResumeAudio() @@ -895,7 +833,7 @@ func (u *UserInterface) update(context driver.UIContext) error { return nil }) if w != 0 || h != 0 { - u.setScreenSize(w, h, u.scale, u.isFullscreen(), u.vsync) + u.setWindowSize(w, h, u.isFullscreen(), u.vsync) } _ = u.t.Call(func() error { u.reqWidth = 0 @@ -958,11 +896,11 @@ func (u *UserInterface) swapBuffers() { } } -func (u *UserInterface) setScreenSize(width, height int, scale float64, fullscreen bool, vsync bool) { +func (u *UserInterface) setWindowSize(width, height int, fullscreen bool, vsync bool) { windowRecreated := false _ = u.t.Call(func() error { - if u.screenWidthInDP == width && u.screenHeightInDP == height && u.scale == scale && u.isFullscreen() == fullscreen && u.vsync == vsync && u.lastDeviceScaleFactor == u.deviceScaleFactor() { + if u.windowWidth == width && u.windowHeight == height && u.isFullscreen() == fullscreen && u.vsync == vsync && u.lastDeviceScaleFactor == u.deviceScaleFactor() { return nil } @@ -973,10 +911,6 @@ func (u *UserInterface) setScreenSize(width, height int, scale float64, fullscre height = 1 } - u.screenWidthInDP = width - u.screenHeightInDP = height - u.scale = scale - u.fullscreenScale = 0 u.vsync = vsync u.lastDeviceScaleFactor = u.deviceScaleFactor() @@ -1025,20 +959,34 @@ func (u *UserInterface) setScreenSize(width, height int, scale float64, fullscre // On Windows, giving a too small width doesn't call a callback (#165). // To prevent hanging up, return asap if the width is too small. - // 252 is an arbitrary number and I guess this is small enough. - minWindowWidth := 252 + // 126 is an arbitrary number and I guess this is small enough. + minWindowWidth := int(u.toDeviceDependentPixel(126)) if u.window.GetAttrib(glfw.Decorated) == glfw.False { minWindowWidth = 1 } - windowWidthInDP := width - s := scale * u.deviceScaleFactor() - if int(float64(width)*s) < minWindowWidth { - windowWidthInDP = int(math.Ceil(float64(minWindowWidth) / s)) + if width < minWindowWidth { + width = minWindowWidth } + if u.origPosX != invalidPos && u.origPosY != invalidPos { + x := u.origPosX + y := u.origPosY + u.window.SetPos(x, y) + // Dirty hack for macOS (#703). Rendering doesn't work correctly with one SetPos, but + // work with two or more SetPos. + if runtime.GOOS == "darwin" { + u.window.SetPos(x+1, y) + u.window.SetPos(x, y) + } + u.origPosX = invalidPos + u.origPosY = invalidPos + } + + // Set the window size after the position. The order matters. + // In the opposite order, the window size might not be correct when going back from fullscreen with multi monitors. oldW, oldH := u.window.GetSize() - newW := int(u.toDeviceDependentPixel(float64(windowWidthInDP) * u.getScale())) - newH := int(u.toDeviceDependentPixel(float64(u.screenHeightInDP) * u.getScale())) + newW := width + newH := height if oldW != newW || oldH != newH { ch := make(chan struct{}) u.window.SetFramebufferSizeCallback(func(_ *glfw.Window, _, _ int) { @@ -1057,24 +1005,14 @@ func (u *UserInterface) setScreenSize(width, height int, scale float64, fullscre } } - if u.origPosX != invalidPos && u.origPosY != invalidPos { - x := u.origPosX - y := u.origPosY - u.window.SetPos(x, y) - // Dirty hack for macOS (#703). Rendering doesn't work correctly with one SetPos, but - // work with two or more SetPos. - if runtime.GOOS == "darwin" { - u.window.SetPos(x+1, y) - u.window.SetPos(x, y) - } - u.origPosX = invalidPos - u.origPosY = invalidPos - } - // Window title might be lost on macOS after coming back from fullscreen. u.window.SetTitle(u.title) } + // As width might be updated, update windowWidth/Height here. + u.windowWidth = width + u.windowHeight = height + if u.graphics.IsGL() { // SwapInterval is affected by the current monitor of the window. // This needs to be called at least after SetMonitor. @@ -1162,6 +1100,19 @@ func (u *UserInterface) IsScreenTransparent() bool { return val } +func (u *UserInterface) SetWindowSize(width, height int) { + if !u.isRunning() { + panic("glfw: SetWindowSize can't be called before the main loop starts") + } + w := int(u.toDeviceDependentPixel(float64(width))) + h := int(u.toDeviceDependentPixel(float64(height))) + u.setWindowSize(w, h, u.isFullscreen(), u.vsync) +} + +func (u *UserInterface) CanHaveWindow() bool { + return true +} + func (u *UserInterface) Input() driver.Input { return &u.input } diff --git a/internal/uidriver/js/input.go b/internal/uidriver/js/input.go index f947c3d13..e9afab6d4 100644 --- a/internal/uidriver/js/input.go +++ b/internal/uidriver/js/input.go @@ -51,7 +51,8 @@ type Input struct { } func (i *Input) CursorPosition() (x, y int) { - return i.ui.adjustPosition(i.cursorX, i.cursorY) + xf, yf := i.ui.context.AdjustPosition(float64(i.cursorX), float64(i.cursorY)) + return int(xf), int(yf) } func (i *Input) GamepadIDs() []int { @@ -110,7 +111,8 @@ func (i *Input) TouchIDs() []int { func (i *Input) TouchPosition(id int) (x, y int) { for tid, pos := range i.touches { if id == tid { - return i.ui.adjustPosition(pos.X, pos.Y) + x, y := i.ui.context.AdjustPosition(float64(pos.X), float64(pos.Y)) + return int(x), int(y) } } return 0, 0 diff --git a/internal/uidriver/js/ui.go b/internal/uidriver/js/ui.go index d8a8b6be1..d483915c7 100644 --- a/internal/uidriver/js/ui.go +++ b/internal/uidriver/js/ui.go @@ -20,7 +20,6 @@ import ( "image" "log" "runtime" - "strconv" "syscall/js" "time" @@ -30,9 +29,6 @@ import ( ) type UserInterface struct { - width int - height int - scale float64 runnableInBackground bool vsync bool running bool @@ -40,14 +36,10 @@ type UserInterface struct { sizeChanged bool contextLost bool - lastActualScale float64 + lastDeviceScaleFactor float64 context driver.UIContext input Input - - // pseudoScale is a value to store 'scale'. This doesn't affect actual rendering. - // This is for backward compatibility. - pseudoScale float64 } var theUI = &UserInterface{ @@ -75,18 +67,6 @@ func (u *UserInterface) ScreenSizeInFullscreen() (int, int) { return window.Get("innerWidth").Int(), window.Get("innerHeight").Int() } -func (u *UserInterface) SetScreenSize(width, height int) { - u.setScreenSize(width, height) -} - -func (u *UserInterface) SetScreenScale(scale float64) { - u.pseudoScale = scale -} - -func (u *UserInterface) ScreenScale() float64 { - return u.pseudoScale -} - func (u *UserInterface) SetFullscreen(fullscreen bool) { // Do nothing } @@ -111,24 +91,11 @@ func (u *UserInterface) IsVsyncEnabled() bool { return u.vsync } -func (u *UserInterface) ScreenPadding() (x0, y0, x1, y1 float64) { - return 0, 0, 0, 0 -} - -func (u *UserInterface) adjustPosition(x, y int) (int, int) { - rect := canvas.Call("getBoundingClientRect") - x -= rect.Get("left").Int() - y -= rect.Get("top").Int() - s := u.scale - return int(float64(x) / s), int(float64(y) / s) -} - func (u *UserInterface) CursorMode() driver.CursorMode { if canvas.Get("style").Get("cursor").String() != "none" { return driver.CursorModeVisible - } else { - return driver.CursorModeHidden } + return driver.CursorModeHidden } func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { @@ -150,6 +117,7 @@ func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { } func (u *UserInterface) SetWindowTitle(title string) { + // TODO: As the page should be in an iframe, this might be useless. document.Set("title", title) } @@ -171,34 +139,31 @@ func (u *UserInterface) IsWindowResizable() bool { func (u *UserInterface) SetWindowResizable(decorated bool) { // Do nothing - if u.running { - panic("js: SetWindowResizable can't be called after the main loop starts") - } +} + +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. + u.sizeChanged = true } func (u *UserInterface) DeviceScaleFactor() float64 { return devicescale.GetAt(0, 0) } -func (u *UserInterface) actualScreenScale() float64 { - // CSS imageRendering property seems useful to enlarge the screen, - // but doesn't work in some cases (#306): - // * Chrome just after restoring the lost context - // * Safari - // Let's use the devicePixelRatio as it is here. - return u.scale * devicescale.GetAt(0, 0) -} - func (u *UserInterface) updateSize() { - a := u.actualScreenScale() - if u.lastActualScale != a { + a := u.DeviceScaleFactor() + if u.lastDeviceScaleFactor != a { u.updateScreenSize() } - u.lastActualScale = a + u.lastDeviceScaleFactor = a if u.sizeChanged { u.sizeChanged = false - u.context.SetSize(u.width, u.height, a) + body := document.Get("body") + bw := body.Get("clientWidth").Float() + bh := body.Get("clientHeight").Float() + u.context.Layout(bw, bh) } } @@ -306,6 +271,7 @@ func init() { canvas = document.Call("createElement", "canvas") canvas.Set("width", 16) canvas.Set("height", 16) + document.Get("body").Call("appendChild", canvas) htmlStyle := document.Get("documentElement").Get("style") @@ -315,13 +281,9 @@ func init() { bodyStyle := document.Get("body").Get("style") bodyStyle.Set("backgroundColor", "#000") - bodyStyle.Set("position", "relative") bodyStyle.Set("height", "100%") bodyStyle.Set("margin", "0") bodyStyle.Set("padding", "0") - bodyStyle.Set("display", "flex") - bodyStyle.Set("alignItems", "center") - bodyStyle.Set("justifyContent", "center") // TODO: This is OK as long as the game is in an independent iframe. // What if the canvas is embedded in a HTML directly? @@ -331,7 +293,10 @@ func init() { })) canvasStyle := canvas.Get("style") - canvasStyle.Set("position", "absolute") + canvasStyle.Set("width", "100%") + canvasStyle.Set("height", "100%") + canvasStyle.Set("margin", "0") + canvasStyle.Set("padding", "0") // Make the canvas focusable. canvas.Call("setAttribute", "tabindex", 1) @@ -438,11 +403,7 @@ func init() { } func (u *UserInterface) Run(width, height int, scale float64, title string, context driver.UIContext, graphics driver.Graphics) error { - // scale is ignored. - document.Set("title", title) - u.setScreenSize(width, height) - u.pseudoScale = scale canvas.Call("focus") u.running = true ch := u.loop(context) @@ -467,37 +428,12 @@ func (u *UserInterface) RunWithoutMainLoop(width, height int, scale float64, tit panic("js: RunWithoutMainLoop is not implemented") } -func (u *UserInterface) setScreenSize(width, height int) bool { - if u.width == width && u.height == height { - return false - } - u.width = width - u.height = height - u.updateScreenSize() - return true -} - func (u *UserInterface) updateScreenSize() { body := document.Get("body") - bw := body.Get("clientWidth").Float() - bh := body.Get("clientHeight").Float() - sw := bw / float64(u.width) - sh := bh / float64(u.height) - if sw > sh { - u.scale = sh - } else { - u.scale = sw - } - - canvas.Set("width", int(float64(u.width)*u.actualScreenScale())) - canvas.Set("height", int(float64(u.height)*u.actualScreenScale())) - canvasStyle := canvas.Get("style") - - cssWidth := int(float64(u.width) * u.scale) - cssHeight := int(float64(u.height) * u.scale) - canvasStyle.Set("width", strconv.Itoa(cssWidth)+"px") - canvasStyle.Set("height", strconv.Itoa(cssHeight)+"px") - + bw := int(body.Get("clientWidth").Float() * u.DeviceScaleFactor()) + bh := int(body.Get("clientHeight").Float() * u.DeviceScaleFactor()) + canvas.Set("width", bw) + canvas.Set("height", bh) u.sizeChanged = true } @@ -530,6 +466,10 @@ func (u *UserInterface) IsScreenTransparent() bool { return bodyStyle.Get("backgroundColor").String() == "transparent" } +func (u *UserInterface) CanHaveWindow() bool { + return false +} + func (u *UserInterface) Input() driver.Input { return &u.input } diff --git a/internal/uidriver/mobile/ui.go b/internal/uidriver/mobile/ui.go index bf07467a1..e0ae17a1a 100644 --- a/internal/uidriver/mobile/ui.go +++ b/internal/uidriver/mobile/ui.go @@ -139,7 +139,7 @@ func (u *UserInterface) appMain(a app.App) { glctx = nil } case size.Event: - u.setGBuildImpl(e.WidthPx, e.HeightPx) + u.setGBuild(e.WidthPx, e.HeightPx) case paint.Event: if glctx == nil || e.External { continue @@ -236,30 +236,37 @@ func (u *UserInterface) run(width, height int, scale float64, title string, cont } func (u *UserInterface) updateSize(context driver.UIContext) { - width, height := 0, 0 - actualScale := 0.0 + var width, height float64 u.m.Lock() sizeChanged := u.sizeChanged if sizeChanged { - width = u.width - height = u.height - actualScale = u.scaleImpl() * deviceScale() + if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 { + s := u.scaleImpl() + width = float64(u.width) * s + height = float64(u.height) * s + } else { + // gomobile build + d := deviceScale() + width = float64(u.gbuildWidthPx) / d + height = float64(u.gbuildHeightPx) / d + } } u.sizeChanged = false u.m.Unlock() if sizeChanged { - // Sizing also calls GL functions - context.SetSize(width, height, actualScale) - } -} + // Dirty hack to set the offscreen size for gomobile-bind. + // TODO: Remove this. The layouting logic must be in the package ebiten, not here. + if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 { + context.(interface { + SetScreenSize(width, height int) + }).SetScreenSize(u.width, u.height) + } -func (u *UserInterface) ActualScale() float64 { - u.m.Lock() - s := u.scaleImpl() * deviceScale() - u.m.Unlock() - return s + // Sizing also calls GL functions + context.Layout(width, height) + } } func (u *UserInterface) scaleImpl() float64 { @@ -295,47 +302,26 @@ func (u *UserInterface) update(context driver.UIContext) error { return nil } -func (u *UserInterface) ScreenSize() (int, int) { - u.m.Lock() - w, h := u.width, u.height - u.m.Unlock() - return w, h -} - func (u *UserInterface) ScreenSizeInFullscreen() (int, int) { // TODO: This function should return gbuildWidthPx, gbuildHeightPx, // but these values are not initialized until the main loop starts. return 0, 0 } -func (u *UserInterface) SetScreenSize(width, height int) { +func (u *UserInterface) SetScreenSizeAndScale(width, height int, scale float64) { + // Called from ebitenmobileview. u.m.Lock() - if u.width != width || u.height != height { + if u.width != width || u.height != height || u.scale != scale { u.width = width u.height = height + u.scale = scale u.updateGBuildScaleIfNeeded() u.sizeChanged = true } u.m.Unlock() } -func (u *UserInterface) SetScreenScale(scale float64) { - u.m.Lock() - if u.scale != scale { - u.scale = scale - u.sizeChanged = true - } - u.m.Unlock() -} - -func (u *UserInterface) ScreenScale() float64 { - u.m.RLock() - s := u.scale - u.m.RUnlock() - return s -} - -func (u *UserInterface) setGBuildImpl(widthPx, heightPx int) { +func (u *UserInterface) setGBuild(widthPx, heightPx int) { u.m.Lock() u.gbuildWidthPx = widthPx u.gbuildHeightPx = heightPx @@ -348,6 +334,7 @@ func (u *UserInterface) updateGBuildScaleIfNeeded() { if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 { return } + w, h := u.width, u.height scaleX := float64(u.gbuildWidthPx) / float64(w) scaleY := float64(u.gbuildHeightPx) / float64(h) @@ -359,14 +346,8 @@ func (u *UserInterface) updateGBuildScaleIfNeeded() { u.sizeChanged = true } -func (u *UserInterface) ScreenPadding() (x0, y0, x1, y1 float64) { - u.m.Lock() - x0, y0, x1, y1 = u.screenPaddingImpl() - u.m.Unlock() - return -} - func (u *UserInterface) screenPaddingImpl() (x0, y0, x1, y1 float64) { + // TODO: Replace this with UIContext's Layout. if u.gbuildScale == 0 { return 0, 0, 0, 0 } @@ -377,6 +358,7 @@ func (u *UserInterface) screenPaddingImpl() (x0, y0, x1, y1 float64) { } func (u *UserInterface) adjustPosition(x, y int) (int, int) { + // TODO: Replace this with UIContext's AdjustPosition. ox, oy, _, _ := u.screenPaddingImpl() s := u.scaleImpl() as := s * deviceScale() @@ -459,6 +441,14 @@ func (u *UserInterface) IsScreenTransparent() bool { return false } +func (u *UserInterface) SetWindowSize(width, height int) { + // Do nothing +} + +func (u *UserInterface) CanHaveWindow() bool { + return false +} + func (u *UserInterface) Input() driver.Input { return &u.input } diff --git a/mobile/ebitenmobileview/funcs.go b/mobile/ebitenmobileview/funcs.go index 32ca80dfd..4e54be185 100644 --- a/mobile/ebitenmobileview/funcs.go +++ b/mobile/ebitenmobileview/funcs.go @@ -60,8 +60,7 @@ func layout(viewWidth, viewHeight int, viewRectSetter ViewRectSetter) { y := (viewHeight - height) / 2 if theState.isRunning() { - mobile.Get().SetScreenSize(w, h) - mobile.Get().SetScreenScale(scale) + mobile.Get().SetScreenSizeAndScale(w, h, scale) } else { // The last argument 'title' is not used on mobile platforms, so just pass an empty string. theState.errorCh = ebiten.RunWithoutMainLoop(theState.game.Update, w, h, scale, "") diff --git a/run.go b/run.go index a432903d9..505f3a19b 100644 --- a/run.go +++ b/run.go @@ -151,8 +151,8 @@ func IsRunningSlowly() bool { func Run(f func(*Image) error, width, height int, scale float64, title string) error { f = (&imageDumper{f: f}).update - c := newUIContext(f) - if err := uiDriver().Run(width, height, scale, title, c, graphicsDriver()); err != nil { + theUIContext = newUIContext(f, width, height, scale) + if err := uiDriver().Run(width, height, scale, title, theUIContext, graphicsDriver()); err != nil { if err == driver.RegularTermination { return nil } @@ -170,8 +170,8 @@ func Run(f func(*Image) error, width, height int, scale float64, title string) e func RunWithoutMainLoop(f func(*Image) error, width, height int, scale float64, title string) <-chan error { f = (&imageDumper{f: f}).update - c := newUIContext(f) - return uiDriver().RunWithoutMainLoop(width, height, scale, title, c, graphicsDriver()) + theUIContext = newUIContext(f, width, height, scale) + return uiDriver().RunWithoutMainLoop(width, height, scale, title, theUIContext, graphicsDriver()) } // ScreenSizeInFullscreen returns the size in device-independent pixels when the game is fullscreen. @@ -221,7 +221,12 @@ func SetScreenSize(width, height int) { if width <= 0 || height <= 0 { panic("ebiten: width and height must be positive") } - uiDriver().SetScreenSize(width, height) + if theUIContext == nil { + panic("ebiten: SetScreenSize can't be called before the main loop starts") + } + theUIContext.SetScreenSize(width, height) + s := theUIContext.getScaleForWindow() + uiDriver().SetWindowSize(int(float64(width)*s), int(float64(height)*s)) } // SetScreenScale changes the scale of the screen on desktops. @@ -243,11 +248,18 @@ func SetScreenSize(width, height int) { // SetScreenScale panics if scale is not a positive number. // // SetScreenScale is concurrent-safe. +// +// TODO: Deprecate this function. func SetScreenScale(scale float64) { if scale <= 0 { panic("ebiten: scale must be positive") } - uiDriver().SetScreenScale(scale) + if theUIContext == nil { + panic("ebiten: SetScreenScale can't be called before the main loop starts") + } + theUIContext.setScaleForWindow(scale) + w, h := theUIContext.size() + uiDriver().SetWindowSize(int(float64(w)*scale), int(float64(h)*scale)) } // ScreenScale returns the current screen scale. @@ -257,8 +269,13 @@ func SetScreenScale(scale float64) { // If Run is not called, this returns 0. // // ScreenScale is concurrent-safe. +// +// TODO: Deprecate this function. func ScreenScale() float64 { - return uiDriver().ScreenScale() + if theUIContext == nil { + panic("ebiten: ScreenScale can't be called before the main loop starts") + } + return theUIContext.getScaleForWindow() } // CursorMode returns the current cursor mode. diff --git a/uicontext.go b/uicontext.go index 7d90724f6..3becd6001 100644 --- a/uicontext.go +++ b/uicontext.go @@ -17,6 +17,7 @@ package ebiten import ( "fmt" "math" + "sync" "github.com/hajimehoshi/ebiten/internal/buffered" "github.com/hajimehoshi/ebiten/internal/clock" @@ -31,45 +32,99 @@ func init() { graphicscommand.SetGraphicsDriver(graphicsDriver()) } -func newUIContext(f func(*Image) error) *uiContext { +func newUIContext(f func(*Image) error, width, height int, scaleForWindow float64) *uiContext { return &uiContext{ - f: f, + f: f, + screenWidth: width, + screenHeight: height, + scaleForWindow: scaleForWindow, } } type uiContext struct { - f func(*Image) error - offscreen *Image - screen *Image - screenWidth int - screenHeight int - screenScale float64 - offsetX float64 - offsetY float64 + f func(*Image) error + offscreen *Image + screen *Image + screenWidth int + screenHeight int + screenScale float64 + scaleForWindow float64 + offsetX float64 + offsetY float64 + + reqWidth int + reqHeight int + m sync.Mutex } -func (c *uiContext) SetSize(screenWidth, screenHeight int, screenScale float64) { - c.screenScale = screenScale +var theUIContext *uiContext - if c.screen != nil { - _ = c.screen.Dispose() +func (c *uiContext) resolveSize() (int, int) { + c.m.Lock() + defer c.m.Unlock() + if c.reqWidth != 0 || c.reqHeight != 0 { + c.screenWidth = c.reqWidth + c.screenHeight = c.reqHeight + c.reqWidth = 0 + c.reqHeight = 0 } if c.offscreen != nil { - _ = c.offscreen.Dispose() + if w, h := c.offscreen.Size(); w != c.screenWidth || h != c.screenHeight { + // The offscreen might still be used somewhere. Do not Dispose it. Finalizer will do that. + c.offscreen = nil + } + } + if c.offscreen == nil { + c.offscreen = newImage(c.screenWidth, c.screenHeight, FilterDefault, true) + } + return c.screenWidth, c.screenHeight +} + +func (c *uiContext) size() (int, int) { + return c.resolveSize() +} + +func (c *uiContext) setScaleForWindow(scale float64) { + c.scaleForWindow = scale +} + +func (c *uiContext) getScaleForWindow() float64 { + return c.scaleForWindow +} + +func (c *uiContext) SetScreenSize(width, height int) { + c.m.Lock() + defer c.m.Unlock() + + // TODO: Use the interface Game's Layout and then update screenWidth and screenHeight, then this function + // is no longer needed. + c.reqWidth = width + c.reqHeight = height +} + +func (c *uiContext) Layout(outsideWidth, outsideHeight float64) { + if c.screen != nil { + _ = c.screen.Dispose() + c.screen = nil } - c.offscreen = newImage(screenWidth, screenHeight, FilterDefault, true) + // TODO: This is duplicated with mobile/ebitenmobileview/funcs.go. Refactor this. + d := uiDriver().DeviceScaleFactor() + c.screen = newScreenFramebufferImage(int(outsideWidth*d), int(outsideHeight*d)) - // Round up the screensize not to cause glitches e.g. on Xperia (#622) - w := int(math.Ceil(float64(screenWidth) * screenScale)) - h := int(math.Ceil(float64(screenHeight) * screenScale)) - px0, py0, px1, py1 := uiDriver().ScreenPadding() - c.screen = newScreenFramebufferImage(w+int(math.Ceil(px0+px1)), h+int(math.Ceil(py0+py1))) - c.screenWidth = w - c.screenHeight = h + sw, sh := c.resolveSize() + scaleX := float64(outsideWidth) / float64(sw) * d + scaleY := float64(outsideHeight) / float64(sh) * d + c.screenScale = math.Min(scaleX, scaleY) + if uiDriver().CanHaveWindow() && !uiDriver().IsFullscreen() { + // When the UI driver cannot have a window, scaleForWindow is updated only via setScaleFowWindow. + c.scaleForWindow = c.screenScale / d + } - c.offsetX = px0 - c.offsetY = py0 + width := float64(sw) * c.screenScale + height := float64(sh) * c.screenScale + c.offsetX = (float64(outsideWidth)*d - width) / 2 + c.offsetY = (float64(outsideHeight)*d - height) / 2 } func (c *uiContext) Update(afterFrameUpdate func()) error { @@ -82,17 +137,17 @@ func (c *uiContext) Update(afterFrameUpdate func()) error { } for i := 0; i < updateCount; i++ { + // Mipmap images should be disposed by Clear. c.offscreen.Clear() - // Mipmap images should be disposed by fill. setDrawingSkipped(i < updateCount-1) + if err := hooks.RunBeforeUpdateHooks(); err != nil { return err } if err := c.f(c.offscreen); err != nil { return err } - uiDriver().Input().ResetForFrame() afterFrameUpdate() } @@ -107,7 +162,7 @@ func (c *uiContext) Update(afterFrameUpdate func()) error { // c.screen is special: its Y axis is down to up, // and the origin point is lower left. op.GeoM.Scale(c.screenScale, -c.screenScale) - op.GeoM.Translate(0, float64(c.screenHeight)) + op.GeoM.Translate(0, float64(c.screenHeight)*c.screenScale) case driver.VUpward: op.GeoM.Scale(c.screenScale, c.screenScale) default: @@ -131,3 +186,8 @@ func (c *uiContext) Update(afterFrameUpdate func()) error { } return nil } + +func (c *uiContext) AdjustPosition(x, y float64) (float64, float64) { + d := uiDriver().DeviceScaleFactor() + return (x*d - c.offsetX) / c.screenScale, (y*d - c.offsetY) / c.screenScale +}