ebiten: add APIs to treat monitors (#2597)

This change adds these APIs:

* `type MonitorType`
* `func (*MonitorType) Bounds() image.Rectangle`
* `func (*MonitorType) Name() string`
* `func Monitor() *MonitorType`
* `func SetMonitor(*MonitorType)`
* `func AppendMonitors([]*MonitorType) []*MonitorType`

Closes #1835
This commit is contained in:
Ketchetwahmeegwun T. Southall 2023-08-30 05:02:04 -07:00 committed by GitHub
parent b1b4335423
commit 60b7de6a3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 480 additions and 30 deletions

127
examples/monitor/main.go Normal file
View File

@ -0,0 +1,127 @@
// Copyright 2023 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"flag"
"fmt"
"image/color"
"log"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
)
var (
mplusNormalFont font.Face
)
func init() {
tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf)
if err != nil {
log.Fatal(err)
}
const dpi = 72
mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 24,
DPI: dpi,
Hinting: font.HintingFull,
})
if err != nil {
log.Fatal(err)
}
}
const (
screenWidth = 640
screenHeight = 480
)
type Game struct {
monitors []*ebiten.MonitorType
}
func (g *Game) Update() error {
// Refresh monitors.
g.monitors = ebiten.AppendMonitors(g.monitors[:0])
// Handle keypresses.
if inpututil.IsKeyJustReleased(ebiten.KeyF) {
ebiten.SetFullscreen(!ebiten.IsFullscreen())
} else {
for i, m := range g.monitors {
if inpututil.IsKeyJustPressed(ebiten.KeyDigit0 + ebiten.Key(i)) {
ebiten.SetWindowTitle(m.Name())
ebiten.SetMonitor(m)
}
}
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
const x = 0
y := 24
text.Draw(screen, "F to toggle fullscreen\n0-9 to change monitor", mplusNormalFont, x, y, color.White)
y += 72
for i, m := range g.monitors {
text.Draw(screen, fmt.Sprintf("%d: %s %s", i, m.Name(), m.Bounds().String()), mplusNormalFont, x, y, color.White)
y += 24
}
activeMonitor := ebiten.Monitor()
for i, m := range g.monitors {
if m == activeMonitor {
text.Draw(screen, fmt.Sprintf("active: %s (%d)", m.Name(), i), mplusNormalFont, x, y, color.White)
}
}
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
func main() {
g := &Game{}
// Allow the user to pass in a monitor flag to target a specific monitor.
var monitor int
flag.IntVar(&monitor, "monitor", 0, "target monitor index to run the program on")
flag.Parse()
// Read our monitors.
g.monitors = ebiten.AppendMonitors(nil)
// Ensure the user did not supply a monitor index beyond the range of available monitors. If they did, set the monitor to the primary.
if monitor < 0 || monitor >= len(g.monitors) {
monitor = 0
}
targetMonitor := g.monitors[monitor]
ebiten.SetMonitor(targetMonitor)
ebiten.SetWindowTitle(targetMonitor.Name())
ebiten.SetWindowSize(screenWidth, screenHeight)
if err := ebiten.RunGame(g); err != nil {
log.Fatal(err)
}
}

View File

@ -94,6 +94,10 @@ func (m *Monitor) GetVideoMode() *VidMode {
}
}
func (m *Monitor) GetName() string {
return m.m.GetName()
}
type Window struct {
w *cglfw.Window

View File

@ -56,6 +56,14 @@ func (m *Monitor) GetVideoMode() *VidMode {
return (*VidMode)(v)
}
func (m *Monitor) GetName() string {
v, err := (*goglfw.Monitor)(m).GetName()
if err != nil {
panic(err)
}
return v
}
type Window goglfw.Window
func (w *Window) Destroy() {

View File

@ -95,3 +95,8 @@ type RunOptions struct {
ScreenTransparent bool
SkipTaskbar bool
}
// InitialWindowPosition returns the position for centering the given second width/height pair within the first width/height pair.
func InitialWindowPosition(mw, mh, ww, wh int) (x, y int) {
return (mw - ww) / 2, (mh - wh) / 3
}

View File

@ -85,6 +85,7 @@ type userInterfaceImpl struct {
initFullscreen bool
initCursorMode CursorMode
initWindowDecorated bool
initWindowMonitor int
initWindowPositionXInDIP int
initWindowPositionYInDIP int
initWindowWidthInDIP int
@ -135,6 +136,7 @@ func init() {
maxWindowHeightInDIP: glfw.DontCare,
initCursorMode: CursorModeVisible,
initWindowDecorated: true,
initWindowMonitor: glfw.DontCare,
initWindowPositionXInDIP: invalidPos,
initWindowPositionYInDIP: invalidPos,
initWindowWidthInDIP: 640,
@ -180,13 +182,7 @@ func initialize() error {
return errors.New("ui: no monitor was found at initialize")
}
theUI.initMonitor = m
theUI.initDeviceScaleFactor = theUI.deviceScaleFactor(m)
// GetVideoMode must be called from the main thread, then call this here and record
// initFullscreen{Width,Height}InDIP.
v := m.GetVideoMode()
theUI.initFullscreenWidthInDIP = int(theUI.dipFromGLFWMonitorPixel(float64(v.Width), m))
theUI.initFullscreenHeightInDIP = int(theUI.dipFromGLFWMonitorPixel(float64(v.Height), m))
theUI.setInitMonitor(m)
// Create system cursors. These cursors are destroyed at glfw.Terminate().
glfwSystemCursors[CursorShapeDefault] = nil
@ -203,12 +199,43 @@ func initialize() error {
return nil
}
type monitor struct {
func (u *userInterfaceImpl) setInitMonitor(m *glfw.Monitor) {
u.initMonitor = m
u.initDeviceScaleFactor = u.deviceScaleFactor(m)
// GetVideoMode must be called from the main thread, then call this here and record
// initFullscreen{Width,Height}InDIP.
v := m.GetVideoMode()
u.initFullscreenWidthInDIP = int(u.dipFromGLFWMonitorPixel(float64(v.Width), m))
u.initFullscreenHeightInDIP = int(u.dipFromGLFWMonitorPixel(float64(v.Height), m))
}
// Monitor is a wrapper around glfw.Monitor.
type Monitor struct {
m *glfw.Monitor
vm *glfw.VidMode
// Pos of monitor in virtual coords
x int
y int
x int
y int
width int
height int
id int
name string
}
// Bounds returns the monitor's bounds.
func (m *Monitor) Bounds() image.Rectangle {
ui := Get()
return image.Rect(
int(ui.dipFromGLFWMonitorPixel(float64(m.x), m.m)),
int(ui.dipFromGLFWMonitorPixel(float64(m.y), m.m)),
int(ui.dipFromGLFWMonitorPixel(float64(m.x+m.width), m.m)),
int(ui.dipFromGLFWMonitorPixel(float64(m.x+m.height), m.m)),
)
}
// Name returns the monitor's name.
func (m *Monitor) Name() string {
return m.name
}
// monitors is the monitor list cache for desktop glfw compile targets.
@ -216,25 +243,63 @@ type monitor struct {
// monitor config change event.
//
// monitors must be manipulated on the main thread.
var monitors []*monitor
var monitors []*Monitor
// AppendMonitors appends the current monitors to the passed in mons slice and returns it.
func (u *userInterfaceImpl) AppendMonitors(mons []*Monitor) []*Monitor {
u.m.RLock()
defer u.m.RUnlock()
return append(mons, monitors...)
}
// Monitor returns the window's current monitor. Returns nil if there is no current monitor yet.
func (u *userInterfaceImpl) Monitor() *Monitor {
if !u.isRunning() {
return nil
}
var monitor *Monitor
u.mainThread.Call(func() {
glfwMonitor := u.currentMonitor()
if glfwMonitor == nil {
return
}
for _, m := range monitors {
if m.m == glfwMonitor {
monitor = m
return
}
}
})
return monitor
}
func updateMonitors() {
monitors = nil
ms := glfw.GetMonitors()
for _, m := range ms {
x, y := m.GetPos()
monitors = append(monitors, &monitor{
m: m,
vm: m.GetVideoMode(),
x: x,
y: y,
})
for i, m := range ms {
monitor := glfwMonitorToMonitor(m)
monitor.id = i
monitors = append(monitors, &monitor)
}
clearVideoModeScaleCache()
devicescale.ClearCache()
}
func ensureMonitors() []*monitor {
func glfwMonitorToMonitor(m *glfw.Monitor) Monitor {
x, y := m.GetPos()
vm := m.GetVideoMode()
return Monitor{
m: m,
vm: m.GetVideoMode(),
x: x,
y: y,
width: vm.Width,
height: vm.Height,
name: m.GetName(),
}
}
func ensureMonitors() []*Monitor {
if len(monitors) == 0 {
updateMonitors()
}
@ -245,7 +310,7 @@ func ensureMonitors() []*monitor {
// or returns nil if monitor is not found.
//
// getMonitorFromPosition must be called on the main thread.
func getMonitorFromPosition(wx, wy int) *monitor {
func getMonitorFromPosition(wx, wy int) *Monitor {
for _, m := range ensureMonitors() {
// TODO: Fix incorrectness in the cases of https://github.com/glfw/glfw/issues/1961.
// See also internal/devicescale/impl_desktop.go for a maybe better way of doing this.
@ -268,6 +333,74 @@ func (u *userInterfaceImpl) setRunning(running bool) {
}
}
// setWindowMonitor must be called on the main thread.
func (u *userInterfaceImpl) setWindowMonitor(monitor int) {
if microsoftgdk.IsXbox() {
return
}
u.m.RLock()
defer u.m.RUnlock()
m := monitors[monitor].m
// Ignore if it is the same monitor.
if m == u.window.GetMonitor() {
return
}
// We set w, h so it can be set to the original dimensions if fullscreen.
w, h := u.window.GetSize()
fullscreen := u.isFullscreen()
// This is copied from setFullscreen. They should probably use a shared function.
if fullscreen {
origX, origY := u.origWindowPos()
w = int(u.dipToGLFWPixel(float64(u.origWindowWidthInDIP), u.currentMonitor()))
h = int(u.dipToGLFWPixel(float64(u.origWindowHeightInDIP), u.currentMonitor()))
if u.isNativeFullscreenAvailable() {
u.setNativeFullscreen(false)
// Adjust the window size later (after adjusting the position).
} else if !u.isNativeFullscreenAvailable() && u.window.GetMonitor() != nil {
u.window.SetMonitor(nil, 0, 0, w, h, 0)
}
// glfw.PollEvents is necessary for macOS to enable (*glfw.Window).SetPos and SetSize (#2296).
// This polling causes issues on Linux and Windows when rapidly toggling fullscreen, so we only run it under macOS.
if runtime.GOOS == "darwin" {
glfw.PollEvents()
}
if origX != invalidPos && origY != invalidPos {
u.window.SetPos(origX, origY)
// 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(origX+1, origY)
u.window.SetPos(origX, origY)
}
u.setOrigWindowPos(invalidPos, invalidPos)
}
}
x, y := m.GetPos()
px, py := InitialWindowPosition(m.GetVideoMode().Width, m.GetVideoMode().Height, w, h)
u.window.SetPos(x+px, y+py)
if fullscreen {
if u.isNativeFullscreenAvailable() {
u.setNativeFullscreen(fullscreen)
} else {
v := m.GetVideoMode()
u.window.SetMonitor(m, 0, 0, v.Width, v.Height, v.RefreshRate)
}
u.setOrigWindowPos(x, y)
u.adjustViewSizeAfterFullscreen()
}
}
func (u *userInterfaceImpl) getWindowSizeLimitsInDIP() (minw, minh, maxw, maxh int) {
if microsoftgdk.IsXbox() {
return glfw.DontCare, glfw.DontCare, glfw.DontCare, glfw.DontCare
@ -381,6 +514,24 @@ func (u *userInterfaceImpl) setIconImages(iconImages []image.Image) {
u.m.Unlock()
}
func (u *userInterfaceImpl) getInitWindowMonitor() int {
u.m.RLock()
v := u.initWindowMonitor
u.m.RUnlock()
return v
}
func (u *userInterfaceImpl) setInitWindowMonitor(monitor int) {
if microsoftgdk.IsXbox() {
return
}
u.m.Lock()
defer u.m.Unlock()
u.initWindowMonitor = monitor
}
func (u *userInterfaceImpl) getInitWindowPositionInDIP() (int, int) {
if microsoftgdk.IsXbox() {
return 0, 0
@ -670,7 +821,7 @@ func init() {
// createWindow must be called from the main thread.
//
// createWindow does not set the position or size so far.
func (u *userInterfaceImpl) createWindow(width, height int) error {
func (u *userInterfaceImpl) createWindow(width, height int, monitor int) error {
if u.window != nil {
panic("ui: u.window must not exist at createWindow")
}
@ -680,7 +831,16 @@ func (u *userInterfaceImpl) createWindow(width, height int) error {
if err != nil {
return err
}
// Set our target monitor if provided. This is required to prevent an initial window flash on the default monitor.
if monitor != glfw.DontCare {
m := monitors[monitor]
x, y := m.m.GetPos()
px, py := InitialWindowPosition(m.m.GetVideoMode().Width, m.m.GetVideoMode().Height, width, height)
window.SetPos(x+px, y+py)
}
initializeWindowAfterCreation(window)
u.window = window
// Even just after a window creation, FramebufferSize callback might be invoked (#1847).
@ -866,10 +1026,17 @@ func (u *userInterfaceImpl) initOnMainThread(options *RunOptions) error {
glfw.WindowHint(glfw.Visible, glfw.True)
}
// Get our target monitor.
monitor := u.getInitWindowMonitor()
if monitor != glfw.DontCare {
u.setInitMonitor(monitors[monitor].m)
}
ww, wh := u.getInitWindowSizeInDIP()
initW := int(u.dipToGLFWPixel(float64(ww), u.initMonitor))
initH := int(u.dipToGLFWPixel(float64(wh), u.initMonitor))
if err := u.createWindow(initW, initH); err != nil {
if err := u.createWindow(initW, initH, monitor); err != nil {
return err
}
@ -1371,11 +1538,6 @@ func (u *userInterfaceImpl) currentMonitor() *glfw.Monitor {
//
// monitorFromWindow must be called on the main thread.
func monitorFromWindow(window *glfw.Window) *glfw.Monitor {
// GetMonitor is available only in fullscreen.
if m := window.GetMonitor(); m != nil {
return m
}
// Getting a monitor from a window position is not reliable in general (e.g., when a window is put across
// multiple monitors, or, before SetWindowPosition is called.).
// Get the monitor which the current window belongs to. This requires OS API.

View File

@ -15,6 +15,7 @@
package ui
import (
"image"
"sync"
"syscall/js"
"time"
@ -756,6 +757,29 @@ func (u *userInterfaceImpl) Window() Window {
return &nullWindow{}
}
type Monitor struct{}
var theMonitor = &Monitor{}
func (m *Monitor) Bounds() image.Rectangle {
screen := window.Get("screen")
w := screen.Get("width").Int()
h := screen.Get("height").Int()
return image.Rect(0, 0, w, h)
}
func (m *Monitor) Name() string {
return ""
}
func (u *userInterfaceImpl) AppendMonitors(mons []*Monitor) []*Monitor {
return append(mons, theMonitor)
}
func (u *userInterfaceImpl) Monitor() *Monitor {
return theMonitor
}
func (u *userInterfaceImpl) beginFrame() {
}

View File

@ -19,6 +19,7 @@ package ui
import (
stdcontext "context"
"fmt"
"image"
"runtime/debug"
"sync"
"sync/atomic"
@ -428,6 +429,27 @@ func (u *userInterfaceImpl) Window() Window {
return &nullWindow{}
}
type Monitor struct{}
var theMonitor = &Monitor{}
func (m *Monitor) Bounds() image.Rectangle {
// TODO: This should return the available viewport dimensions.
return image.Rectangle{}
}
func (m *Monitor) Name() string {
return ""
}
func (u *userInterfaceImpl) AppendMonitors(mons []*Monitor) []*Monitor {
return append(mons, theMonitor)
}
func (u *userInterfaceImpl) Monitor() *Monitor {
return theMonitor
}
func (u *userInterfaceImpl) UpdateInput(keys map[Key]struct{}, runes []rune, touches []TouchForInput) {
u.updateInputState(keys, runes, touches)
if u.fpsMode == FPSModeVsyncOffMinimum {

View File

@ -22,6 +22,7 @@ import "C"
import (
stdcontext "context"
"image"
"runtime"
"sync"
@ -193,6 +194,27 @@ func (*userInterfaceImpl) Window() Window {
return &nullWindow{}
}
type Monitor struct{}
var theMonitor = &Monitor{}
func (m *Monitor) Bounds() image.Rectangle {
// TODO: This should return the available viewport dimensions.
return image.Rectangle{}
}
func (m *Monitor) Name() string {
return ""
}
func (u *userInterfaceImpl) AppendMonitors(mons []*Monitor) []*Monitor {
return append(mons, theMonitor)
}
func (u *userInterfaceImpl) Monitor() *Monitor {
return theMonitor
}
func (u *userInterfaceImpl) beginFrame() {
}

View File

@ -23,6 +23,7 @@ type Window interface {
SetDecorated(decorated bool)
ResizingMode() WindowResizingMode
SetResizingMode(mode WindowResizingMode)
SetMonitor(*Monitor)
Position() (int, int)
SetPosition(x, y int)
Size() (int, int)
@ -58,6 +59,9 @@ func (*nullWindow) ResizingMode() WindowResizingMode {
func (*nullWindow) SetResizingMode(mode WindowResizingMode) {
}
func (*nullWindow) SetMonitor(monitor *Monitor) {
}
func (*nullWindow) Position() (int, int) {
return 0, 0
}

View File

@ -159,6 +159,19 @@ func (w *glfwWindow) Restore() {
w.ui.mainThread.Call(w.ui.restoreWindow)
}
func (w *glfwWindow) SetMonitor(monitor *Monitor) {
if monitor == nil {
panic("ui: monitor cannot be nil at SetMonitor")
}
if !w.ui.isRunning() {
w.ui.setInitWindowMonitor(monitor.id)
return
}
w.ui.mainThread.Call(func() {
w.ui.setWindowMonitor(monitor.id)
})
}
func (w *glfwWindow) Position() (int, int) {
if !w.ui.isRunning() {
panic("ui: WindowPosition can't be called before the main loop starts")

60
monitor.go Normal file
View File

@ -0,0 +1,60 @@
// Copyright 2023 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ebiten
import (
"image"
"github.com/hajimehoshi/ebiten/v2/internal/ui"
)
// MonitorType represents a monitor available to the system.
type MonitorType ui.Monitor
// Bounds returns the position and size of the monitor in device-independent pixels.
func (m *MonitorType) Bounds() image.Rectangle {
return (*ui.Monitor)(m).Bounds()
}
// Name returns the monitor's name. On Linux, this reports the monitors in xrandr format.
// On Windows, this reports "Generic PnP Monitor" for all monitors.
func (m *MonitorType) Name() string {
return (*ui.Monitor)(m).Name()
}
// Monitor returns the current monitor.
func Monitor() *MonitorType {
m := ui.Get().Monitor()
if m == nil {
return nil
}
return (*MonitorType)(m)
}
// SetMonitor sets the monitor that the window should be on. This can be called before or after Run.
func SetMonitor(monitor *MonitorType) {
ui.Get().Window().SetMonitor((*ui.Monitor)(monitor))
}
// AppendMonitors returns the monitors reported by the system.
// On desktop platforms, there will always be at least one monitor appended and the first monitor in the slice will be the primary monitor.
// Any monitors added or removed will show up with subsequent calls to this function.
func AppendMonitors(monitors []*MonitorType) []*MonitorType {
// TODO: This is not an efficient operation. It would be best if we could directly pass monitors directly into `ui.AppendMonitors`.
for _, m := range ui.Get().AppendMonitors(nil) {
monitors = append(monitors, (*MonitorType)(m))
}
return monitors
}

View File

@ -166,8 +166,7 @@ var (
func initializeWindowPositionIfNeeded(width, height int) {
if atomic.LoadUint32(&windowPositionSetExplicitly) == 0 {
sw, sh := ui.Get().ScreenSizeInFullscreen()
x := (sw - width) / 2
y := (sh - height) / 3
x, y := ui.InitialWindowPosition(sw, sh, width, height)
ui.Get().Window().SetPosition(x, y)
}
}