app: [API] add minimized window mode, change methods to options

The window modes are extended, following microsoft conventions.
We have Fullscreen, Overlapping, Maximized and Minimized.
These modes can be set via options when a new window is creates,
or modified later by calling helper functions like w.Maximize() and w.Center()

The window configuration is automatically updated when a user
modifies the window by dragging or clicking the icons on the window's title-bar,
minimizing or maximizing the window.

Any change, either by the user or the application will emit a ConfigChange event.

This is implemented and tested on Windows only.

API change. the app.Window methods Maximize and Center are replaced with similar
options. For example, to maximize a window use

    w.Option(app.Maximized.Option())

Also, Maximize and Center implementations for X11 and macOS are left for a future
change.

Fixes: https://todo.sr.ht/~eliasnaur/gio/315
Signed-off-by: Jan Kåre Vatne <jkvatne@online.no>
This commit is contained in:
Jan Kåre Vatne
2022-01-12 20:41:44 +01:00
committed by Elias Naur
parent 13183522dd
commit c4f98d3c1e
8 changed files with 161 additions and 224 deletions
+58 -37
View File
@@ -106,9 +106,13 @@ const (
SIZE_MINIMIZED = 1
SIZE_RESTORED = 0
SW_SHOWDEFAULT = 10
SW_SHOWDEFAULT = 10
SW_SHOWMINIMIZED = 2
SW_SHOWMAXIMIZED = 3
SW_SHOWNORMAL = 1
SW_SHOW = 5
SWP_FRAMECHANGED = 0x0020
SWP_FRAMECHANGED = 0x0020
SWP_NOMOVE = 0x0002
SWP_NOOWNERZORDER = 0x0200
SWP_NOSIZE = 0x0001
@@ -166,42 +170,44 @@ const (
UNICODE_NOCHAR = 65535
WM_CANCELMODE = 0x001F
WM_CHAR = 0x0102
WM_CREATE = 0x0001
WM_DPICHANGED = 0x02E0
WM_DESTROY = 0x0002
WM_ERASEBKGND = 0x0014
WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101
WM_LBUTTONDOWN = 0x0201
WM_LBUTTONUP = 0x0202
WM_MBUTTONDOWN = 0x0207
WM_MBUTTONUP = 0x0208
WM_MOUSEMOVE = 0x0200
WM_MOUSEWHEEL = 0x020A
WM_MOUSEHWHEEL = 0x020E
WM_PAINT = 0x000F
WM_CLOSE = 0x0010
WM_QUIT = 0x0012
WM_SETCURSOR = 0x0020
WM_SETFOCUS = 0x0007
WM_KILLFOCUS = 0x0008
WM_SHOWWINDOW = 0x0018
WM_SIZE = 0x0005
WM_SYSKEYDOWN = 0x0104
WM_SYSKEYUP = 0x0105
WM_RBUTTONDOWN = 0x0204
WM_RBUTTONUP = 0x0205
WM_TIMER = 0x0113
WM_UNICHAR = 0x0109
WM_USER = 0x0400
WM_GETMINMAXINFO = 0x0024
WM_CANCELMODE = 0x001F
WM_CHAR = 0x0102
WM_CREATE = 0x0001
WM_DPICHANGED = 0x02E0
WM_DESTROY = 0x0002
WM_ERASEBKGND = 0x0014
WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101
WM_LBUTTONDOWN = 0x0201
WM_LBUTTONUP = 0x0202
WM_MBUTTONDOWN = 0x0207
WM_MBUTTONUP = 0x0208
WM_MOUSEMOVE = 0x0200
WM_MOUSEWHEEL = 0x020A
WM_MOUSEHWHEEL = 0x020E
WM_PAINT = 0x000F
WM_CLOSE = 0x0010
WM_QUIT = 0x0012
WM_SETCURSOR = 0x0020
WM_SETFOCUS = 0x0007
WM_KILLFOCUS = 0x0008
WM_SHOWWINDOW = 0x0018
WM_SIZE = 0x0005
WM_SYSKEYDOWN = 0x0104
WM_SYSKEYUP = 0x0105
WM_RBUTTONDOWN = 0x0204
WM_RBUTTONUP = 0x0205
WM_TIMER = 0x0113
WM_UNICHAR = 0x0109
WM_USER = 0x0400
WM_GETMINMAXINFO = 0x0024
WM_WINDOWPOSCHANGED = 0x0047
WS_CLIPCHILDREN = 0x00010000
WS_CLIPSIBLINGS = 0x04000000
WS_MAXIMIZE = 0x01000000
WS_ICONIC = 0x20000000
WS_VISIBLE = 0x10000000
WS_OVERLAPPED = 0x00000000
WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME |
WS_MINIMIZEBOX | WS_MAXIMIZEBOX
@@ -259,7 +265,7 @@ var (
_DestroyWindow = user32.NewProc("DestroyWindow")
_DispatchMessage = user32.NewProc("DispatchMessageW")
_EmptyClipboard = user32.NewProc("EmptyClipboard")
_GetClientRect = user32.NewProc("GetClientRect")
_GetWindowRect = user32.NewProc("GetWindowRect")
_GetClipboardData = user32.NewProc("GetClipboardData")
_GetDC = user32.NewProc("GetDC")
_GetDpiForWindow = user32.NewProc("GetDpiForWindow")
@@ -370,8 +376,8 @@ func EmptyClipboard() error {
return nil
}
func GetClientRect(hwnd syscall.Handle, r *Rect) {
_GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(r)))
func GetWindowRect(hwnd syscall.Handle, r *Rect) {
_GetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(r)))
issue34474KeepAlive(r)
}
@@ -683,6 +689,21 @@ func (p WindowPlacement) Rect() Rect {
return p.rcNormalPosition
}
func (p WindowPlacement) IsMinimized() bool {
return p.showCmd == SW_SHOWMINIMIZED
}
func (p WindowPlacement) IsMaximized() bool {
return p.showCmd == SW_SHOWMAXIMIZED
}
func (p *WindowPlacement) Set(Left, Top, Right, Bottom int) {
p.rcNormalPosition.Left = int32(Left)
p.rcNormalPosition.Top = int32(Top)
p.rcNormalPosition.Right = int32(Right)
p.rcNormalPosition.Bottom = int32(Bottom)
}
// issue34474KeepAlive calls runtime.KeepAlive as a
// workaround for golang.org/issue/34474.
func issue34474KeepAlive(v interface{}) {
+14 -7
View File
@@ -41,6 +41,8 @@ type Config struct {
// CustomRenderer is true when the window content is rendered by the
// client.
CustomRenderer bool
// center is a flag used to center the window. Set by option.
center bool
}
// ConfigEvent is sent whenever the configuration of a Window changes.
@@ -57,8 +59,8 @@ func (c *Config) apply(m unit.Metric, options []Option) {
type wakeupEvent struct{}
// WindowMode is the window mode (WindowMode.Option sets it).
//
// Supported platforms are macOS, X11, Windows, Android and JS.
// Note that mode can be changed programatically as well as by the user
// clicking on the minimize/maximize buttons on the window's title bar.
type WindowMode uint8
const (
@@ -66,20 +68,30 @@ const (
Windowed WindowMode = iota
// Fullscreen is the full screen window mode.
Fullscreen
// Minimized is for systems where the window can be minimized to an icon.
Minimized
// Maximized is for systems where the window can be made to fill the available monitor area.
Maximized
)
// Option changes the mode of a Window.
func (m WindowMode) Option() Option {
return func(_ unit.Metric, cnf *Config) {
cnf.Mode = m
}
}
// String returns the mode name.
func (m WindowMode) String() string {
switch m {
case Windowed:
return "windowed"
case Fullscreen:
return "fullscreen"
case Minimized:
return "minimized"
case Maximized:
return "maximized"
}
return ""
}
@@ -165,11 +177,6 @@ type driver interface {
// Wakeup wakes up the event loop and sends a WakeupEvent.
Wakeup()
// Maximize will make the window as large as possible, but keep the frame decorations.
Maximize()
// Center will place the window at monitor center.
Center()
}
type windowRendezvous struct {
-6
View File
@@ -1261,12 +1261,6 @@ func setNavigationColor(env *C.JNIEnv, view C.jobject, color color.NRGBA) {
// Close the window. Not implemented for Android.
func (w *window) Close() {}
// Maximize maximizes the window. Not implemented for Android.
func (w *window) Maximize() {}
// Center the window. Not implemented for Android.
func (w *window) Center() {}
// runOnMain runs a function on the Java main thread.
func runOnMain(f func(env *C.JNIEnv)) {
go func() {
-6
View File
@@ -336,12 +336,6 @@ func (w *window) SetInputHint(_ key.InputHint) {}
// Close the window. Not implemented for iOS.
func (w *window) Close() {}
// Maximize the window. Not implemented for iOS.
func (w *window) Maximize() {}
// Center the window. Not implemented for iOS.
func (w *window) Center() {}
func newWindow(win *callbacks, options []Option) error {
mainWindow.in <- windowAndConfig{win, options}
return <-mainWindow.errs
-6
View File
@@ -566,12 +566,6 @@ func (w *window) SetInputHint(mode key.InputHint) {
// Close the window. Not implemented for js.
func (w *window) Close() {}
// Maximize the window. Not implemented for js.
func (w *window) Maximize() {}
// Center the window. Not implemented for js.
func (w *window) Center() {}
func (w *window) resize() {
w.scale = float32(w.window.Get("devicePixelRatio").Float())
-6
View File
@@ -1487,12 +1487,6 @@ func (w *window) Close() {
w.dead = true
}
// Maximize the window. Not implemented for Wayland.
func (w *window) Maximize() {}
// Center the window. Not implemented for Wayland.
func (w *window) Center() {}
func (w *window) NewContext() (context, error) {
var firstErr error
if f := newWaylandVulkanContext; f != nil {
+78 -138
View File
@@ -108,7 +108,6 @@ func newWindow(window *callbacks, options []Option) error {
w.w.SetDriver(w)
w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)})
w.Configure(options)
windows.ShowWindow(w.hwnd, windows.SW_SHOWDEFAULT)
windows.SetForegroundWindow(w.hwnd)
windows.SetFocus(w.hwnd)
// Since the window class for the cursor is null,
@@ -185,6 +184,43 @@ func createNativeWindow() (*window, error) {
return w, nil
}
// update() handles changes done by the user, and updates the configuration.
// It reads the window style and size/position and updates w.config.
// If anything has changed it emits a ConfigEvent to notify the application.
func (w *window) update() {
var triggerEvent bool
var r windows.Rect
windows.GetWindowRect(w.hwnd, &r)
size := image.Point{
X: int(r.Right - r.Left - w.deltas.width),
Y: int(r.Bottom - r.Top - w.deltas.height),
}
if size != w.config.Size {
w.config.Size = size
triggerEvent = true
}
// Check the window mode.
mode := w.config.Mode
p := windows.GetWindowPlacement(w.hwnd)
style := windows.GetWindowLong(w.hwnd)
if style&windows.WS_OVERLAPPEDWINDOW == 0 {
mode = Fullscreen
} else if p.IsMinimized() {
mode = Minimized
} else if p.IsMaximized() {
mode = Maximized
} else {
mode = Windowed
}
if mode != w.config.Mode {
w.config.Mode = mode
triggerEvent = true
}
if triggerEvent {
w.w.Event(ConfigEvent{Config: w.config})
}
}
func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
win, exists := winMap.Load(hwnd)
if !exists {
@@ -280,29 +316,10 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_SIZE:
switch wParam {
case windows.SIZE_MINIMIZED:
w.update()
w.setStage(system.StagePaused)
case windows.SIZE_MAXIMIZED, windows.SIZE_RESTORED:
var triggerEvent bool
// Check the window size change.
var r windows.Rect
windows.GetClientRect(w.hwnd, &r)
size := image.Point{
X: int(r.Right - r.Left),
Y: int(r.Bottom - r.Top),
}
if size != w.config.Size {
w.config.Size = size
triggerEvent = true
}
// Check the window mode.
mode := w.config.Mode
w.updateWindowMode()
if mode != w.config.Mode {
triggerEvent = true
}
if triggerEvent {
w.w.Event(ConfigEvent{Config: w.config})
}
w.update()
w.setStage(system.StageRunning)
}
case windows.WM_GETMINMAXINFO:
@@ -503,127 +520,44 @@ func (w *window) readClipboard() error {
return nil
}
func (w *window) updateWindowMode() {
p := windows.GetWindowPlacement(w.hwnd)
r := p.Rect()
mi := windows.GetMonitorInfo(w.hwnd)
if r == mi.Monitor {
w.config.Mode = Fullscreen
} else {
w.config.Mode = Windowed
}
}
func (w *window) Configure(options []Option) {
oldConfig := w.config
dpi := windows.GetSystemDPI()
cfg := configForDPI(dpi)
prev := w.config
w.updateWindowMode()
cnf := w.config
cnf.apply(cfg, options)
metric := configForDPI(dpi)
w.config.apply(metric, options)
switch w.config.Mode {
case Minimized:
windows.ShowWindow(w.hwnd, windows.SW_SHOWMINIMIZED)
if cnf.Mode != Fullscreen && prev.Size != cnf.Size {
width := int32(cnf.Size.X)
height := int32(cnf.Size.Y)
w.config.Size = cnf.Size
// Include the window decorations.
wr := windows.Rect{
Right: width,
Bottom: height,
}
dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW)
dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE)
windows.AdjustWindowRectEx(&wr, dwStyle, 0, dwExStyle)
dw, dh := width, height
width = wr.Right - wr.Left
height = wr.Bottom - wr.Top
w.deltas.width = width - dw
w.deltas.height = height - dh
windows.MoveWindow(w.hwnd, 0, 0, width, height, true)
}
if prev.MinSize != cnf.MinSize {
w.config.MinSize = cnf.MinSize
}
if prev.MaxSize != cnf.MaxSize {
w.config.MaxSize = cnf.MaxSize
}
if prev.Title != cnf.Title {
w.config.Title = cnf.Title
windows.SetWindowText(w.hwnd, cnf.Title)
}
if prev.Mode != cnf.Mode {
w.SetWindowMode(cnf.Mode)
}
if w.config != prev {
w.w.Event(ConfigEvent{Config: w.config})
}
}
// Maximize the window. It will have no effect when in fullscreen mode.
func (w *window) Maximize() {
if w.config.Mode == Fullscreen {
return
}
style := windows.GetWindowLong(w.hwnd)
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style|windows.WS_OVERLAPPEDWINDOW|windows.WS_MAXIMIZE)
mi := windows.GetMonitorInfo(w.hwnd)
windows.SetWindowPos(w.hwnd, 0,
mi.Monitor.Left, mi.Monitor.Top,
mi.Monitor.Right-mi.Monitor.Left,
mi.Monitor.Bottom-mi.Monitor.Top,
windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED,
)
}
// Center will place window at monitor center.
func (w *window) Center() {
// Make sure that the window is sizeable
style := windows.GetWindowLong(w.hwnd) & (^uintptr(windows.WS_MAXIMIZE))
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style|windows.WS_OVERLAPPEDWINDOW)
// Find with/height including the window decorations.
wr := windows.Rect{
Right: int32(w.config.Size.X),
Bottom: int32(w.config.Size.Y),
}
dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW)
dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE)
windows.AdjustWindowRectEx(&wr, dwStyle, 0, dwExStyle)
width := wr.Right - wr.Left
height := wr.Bottom - wr.Top
// Move to center of current monitor
mi := windows.GetMonitorInfo(w.hwnd).Monitor
x := mi.Left + (mi.Right-mi.Left-width)/2
y := mi.Top + (mi.Bottom-mi.Top-height)/2
windows.MoveWindow(w.hwnd, x, y, width, height, true)
}
func (w *window) SetWindowMode(mode WindowMode) {
// https://devblogs.microsoft.com/oldnewthing/20100412-00/?p=14353
switch mode {
case Windowed:
if w.placement == nil {
return
}
w.config.Mode = Windowed
windows.SetWindowPlacement(w.hwnd, w.placement)
w.placement = nil
style := windows.GetWindowLong(w.hwnd)
case Maximized:
windows.SetWindowText(w.hwnd, w.config.Title)
style := windows.GetWindowLong(w.hwnd) & (^uintptr(windows.WS_MAXIMIZE))
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style|windows.WS_OVERLAPPEDWINDOW)
windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST,
0, 0, 0, 0,
windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED,
)
case Fullscreen:
if w.placement != nil {
return
windows.ShowWindow(w.hwnd, windows.SW_SHOWMAXIMIZED)
case Windowed:
var r windows.Rect
windows.SetWindowText(w.hwnd, w.config.Title)
windows.GetWindowRect(w.hwnd, &r)
style := windows.GetWindowLong(w.hwnd) & (^uintptr(windows.WS_MAXIMIZE))
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style|windows.WS_OVERLAPPEDWINDOW)
width := int32(w.config.Size.X)
height := int32(w.config.Size.Y)
if w.config.center {
// Calculate center position on current monitor
mi := windows.GetMonitorInfo(w.hwnd).Monitor
r.Left = mi.Left + (mi.Right-mi.Left-width)/2
r.Top = mi.Top + (mi.Bottom-mi.Top-height)/2
// Centering is done only once.
w.config.center = false
}
w.config.Mode = Fullscreen
w.placement = windows.GetWindowPlacement(w.hwnd)
windows.SetWindowPos(w.hwnd, 0, r.Left, r.Top, width, height,
windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED)
windows.ShowWindow(w.hwnd, windows.SW_SHOWNORMAL)
case Fullscreen:
style := windows.GetWindowLong(w.hwnd)
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style&^windows.WS_OVERLAPPEDWINDOW)
mi := windows.GetMonitorInfo(w.hwnd)
@@ -633,6 +567,12 @@ func (w *window) SetWindowMode(mode WindowMode) {
mi.Monitor.Bottom-mi.Monitor.Top,
windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED,
)
windows.ShowWindow(w.hwnd, windows.SW_SHOWNORMAL)
}
// A config event is sent to the main event loop whenever the configuration is changed
if oldConfig.Mode != w.config.Mode || oldConfig.Size != w.config.Size {
w.w.Event(ConfigEvent{Config: w.config})
}
}
+11 -18
View File
@@ -321,22 +321,6 @@ func (w *Window) Close() {
})
}
// Maximize the window.
// Note: only implemented on Windows, macOS and X11.
func (w *Window) Maximize() {
w.driverDefer(func(d driver) {
d.Maximize()
})
}
// Center the window.
// Note: only implemented on Windows, macOS and X11.
func (w *Window) Center() {
w.driverDefer(func(d driver) {
d.Center()
})
}
// Run f in the same thread as the native window event loop, and wait for f to
// return or the window to close. Run is guaranteed not to deadlock if it is
// invoked during the handling of a ViewEvent, system.FrameEvent,
@@ -703,8 +687,7 @@ func Title(t string) Option {
}
}
// Size sets the size of the window. The option is ignored
// in Fullscreen mode.
// Size sets the size of the window. The mode will be changed to Windowed.
func Size(w, h unit.Value) Option {
if w.V <= 0 {
panic("width must be larger than or equal to 0")
@@ -713,6 +696,7 @@ func Size(w, h unit.Value) Option {
panic("height must be larger than or equal to 0")
}
return func(m unit.Metric, cnf *Config) {
cnf.Mode = Windowed
cnf.Size = image.Point{
X: m.Px(w),
Y: m.Px(h),
@@ -720,6 +704,15 @@ func Size(w, h unit.Value) Option {
}
}
// Center is an option to center the window on the screen.
// The option is ignored in Fullscreen mode.
func Centered() Option {
return func(m unit.Metric, cnf *Config) {
// Set the flag so the driver can later do the actual centering.
cnf.center = true
}
}
// MaxSize sets the maximum size of the window.
func MaxSize(w, h unit.Value) Option {
if w.V <= 0 {