diff --git a/app/os_macos.go b/app/os_macos.go index 3cbbcc3c..10feab26 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -129,6 +129,16 @@ static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, [window setFrame:r display:YES]; } +static void hideWindow(CFTypeRef windowRef) { + NSWindow* window = (__bridge NSWindow *)windowRef; + [window miniaturize:window]; +} + +static void unhideWindow(CFTypeRef windowRef) { + NSWindow* window = (__bridge NSWindow *)windowRef; + [window deminiaturize:window]; +} + static NSRect getScreenFrame(CFTypeRef windowRef) { NSWindow* window = (__bridge NSWindow *)windowRef; return [[window screen] frame]; @@ -252,35 +262,81 @@ func (w *window) Configure(options []Option) { cnf.MinSize = cnf.MinSize.Div(int(screenScale)) cnf.MaxSize = cnf.MaxSize.Div(int(screenScale)) - if cnf.Mode != Fullscreen && prev.Size != cnf.Size { - w.config.Size = cnf.Size - C.setSize(w.window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y)) + switch cnf.Mode { + case Fullscreen: + switch prev.Mode { + case Fullscreen: + case Minimized: + C.unhideWindow(w.window) + fallthrough + default: + w.config.Mode = Fullscreen + C.toggleFullScreen(w.window) + } + case Minimized: + switch prev.Mode { + case Minimized, Fullscreen: + default: + w.config.Mode = Minimized + C.hideWindow(w.window) + } + case Maximized: + switch prev.Mode { + case Fullscreen: + case Minimized: + C.unhideWindow(w.window) + fallthrough + default: + w.config.Mode = Maximized + r := C.getScreenFrame(w.window) // the screen size of the window + C.setScreenFrame(w.window, C.CGFloat(0), C.CGFloat(0), r.size.width, r.size.height) + w.config.Size = image.Pt(int(r.size.width), int(r.size.height)) + w.setTitle(prev, cnf) + } + case Windowed: + switch prev.Mode { + case Fullscreen: + w.config.Mode = Windowed + C.toggleFullScreen(w.window) + case Minimized: + w.config.Mode = Windowed + C.unhideWindow(w.window) + case Maximized: + w.config.Mode = Windowed + } + w.setTitle(prev, cnf) + if prev.Size != cnf.Size { + w.config.Size = cnf.Size + C.setSize(w.window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y)) + } + if prev.MinSize != cnf.MinSize { + w.config.MinSize = cnf.MinSize + C.setMinSize(w.window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y)) + } + if prev.MaxSize != cnf.MaxSize { + w.config.MaxSize = cnf.MaxSize + C.setMaxSize(w.window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y)) + } + if cnf.center { + r := C.getScreenFrame(w.window) // the screen size of the window + sz := w.config.Size + x := (int(r.size.width) - sz.X) / 2 + y := (int(r.size.height) - sz.Y) / 2 + C.setScreenFrame(w.window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y)) + } } - if prev.MinSize != cnf.MinSize { - w.config.MinSize = cnf.MinSize - C.setMinSize(w.window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y)) - } - if prev.MaxSize != cnf.MaxSize { - w.config.MaxSize = cnf.MaxSize - C.setMaxSize(w.window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y)) + if w.config != prev { + w.w.Event(ConfigEvent{Config: w.config}) } +} +func (w *window) setTitle(prev, cnf Config) { if prev.Title != cnf.Title { w.config.Title = cnf.Title title := C.CString(cnf.Title) defer C.free(unsafe.Pointer(title)) C.setTitle(w.window, title) } - if prev.Mode != cnf.Mode { - switch cnf.Mode { - case Windowed, Fullscreen: - w.config.Mode = cnf.Mode - C.toggleFullScreen(w.window) - } - } - if w.config != prev { - w.w.Event(ConfigEvent{Config: w.config}) - } } func (w *window) SetCursor(name pointer.CursorName) { @@ -317,24 +373,6 @@ func (w *window) Close() { C.closeWindow(w.window) } -// Maximize the window. -func (w *window) Maximize() { - r := C.getScreenFrame(w.window) // the screen size of the window - C.setScreenFrame(w.window, C.CGFloat(0), C.CGFloat(0), r.size.width, r.size.height) -} - -// Center the window. -func (w *window) Center() { - r := C.getScreenFrame(w.window) // the screen size of the window - - screenScale := float32(C.getScreenBackingScale()) - sz := w.config.Size.Div(int(screenScale)) - x := (int(r.size.width) - sz.X) / 2 - y := (int(r.size.height) - sz.Y) / 2 - - C.setScreenFrame(w.window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y)) -} - func (w *window) setStage(stage system.Stage) { if stage == w.stage { return diff --git a/app/os_wayland.go b/app/os_wayland.go index c715e854..0424b75f 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -190,6 +190,7 @@ type window struct { // size is the unscaled window size (unlike config.Size which is scaled). size image.Point config Config + wsize image.Point // window config size before going fullscreen wakeups chan struct{} } @@ -919,22 +920,77 @@ func (w *window) Configure(options []Option) { prev := w.config cnf := w.config cnf.apply(cfg, options) - if prev.Size != cnf.Size { - w.size = image.Pt(cnf.Size.X/w.scale, cnf.Size.Y/w.scale) - w.config.Size = cnf.Size - } - if prev.Title != cnf.Title { - w.config.Title = cnf.Title - title := C.CString(cnf.Title) - C.xdg_toplevel_set_title(w.topLvl, title) - C.free(unsafe.Pointer(title)) + + switch cnf.Mode { + case Fullscreen: + switch prev.Mode { + case Minimized, Fullscreen: + default: + w.config.Mode = Fullscreen + w.wsize = w.config.Size + C.xdg_toplevel_set_fullscreen(w.topLvl, nil) + } + case Minimized: + switch prev.Mode { + case Minimized, Fullscreen: + default: + w.config.Mode = Minimized + C.xdg_toplevel_set_minimized(w.topLvl) + } + case Maximized: + switch prev.Mode { + case Minimized, Fullscreen: + default: + w.config.Mode = Maximized + w.wsize = w.config.Size + C.xdg_toplevel_set_maximized(w.topLvl) + } + case Windowed: + switch prev.Mode { + case Fullscreen: + w.config.Mode = Windowed + w.size = w.wsize.Div(w.scale) + C.xdg_toplevel_unset_fullscreen(w.topLvl) + case Minimized: + w.config.Mode = Windowed + w.config.Size = w.wsize + case Maximized: + w.config.Mode = Windowed + w.size = w.wsize.Div(w.scale) + C.xdg_toplevel_unset_maximized(w.topLvl) + } + w.setTitle(prev, cnf) + if prev.Size != cnf.Size { + w.config.Size = cnf.Size + w.size = cnf.Size.Div(w.scale) + } + if prev.MinSize != cnf.MinSize { + w.config.MinSize = cnf.MinSize + C.xdg_toplevel_set_min_size(w.topLvl, C.int32_t(cnf.MinSize.X), C.int32_t(cnf.MinSize.Y)) + } + if prev.MaxSize != cnf.MaxSize { + w.config.MaxSize = cnf.MaxSize + C.xdg_toplevel_set_max_size(w.topLvl, C.int32_t(cnf.MaxSize.X), C.int32_t(cnf.MaxSize.Y)) + } } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } } -func (w *window) Raise() {} +func (w *window) setTitle(prev, cnf Config) { + if prev.Title != cnf.Title { + w.config.Title = cnf.Title + title := C.CString(cnf.Title) + C.xdg_toplevel_set_title(w.topLvl, title) + C.free(unsafe.Pointer(title)) + } +} + +func (w *window) Raise() { + // NB. there is no way for a minimized window to be unminimized. + // https://wayland.app/protocols/xdg-shell#xdg_toplevel:request:set_minimized +} func (w *window) SetCursor(name pointer.CursorName) { ptr := w.disp.seat.pointer diff --git a/app/os_windows.go b/app/os_windows.go index 42594bd2..b6da2af6 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -571,7 +571,7 @@ func (w *window) Configure(options []Option) { } // 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 { + if oldConfig != w.config { w.w.Event(ConfigEvent{Config: w.config}) } } diff --git a/app/os_x11.go b/app/os_x11.go index 1c752473..939b3bec 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -160,27 +160,93 @@ func (w *x11Window) Configure(options []Option) { prev := w.config cnf := w.config cnf.apply(w.metric, options) - if prev.MinSize != cnf.MinSize { - w.config.MinSize = cnf.MinSize - shints.min_width = C.int(cnf.MinSize.X) - shints.min_height = C.int(cnf.MinSize.Y) - shints.flags = C.PMinSize - } - if prev.MaxSize != cnf.MaxSize { - w.config.MaxSize = cnf.MaxSize - shints.max_width = C.int(cnf.MaxSize.X) - shints.max_height = C.int(cnf.MaxSize.Y) - shints.flags = shints.flags | C.PMaxSize - } - if shints.flags != 0 { - C.XSetWMNormalHints(w.x, w.xw, &shints) - } - if cnf.Mode != Fullscreen && prev.Size != cnf.Size { - w.config.Size = cnf.Size - C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y)) - } + switch cnf.Mode { + case Fullscreen: + switch prev.Mode { + case Fullscreen: + case Minimized: + w.Raise() + fallthrough + default: + w.config.Mode = Fullscreen + w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateFullscreen, 0) + } + case Minimized: + switch prev.Mode { + case Minimized, Fullscreen: + default: + w.config.Mode = Minimized + screen := C.XDefaultScreen(w.x) + C.XIconifyWindow(w.x, w.xw, screen) + } + case Maximized: + switch prev.Mode { + case Fullscreen: + case Minimized: + w.Raise() + fallthrough + default: + w.config.Mode = Maximized + w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert) + w.setTitle(prev, cnf) + } + case Windowed: + switch prev.Mode { + case Fullscreen: + w.config.Mode = Windowed + w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateFullscreen, 0) + C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y)) + case Minimized: + w.config.Mode = Windowed + w.Raise() + case Maximized: + w.config.Mode = Windowed + w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert) + } + w.setTitle(prev, cnf) + if prev.Size != cnf.Size { + w.config.Size = cnf.Size + C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y)) + } + if prev.MinSize != cnf.MinSize { + w.config.MinSize = cnf.MinSize + shints.min_width = C.int(cnf.MinSize.X) + shints.min_height = C.int(cnf.MinSize.Y) + shints.flags = C.PMinSize + } + if prev.MaxSize != cnf.MaxSize { + w.config.MaxSize = cnf.MaxSize + shints.max_width = C.int(cnf.MaxSize.X) + shints.max_height = C.int(cnf.MaxSize.Y) + shints.flags = shints.flags | C.PMaxSize + } + if shints.flags != 0 { + C.XSetWMNormalHints(w.x, w.xw, &shints) + } + if cnf.center { + screen := C.XDefaultScreen(w.x) + width := C.XDisplayWidth(w.x, screen) + height := C.XDisplayHeight(w.x, screen) + var attrs C.XWindowAttributes + C.XGetWindowAttributes(w.x, w.xw, &attrs) + width -= attrs.border_width + height -= attrs.border_width + + sz := w.config.Size + x := (int(width) - sz.X) / 2 + y := (int(height) - sz.Y) / 2 + + C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y)) + } + } + if w.config != prev { + w.w.Event(ConfigEvent{Config: w.config}) + } +} + +func (w *x11Window) setTitle(prev, cnf Config) { if prev.Title != cnf.Title { title := cnf.Title ctitle := C.CString(title) @@ -196,13 +262,6 @@ func (w *x11Window) Configure(options []Option) { }, w.atoms.wmName) } - - if prev.Mode != cnf.Mode { - w.SetWindowMode(cnf.Mode) - } - if w.config != prev { - w.w.Event(ConfigEvent{Config: w.config}) - } } func (w *x11Window) Raise() { @@ -249,22 +308,6 @@ func (w *x11Window) SetCursor(name pointer.CursorName) { C.XDefineCursor(w.x, w.xw, c) } -func (w *x11Window) SetWindowMode(mode WindowMode) { - var action C.long - switch mode { - case Windowed: - action = _NET_WM_STATE_REMOVE - case Fullscreen: - action = _NET_WM_STATE_ADD - default: - return - } - w.config.Mode = mode - // "A Client wishing to change the state of a window MUST send - // a _NET_WM_STATE client message to the root window." - w.sendWMStateEvent(action, w.atoms.wmStateFullscreen, 0) -} - func (w *x11Window) ShowTextInput(show bool) {} func (w *x11Window) SetInputHint(_ key.InputHint) {} @@ -286,29 +329,6 @@ func (w *x11Window) Close() { C.XSendEvent(w.x, w.xw, C.False, C.NoEventMask, &xev) } -// Maximize the window. -func (w *x11Window) Maximize() { - w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert) -} - -// Center the window. -func (w *x11Window) Center() { - screen := C.XDefaultScreen(w.x) - width := C.XDisplayWidth(w.x, screen) - height := C.XDisplayHeight(w.x, screen) - - var attrs C.XWindowAttributes - C.XGetWindowAttributes(w.x, w.xw, &attrs) - width -= attrs.border_width - height -= attrs.border_width - - sz := w.config.Size - x := (int(width) - sz.X) / 2 - y := (int(height) - sz.Y) / 2 - - C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y)) -} - // action is one of _NET_WM_STATE_REMOVE, _NET_WM_STATE_ADD. func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) { var xev C.XEvent diff --git a/app/window.go b/app/window.go index d0764a0c..b853a8cd 100644 --- a/app/window.go +++ b/app/window.go @@ -704,8 +704,8 @@ 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. +// Centered is an option to center the window on the screen. +// The option is ignored in Fullscreen mode and on Wayland. func Centered() Option { return func(m unit.Metric, cnf *Config) { // Set the flag so the driver can later do the actual centering.