driver: Add UI.SetWindowSize and UIContext.Layout

This is a preparation to introduce RunGame function.

Updates # 943 (Fix this line before committing)
This commit is contained in:
Hajime Hoshi 2019-12-10 02:37:10 +09:00
parent 4341c5ff1a
commit bda11b0e17
10 changed files with 309 additions and 341 deletions

View File

@ -165,6 +165,7 @@ func update(screen *ebiten.Image) error {
} }
ebiten.SetScreenSize(screenWidth, screenHeight) ebiten.SetScreenSize(screenWidth, screenHeight)
// TODO: Add a flag for compatibility mode and call SetScreenScale only when the flag is on.
ebiten.SetScreenScale(screenScale) ebiten.SetScreenScale(screenScale)
ebiten.SetFullscreen(fullscreen) ebiten.SetFullscreen(fullscreen)
ebiten.SetRunnableInBackground(runnableInBackground) ebiten.SetRunnableInBackground(runnableInBackground)

View File

@ -20,8 +20,9 @@ import (
) )
type UIContext interface { type UIContext interface {
SetSize(width, height int, scale float64)
Update(afterFrameUpdate func()) error Update(afterFrameUpdate func()) error
Layout(outsideWidth, outsideHeight float64)
AdjustPosition(x, y float64) (float64, float64)
} }
// RegularTermination represents a regular termination. // RegularTermination represents a regular termination.
@ -40,23 +41,21 @@ type UI interface {
IsVsyncEnabled() bool IsVsyncEnabled() bool
IsWindowDecorated() bool IsWindowDecorated() bool
IsWindowResizable() bool IsWindowResizable() bool
ScreenPadding() (x0, y0, x1, y1 float64)
ScreenScale() float64
ScreenSizeInFullscreen() (int, int) ScreenSizeInFullscreen() (int, int)
WindowPosition() (int, int) WindowPosition() (int, int)
IsScreenTransparent() bool IsScreenTransparent() bool
CanHaveWindow() bool // TODO: Create a 'Widnow' interface.
SetCursorMode(mode CursorMode) SetCursorMode(mode CursorMode)
SetFullscreen(fullscreen bool) SetFullscreen(fullscreen bool)
SetRunnableInBackground(runnableInBackground bool) SetRunnableInBackground(runnableInBackground bool)
SetScreenScale(scale float64)
SetScreenSize(width, height int)
SetVsyncEnabled(enabled bool) SetVsyncEnabled(enabled bool)
SetWindowDecorated(decorated bool) SetWindowDecorated(decorated bool)
SetWindowIcon(iconImages []image.Image) SetWindowIcon(iconImages []image.Image)
SetWindowResizable(resizable bool) SetWindowResizable(resizable bool)
SetWindowTitle(title string) SetWindowTitle(title string)
SetWindowPosition(x, y int) SetWindowPosition(x, y int)
SetWindowSize(width, height int)
SetScreenTransparent(transparent bool) SetScreenTransparent(transparent bool)
Input() Input Input() Input

View File

@ -63,7 +63,7 @@ func (i *Input) CursorPosition() (x, y int) {
cx, cy = i.cursorX, i.cursorY cx, cy = i.cursorX, i.cursorY
return nil return nil
}) })
return i.ui.adjustPosition(cx, cy) return cx, cy
} }
func (i *Input) GamepadIDs() []int { func (i *Input) GamepadIDs() []int {
@ -181,7 +181,7 @@ func (i *Input) TouchPosition(id int) (x, y int) {
if !found { if !found {
return 0, 0 return 0, 0
} }
return i.ui.adjustPosition(p.X, p.Y) return p.X, p.Y
} }
func (i *Input) RuneBuffer() []rune { func (i *Input) RuneBuffer() []rune {
@ -284,7 +284,8 @@ func (i *Input) setWheel(xoff, yoff float64) {
i.scrollY = yoff 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.ui.t.Call(func() error {
i.onceCallback.Do(func() { i.onceCallback.Do(func() {
window.SetCharModsCallback(func(w *glfw.Window, char rune, mods glfw.ModifierKey) { 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 { for gb := range glfwMouseButtonToMouseButton {
i.mouseButtonPressed[gb] = window.GetMouseButton(gb) == glfw.Press i.mouseButtonPressed[gb] = window.GetMouseButton(gb) == glfw.Press
} }
x, y := window.GetCursorPos() cx, cy = window.GetCursorPos()
i.cursorX = int(i.ui.toDeviceIndependentPixel(x) / i.ui.getScale()) cx = i.ui.toDeviceIndependentPixel(cx)
i.cursorY = int(i.ui.toDeviceIndependentPixel(y) / i.ui.getScale()) 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++ { for id := glfw.Joystick(0); id < glfw.Joystick(len(i.gamepads)); id++ {
i.gamepads[id].valid = false i.gamepads[id].valid = false
if !id.Present() { if !id.Present() {

View File

@ -23,7 +23,6 @@ import (
"context" "context"
"fmt" "fmt"
"image" "image"
"math"
"os" "os"
"runtime" "runtime"
"sync" "sync"
@ -40,10 +39,11 @@ import (
type UserInterface struct { type UserInterface struct {
title string title string
window *glfw.Window window *glfw.Window
screenWidthInDP int
screenHeightInDP int // windowWidth and windowHeight represents a window size.
scale float64 // The unit is device-dependent pixels.
fullscreenScale float64 windowWidth int
windowHeight int
running bool running bool
toChangeSize bool toChangeSize bool
@ -329,32 +329,6 @@ func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
return w, h 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. // isFullscreen must be called from the main thread.
func (u *UserInterface) isFullscreen() bool { func (u *UserInterface) isFullscreen() bool {
if !u.isRunning() { if !u.isRunning() {
@ -380,7 +354,22 @@ func (u *UserInterface) SetFullscreen(fullscreen bool) {
u.setInitFullscreen(fullscreen) u.setInitFullscreen(fullscreen)
return 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) { func (u *UserInterface) SetRunnableInBackground(runnableInBackground bool) {
@ -394,15 +383,20 @@ func (u *UserInterface) IsRunnableInBackground() bool {
func (u *UserInterface) SetVsyncEnabled(enabled bool) { func (u *UserInterface) SetVsyncEnabled(enabled bool) {
if !u.isRunning() { if !u.isRunning() {
// In general, m is used for locking init* values. // 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 // 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.m.Lock()
u.vsync = enabled u.vsync = enabled
u.m.Unlock() u.m.Unlock()
return 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 { 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 { func (u *UserInterface) CursorMode() driver.CursorMode {
if !u.isRunning() { if !u.isRunning() {
return u.getInitCursorMode() return u.getInitCursorMode()
@ -684,11 +625,8 @@ func (u *UserInterface) createWindow() error {
if u.isFullscreen() { if u.isFullscreen() {
return return
} }
u.reqWidth = width
w := int(u.toDeviceIndependentPixel(float64(width)) / u.scale) u.reqHeight = height
h := int(u.toDeviceIndependentPixel(float64(height)) / u.scale)
u.reqWidth = w
u.reqHeight = h
}) })
return nil return nil
@ -699,6 +637,7 @@ func (u *UserInterface) run(width, height int, scale float64, title string, cont
m *glfw.Monitor m *glfw.Monitor
mx, my int mx, my int
v *glfw.VidMode v *glfw.VidMode
ww, wh int
) )
if err := u.t.Call(func() error { 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() mx, my = m.GetPos()
v = m.GetVideoMode() v = m.GetVideoMode()
ww = int(u.toDeviceDependentPixel(float64(width) * scale))
wh = int(u.toDeviceDependentPixel(float64(height) * scale))
return nil return nil
}); err != nil { }); err != nil {
return err 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. // The game is in window mode (not fullscreen mode) at the first state.
// Don't refer u.initFullscreen here to avoid some GLFW problems. // 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 { _ = u.t.Call(func() error {
// Get the window size before showing it. Showing the window might change the current monitor which // 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) 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) { 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 sizeChanged := false
_ = u.t.Call(func() error { _ = u.t.Call(func() error {
@ -835,12 +759,21 @@ func (u *UserInterface) updateSize(context driver.UIContext) {
return nil return nil
}) })
if sizeChanged { if sizeChanged {
actualScale := 0.0 var w, h float64
_ = u.t.Call(func() error { _ = 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 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() { 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) u.setInitFullscreen(false)
} }
@ -866,7 +804,7 @@ func (u *UserInterface) update(context driver.UIContext) error {
glfw.PollEvents() glfw.PollEvents()
return nil return nil
}) })
u.input.update(u.window) u.input.update(u.window, context)
_ = u.t.Call(func() error { _ = u.t.Call(func() error {
defer hooks.ResumeAudio() defer hooks.ResumeAudio()
@ -895,7 +833,7 @@ func (u *UserInterface) update(context driver.UIContext) error {
return nil return nil
}) })
if w != 0 || h != 0 { 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.t.Call(func() error {
u.reqWidth = 0 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 windowRecreated := false
_ = u.t.Call(func() error { _ = 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 return nil
} }
@ -973,10 +911,6 @@ func (u *UserInterface) setScreenSize(width, height int, scale float64, fullscre
height = 1 height = 1
} }
u.screenWidthInDP = width
u.screenHeightInDP = height
u.scale = scale
u.fullscreenScale = 0
u.vsync = vsync u.vsync = vsync
u.lastDeviceScaleFactor = u.deviceScaleFactor() 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). // 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. // To prevent hanging up, return asap if the width is too small.
// 252 is an arbitrary number and I guess this is small enough. // 126 is an arbitrary number and I guess this is small enough.
minWindowWidth := 252 minWindowWidth := int(u.toDeviceDependentPixel(126))
if u.window.GetAttrib(glfw.Decorated) == glfw.False { if u.window.GetAttrib(glfw.Decorated) == glfw.False {
minWindowWidth = 1 minWindowWidth = 1
} }
windowWidthInDP := width if width < minWindowWidth {
s := scale * u.deviceScaleFactor() width = minWindowWidth
if int(float64(width)*s) < minWindowWidth {
windowWidthInDP = int(math.Ceil(float64(minWindowWidth) / s))
} }
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() oldW, oldH := u.window.GetSize()
newW := int(u.toDeviceDependentPixel(float64(windowWidthInDP) * u.getScale())) newW := width
newH := int(u.toDeviceDependentPixel(float64(u.screenHeightInDP) * u.getScale())) newH := height
if oldW != newW || oldH != newH { if oldW != newW || oldH != newH {
ch := make(chan struct{}) ch := make(chan struct{})
u.window.SetFramebufferSizeCallback(func(_ *glfw.Window, _, _ int) { 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. // Window title might be lost on macOS after coming back from fullscreen.
u.window.SetTitle(u.title) u.window.SetTitle(u.title)
} }
// As width might be updated, update windowWidth/Height here.
u.windowWidth = width
u.windowHeight = height
if u.graphics.IsGL() { if u.graphics.IsGL() {
// SwapInterval is affected by the current monitor of the window. // SwapInterval is affected by the current monitor of the window.
// This needs to be called at least after SetMonitor. // This needs to be called at least after SetMonitor.
@ -1162,6 +1100,19 @@ func (u *UserInterface) IsScreenTransparent() bool {
return val 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 { func (u *UserInterface) Input() driver.Input {
return &u.input return &u.input
} }

View File

@ -51,7 +51,8 @@ type Input struct {
} }
func (i *Input) CursorPosition() (x, y int) { 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 { func (i *Input) GamepadIDs() []int {
@ -110,7 +111,8 @@ func (i *Input) TouchIDs() []int {
func (i *Input) TouchPosition(id int) (x, y int) { func (i *Input) TouchPosition(id int) (x, y int) {
for tid, pos := range i.touches { for tid, pos := range i.touches {
if id == tid { 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 return 0, 0

View File

@ -20,7 +20,6 @@ import (
"image" "image"
"log" "log"
"runtime" "runtime"
"strconv"
"syscall/js" "syscall/js"
"time" "time"
@ -30,9 +29,6 @@ import (
) )
type UserInterface struct { type UserInterface struct {
width int
height int
scale float64
runnableInBackground bool runnableInBackground bool
vsync bool vsync bool
running bool running bool
@ -40,14 +36,10 @@ type UserInterface struct {
sizeChanged bool sizeChanged bool
contextLost bool contextLost bool
lastActualScale float64 lastDeviceScaleFactor float64
context driver.UIContext context driver.UIContext
input Input 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{ var theUI = &UserInterface{
@ -75,18 +67,6 @@ func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
return window.Get("innerWidth").Int(), window.Get("innerHeight").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) { func (u *UserInterface) SetFullscreen(fullscreen bool) {
// Do nothing // Do nothing
} }
@ -111,24 +91,11 @@ func (u *UserInterface) IsVsyncEnabled() bool {
return u.vsync 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 { func (u *UserInterface) CursorMode() driver.CursorMode {
if canvas.Get("style").Get("cursor").String() != "none" { if canvas.Get("style").Get("cursor").String() != "none" {
return driver.CursorModeVisible return driver.CursorModeVisible
} else {
return driver.CursorModeHidden
} }
return driver.CursorModeHidden
} }
func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
@ -150,6 +117,7 @@ func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
} }
func (u *UserInterface) SetWindowTitle(title string) { func (u *UserInterface) SetWindowTitle(title string) {
// TODO: As the page should be in an iframe, this might be useless.
document.Set("title", title) document.Set("title", title)
} }
@ -171,34 +139,31 @@ func (u *UserInterface) IsWindowResizable() bool {
func (u *UserInterface) SetWindowResizable(decorated bool) { func (u *UserInterface) SetWindowResizable(decorated bool) {
// Do nothing // 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 { func (u *UserInterface) DeviceScaleFactor() float64 {
return devicescale.GetAt(0, 0) 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() { func (u *UserInterface) updateSize() {
a := u.actualScreenScale() a := u.DeviceScaleFactor()
if u.lastActualScale != a { if u.lastDeviceScaleFactor != a {
u.updateScreenSize() u.updateScreenSize()
} }
u.lastActualScale = a u.lastDeviceScaleFactor = a
if u.sizeChanged { if u.sizeChanged {
u.sizeChanged = false 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 = document.Call("createElement", "canvas")
canvas.Set("width", 16) canvas.Set("width", 16)
canvas.Set("height", 16) canvas.Set("height", 16)
document.Get("body").Call("appendChild", canvas) document.Get("body").Call("appendChild", canvas)
htmlStyle := document.Get("documentElement").Get("style") htmlStyle := document.Get("documentElement").Get("style")
@ -315,13 +281,9 @@ func init() {
bodyStyle := document.Get("body").Get("style") bodyStyle := document.Get("body").Get("style")
bodyStyle.Set("backgroundColor", "#000") bodyStyle.Set("backgroundColor", "#000")
bodyStyle.Set("position", "relative")
bodyStyle.Set("height", "100%") bodyStyle.Set("height", "100%")
bodyStyle.Set("margin", "0") bodyStyle.Set("margin", "0")
bodyStyle.Set("padding", "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. // TODO: This is OK as long as the game is in an independent iframe.
// What if the canvas is embedded in a HTML directly? // What if the canvas is embedded in a HTML directly?
@ -331,7 +293,10 @@ func init() {
})) }))
canvasStyle := canvas.Get("style") 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. // Make the canvas focusable.
canvas.Call("setAttribute", "tabindex", 1) 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 { 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) document.Set("title", title)
u.setScreenSize(width, height)
u.pseudoScale = scale
canvas.Call("focus") canvas.Call("focus")
u.running = true u.running = true
ch := u.loop(context) ch := u.loop(context)
@ -467,37 +428,12 @@ func (u *UserInterface) RunWithoutMainLoop(width, height int, scale float64, tit
panic("js: RunWithoutMainLoop is not implemented") 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() { func (u *UserInterface) updateScreenSize() {
body := document.Get("body") body := document.Get("body")
bw := body.Get("clientWidth").Float() bw := int(body.Get("clientWidth").Float() * u.DeviceScaleFactor())
bh := body.Get("clientHeight").Float() bh := int(body.Get("clientHeight").Float() * u.DeviceScaleFactor())
sw := bw / float64(u.width) canvas.Set("width", bw)
sh := bh / float64(u.height) canvas.Set("height", bh)
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")
u.sizeChanged = true u.sizeChanged = true
} }
@ -530,6 +466,10 @@ func (u *UserInterface) IsScreenTransparent() bool {
return bodyStyle.Get("backgroundColor").String() == "transparent" return bodyStyle.Get("backgroundColor").String() == "transparent"
} }
func (u *UserInterface) CanHaveWindow() bool {
return false
}
func (u *UserInterface) Input() driver.Input { func (u *UserInterface) Input() driver.Input {
return &u.input return &u.input
} }

View File

@ -139,7 +139,7 @@ func (u *UserInterface) appMain(a app.App) {
glctx = nil glctx = nil
} }
case size.Event: case size.Event:
u.setGBuildImpl(e.WidthPx, e.HeightPx) u.setGBuild(e.WidthPx, e.HeightPx)
case paint.Event: case paint.Event:
if glctx == nil || e.External { if glctx == nil || e.External {
continue continue
@ -236,30 +236,37 @@ func (u *UserInterface) run(width, height int, scale float64, title string, cont
} }
func (u *UserInterface) updateSize(context driver.UIContext) { func (u *UserInterface) updateSize(context driver.UIContext) {
width, height := 0, 0 var width, height float64
actualScale := 0.0
u.m.Lock() u.m.Lock()
sizeChanged := u.sizeChanged sizeChanged := u.sizeChanged
if sizeChanged { if sizeChanged {
width = u.width if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 {
height = u.height s := u.scaleImpl()
actualScale = u.scaleImpl() * deviceScale() 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.sizeChanged = false
u.m.Unlock() u.m.Unlock()
if sizeChanged { if sizeChanged {
// Sizing also calls GL functions // Dirty hack to set the offscreen size for gomobile-bind.
context.SetSize(width, height, actualScale) // 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 { // Sizing also calls GL functions
u.m.Lock() context.Layout(width, height)
s := u.scaleImpl() * deviceScale() }
u.m.Unlock()
return s
} }
func (u *UserInterface) scaleImpl() float64 { func (u *UserInterface) scaleImpl() float64 {
@ -295,47 +302,26 @@ func (u *UserInterface) update(context driver.UIContext) error {
return nil 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) { func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
// TODO: This function should return gbuildWidthPx, gbuildHeightPx, // TODO: This function should return gbuildWidthPx, gbuildHeightPx,
// but these values are not initialized until the main loop starts. // but these values are not initialized until the main loop starts.
return 0, 0 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() u.m.Lock()
if u.width != width || u.height != height { if u.width != width || u.height != height || u.scale != scale {
u.width = width u.width = width
u.height = height u.height = height
u.scale = scale
u.updateGBuildScaleIfNeeded() u.updateGBuildScaleIfNeeded()
u.sizeChanged = true u.sizeChanged = true
} }
u.m.Unlock() u.m.Unlock()
} }
func (u *UserInterface) SetScreenScale(scale float64) { func (u *UserInterface) setGBuild(widthPx, heightPx int) {
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) {
u.m.Lock() u.m.Lock()
u.gbuildWidthPx = widthPx u.gbuildWidthPx = widthPx
u.gbuildHeightPx = heightPx u.gbuildHeightPx = heightPx
@ -348,6 +334,7 @@ func (u *UserInterface) updateGBuildScaleIfNeeded() {
if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 { if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 {
return return
} }
w, h := u.width, u.height w, h := u.width, u.height
scaleX := float64(u.gbuildWidthPx) / float64(w) scaleX := float64(u.gbuildWidthPx) / float64(w)
scaleY := float64(u.gbuildHeightPx) / float64(h) scaleY := float64(u.gbuildHeightPx) / float64(h)
@ -359,14 +346,8 @@ func (u *UserInterface) updateGBuildScaleIfNeeded() {
u.sizeChanged = true 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) { func (u *UserInterface) screenPaddingImpl() (x0, y0, x1, y1 float64) {
// TODO: Replace this with UIContext's Layout.
if u.gbuildScale == 0 { if u.gbuildScale == 0 {
return 0, 0, 0, 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) { func (u *UserInterface) adjustPosition(x, y int) (int, int) {
// TODO: Replace this with UIContext's AdjustPosition.
ox, oy, _, _ := u.screenPaddingImpl() ox, oy, _, _ := u.screenPaddingImpl()
s := u.scaleImpl() s := u.scaleImpl()
as := s * deviceScale() as := s * deviceScale()
@ -459,6 +441,14 @@ func (u *UserInterface) IsScreenTransparent() bool {
return false return false
} }
func (u *UserInterface) SetWindowSize(width, height int) {
// Do nothing
}
func (u *UserInterface) CanHaveWindow() bool {
return false
}
func (u *UserInterface) Input() driver.Input { func (u *UserInterface) Input() driver.Input {
return &u.input return &u.input
} }

View File

@ -60,8 +60,7 @@ func layout(viewWidth, viewHeight int, viewRectSetter ViewRectSetter) {
y := (viewHeight - height) / 2 y := (viewHeight - height) / 2
if theState.isRunning() { if theState.isRunning() {
mobile.Get().SetScreenSize(w, h) mobile.Get().SetScreenSizeAndScale(w, h, scale)
mobile.Get().SetScreenScale(scale)
} else { } else {
// The last argument 'title' is not used on mobile platforms, so just pass an empty string. // 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, "") theState.errorCh = ebiten.RunWithoutMainLoop(theState.game.Update, w, h, scale, "")

31
run.go
View File

@ -151,8 +151,8 @@ func IsRunningSlowly() bool {
func Run(f func(*Image) error, width, height int, scale float64, title string) error { func Run(f func(*Image) error, width, height int, scale float64, title string) error {
f = (&imageDumper{f: f}).update f = (&imageDumper{f: f}).update
c := newUIContext(f) theUIContext = newUIContext(f, width, height, scale)
if err := uiDriver().Run(width, height, scale, title, c, graphicsDriver()); err != nil { if err := uiDriver().Run(width, height, scale, title, theUIContext, graphicsDriver()); err != nil {
if err == driver.RegularTermination { if err == driver.RegularTermination {
return nil 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 { func RunWithoutMainLoop(f func(*Image) error, width, height int, scale float64, title string) <-chan error {
f = (&imageDumper{f: f}).update f = (&imageDumper{f: f}).update
c := newUIContext(f) theUIContext = newUIContext(f, width, height, scale)
return uiDriver().RunWithoutMainLoop(width, height, scale, title, c, graphicsDriver()) return uiDriver().RunWithoutMainLoop(width, height, scale, title, theUIContext, graphicsDriver())
} }
// ScreenSizeInFullscreen returns the size in device-independent pixels when the game is fullscreen. // 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 { if width <= 0 || height <= 0 {
panic("ebiten: width and height must be positive") 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. // 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 panics if scale is not a positive number.
// //
// SetScreenScale is concurrent-safe. // SetScreenScale is concurrent-safe.
//
// TODO: Deprecate this function.
func SetScreenScale(scale float64) { func SetScreenScale(scale float64) {
if scale <= 0 { if scale <= 0 {
panic("ebiten: scale must be positive") 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. // ScreenScale returns the current screen scale.
@ -257,8 +269,13 @@ func SetScreenScale(scale float64) {
// If Run is not called, this returns 0. // If Run is not called, this returns 0.
// //
// ScreenScale is concurrent-safe. // ScreenScale is concurrent-safe.
//
// TODO: Deprecate this function.
func ScreenScale() float64 { 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. // CursorMode returns the current cursor mode.

View File

@ -17,6 +17,7 @@ package ebiten
import ( import (
"fmt" "fmt"
"math" "math"
"sync"
"github.com/hajimehoshi/ebiten/internal/buffered" "github.com/hajimehoshi/ebiten/internal/buffered"
"github.com/hajimehoshi/ebiten/internal/clock" "github.com/hajimehoshi/ebiten/internal/clock"
@ -31,9 +32,12 @@ func init() {
graphicscommand.SetGraphicsDriver(graphicsDriver()) graphicscommand.SetGraphicsDriver(graphicsDriver())
} }
func newUIContext(f func(*Image) error) *uiContext { func newUIContext(f func(*Image) error, width, height int, scaleForWindow float64) *uiContext {
return &uiContext{ return &uiContext{
f: f, f: f,
screenWidth: width,
screenHeight: height,
scaleForWindow: scaleForWindow,
} }
} }
@ -44,32 +48,83 @@ type uiContext struct {
screenWidth int screenWidth int
screenHeight int screenHeight int
screenScale float64 screenScale float64
scaleForWindow float64
offsetX float64 offsetX float64
offsetY float64 offsetY float64
reqWidth int
reqHeight int
m sync.Mutex
} }
func (c *uiContext) SetSize(screenWidth, screenHeight int, screenScale float64) { var theUIContext *uiContext
c.screenScale = screenScale
if c.screen != nil { func (c *uiContext) resolveSize() (int, int) {
_ = c.screen.Dispose() 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 { 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
} }
c.offscreen = newImage(screenWidth, screenHeight, FilterDefault, true) func (c *uiContext) size() (int, int) {
return c.resolveSize()
}
// Round up the screensize not to cause glitches e.g. on Xperia (#622) func (c *uiContext) setScaleForWindow(scale float64) {
w := int(math.Ceil(float64(screenWidth) * screenScale)) c.scaleForWindow = scale
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
c.offsetX = px0 func (c *uiContext) getScaleForWindow() float64 {
c.offsetY = py0 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
}
// TODO: This is duplicated with mobile/ebitenmobileview/funcs.go. Refactor this.
d := uiDriver().DeviceScaleFactor()
c.screen = newScreenFramebufferImage(int(outsideWidth*d), int(outsideHeight*d))
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
}
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 { func (c *uiContext) Update(afterFrameUpdate func()) error {
@ -82,17 +137,17 @@ func (c *uiContext) Update(afterFrameUpdate func()) error {
} }
for i := 0; i < updateCount; i++ { for i := 0; i < updateCount; i++ {
// Mipmap images should be disposed by Clear.
c.offscreen.Clear() c.offscreen.Clear()
// Mipmap images should be disposed by fill.
setDrawingSkipped(i < updateCount-1) setDrawingSkipped(i < updateCount-1)
if err := hooks.RunBeforeUpdateHooks(); err != nil { if err := hooks.RunBeforeUpdateHooks(); err != nil {
return err return err
} }
if err := c.f(c.offscreen); err != nil { if err := c.f(c.offscreen); err != nil {
return err return err
} }
uiDriver().Input().ResetForFrame() uiDriver().Input().ResetForFrame()
afterFrameUpdate() afterFrameUpdate()
} }
@ -107,7 +162,7 @@ func (c *uiContext) Update(afterFrameUpdate func()) error {
// c.screen is special: its Y axis is down to up, // c.screen is special: its Y axis is down to up,
// and the origin point is lower left. // and the origin point is lower left.
op.GeoM.Scale(c.screenScale, -c.screenScale) 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: case driver.VUpward:
op.GeoM.Scale(c.screenScale, c.screenScale) op.GeoM.Scale(c.screenScale, c.screenScale)
default: default:
@ -131,3 +186,8 @@ func (c *uiContext) Update(afterFrameUpdate func()) error {
} }
return nil 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
}