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)
// 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)

View File

@ -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

View File

@ -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() {

View File

@ -23,7 +23,6 @@ import (
"context"
"fmt"
"image"
"math"
"os"
"runtime"
"sync"
@ -40,10 +39,11 @@ import (
type UserInterface struct {
title string
window *glfw.Window
screenWidthInDP int
screenHeightInDP int
scale float64
fullscreenScale float64
// 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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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, "")

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 {
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.

View File

@ -17,6 +17,7 @@ package ebiten
import (
"fmt"
"math"
"sync"
"github.com/hajimehoshi/ebiten/internal/buffered"
"github.com/hajimehoshi/ebiten/internal/clock"
@ -31,9 +32,12 @@ 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,
screenWidth: width,
screenHeight: height,
scaleForWindow: scaleForWindow,
}
}
@ -44,32 +48,83 @@ type uiContext struct {
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
}
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)
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
func (c *uiContext) setScaleForWindow(scale float64) {
c.scaleForWindow = scale
}
c.offsetX = px0
c.offsetY = py0
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
}
// 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 {
@ -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
}