diff --git a/app/os.go b/app/os.go index 329f86d09..e44b0d514 100644 --- a/app/os.go +++ b/app/os.go @@ -43,6 +43,8 @@ type Config struct { CustomRenderer bool // center is a flag used to center the window. Set by option. center bool + // Decorated reports whether window decorations are provided automatically. + Decorated bool } // ConfigEvent is sent whenever the configuration of a Window changes. @@ -177,6 +179,9 @@ type driver interface { // Wakeup wakes up the event loop and sends a WakeupEvent. Wakeup() + + // Perform actions on the window. + Perform(system.Action) } type windowRendezvous struct { @@ -218,3 +223,12 @@ func newWindowRendezvous() *windowRendezvous { func (wakeupEvent) ImplementsEvent() {} func (ConfigEvent) ImplementsEvent() {} + +func walkActions(actions system.Action, do func(system.Action)) { + for a := system.Action(1); actions != 0; a <<= 1 { + if actions&a != 0 { + actions &^= a + do(a) + } + } +} diff --git a/app/os_android.go b/app/os_android.go index f835cbdb6..f35294222 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -1166,6 +1166,9 @@ func (w *window) Configure(options []Option) { prev := w.config cnf := w.config cnf.apply(unit.Metric{}, options) + // Decorations are never disabled. + cnf.Decorated = true + if prev.Orientation != cnf.Orientation { w.config.Orientation = cnf.Orientation setOrientation(env, w.view, cnf.Orientation) @@ -1188,12 +1191,17 @@ func (w *window) Configure(options []Option) { w.config.Mode = Windowed } } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.callbacks.Event(ConfigEvent{Config: w.config}) } }) } +func (w *window) Perform(system.Action) {} + func (w *window) Raise() {} func (w *window) SetCursor(name pointer.CursorName) { diff --git a/app/os_ios.go b/app/os_ios.go index 3e8c3ba17..34a321e3f 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -98,6 +98,7 @@ type window struct { visible bool cursor pointer.CursorName + config Config pointerMap []C.CFTypeRef } @@ -273,7 +274,19 @@ func (w *window) WriteClipboard(s string) { C.writeClipboard(chars, C.NSUInteger(len(u16))) } -func (w *window) Configure([]Option) {} +func (w *window) Configure([]Option) { + prev := w.config + // Decorations are never disabled. + w.config.Decorated = true + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } + if w.config != prev { + w.w.Event(ConfigEvent{Config: w.config}) + } +} + +func (w *window) Perform(system.Action) {} func (w *window) Raise() {} diff --git a/app/os_js.go b/app/os_js.go index 23e49a993..728ca14a2 100644 --- a/app/os_js.go +++ b/app/os_js.go @@ -513,6 +513,9 @@ func (w *window) Configure(options []Option) { prev := w.config cnf := w.config cnf.apply(unit.Metric{}, options) + // Decorations are never disabled. + cnf.Decorated = true + if prev.Title != cnf.Title { w.config.Title = cnf.Title w.document.Set("title", cnf.Title) @@ -528,11 +531,16 @@ func (w *window) Configure(options []Option) { w.config.Orientation = cnf.Orientation w.orientation(cnf.Orientation) } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } } +func (w *window) Perform(system.Action) {} + func (w *window) Raise() {} func (w *window) SetCursor(name pointer.CursorName) { diff --git a/app/os_macos.go b/app/os_macos.go index 10feab26c..67b17b1e9 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -261,6 +261,8 @@ func (w *window) Configure(options []Option) { cnf.Size = cnf.Size.Div(int(screenScale)) cnf.MinSize = cnf.MinSize.Div(int(screenScale)) cnf.MaxSize = cnf.MaxSize.Div(int(screenScale)) + // Decorations are never disabled. + cnf.Decorated = true switch cnf.Mode { case Fullscreen: @@ -325,6 +327,9 @@ func (w *window) Configure(options []Option) { C.setScreenFrame(w.window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y)) } } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } @@ -339,6 +344,8 @@ func (w *window) setTitle(prev, cnf Config) { } } +func (w *window) Perform(system.Action) {} + func (w *window) SetCursor(name pointer.CursorName) { w.cursor = windowSetCursor(w.cursor, name) } diff --git a/app/os_unix.go b/app/os_unix.go index ee831b308..c834b6e1f 100644 --- a/app/os_unix.go +++ b/app/os_unix.go @@ -29,7 +29,7 @@ var wlDriver, x11Driver windowDriver func newWindow(window *callbacks, options []Option) error { var errFirst error - for _, d := range []windowDriver{x11Driver, wlDriver} { + for _, d := range []windowDriver{wlDriver, x11Driver} { if d == nil { continue } diff --git a/app/os_wayland.c b/app/os_wayland.c index b137e1f9e..a9752aaa0 100644 --- a/app/os_wayland.c +++ b/app/os_wayland.c @@ -6,6 +6,7 @@ #include #include "wayland_xdg_shell.h" +#include "wayland_xdg_decoration.h" #include "wayland_text_input.h" #include "_cgo_export.h" @@ -29,6 +30,10 @@ const struct xdg_toplevel_listener gio_xdg_toplevel_listener = { .close = gio_onToplevelClose, }; +const struct zxdg_toplevel_decoration_v1_listener gio_zxdg_toplevel_decoration_v1_listener = { + .configure = gio_onToplevelDecorationConfigure, +}; + static void xdg_wm_base_handle_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) { xdg_wm_base_pong(wm, serial); } diff --git a/app/os_wayland.go b/app/os_wayland.go index ed103694c..cb22d7db0 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -64,6 +64,7 @@ extern const struct wl_registry_listener gio_registry_listener; extern const struct wl_surface_listener gio_surface_listener; extern const struct xdg_surface_listener gio_xdg_surface_listener; extern const struct xdg_toplevel_listener gio_xdg_toplevel_listener; +extern const struct zxdg_toplevel_decoration_v1_listener gio_zxdg_toplevel_decoration_v1_listener; extern const struct xdg_wm_base_listener gio_xdg_wm_base_listener; extern const struct wl_callback_listener gio_callback_listener; extern const struct wl_output_listener gio_output_listener; @@ -149,6 +150,7 @@ type repeatState struct { type window struct { w *callbacks disp *wlDisplay + seat *wlSeat surf *C.struct_wl_surface wmSurf *C.struct_xdg_surface topLvl *C.struct_xdg_toplevel @@ -188,9 +190,10 @@ type window struct { newScale bool scale int // 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 + size image.Point + config Config + wsize image.Point // window config size before going fullscreen or maximized + inCompositor bool // window is moving or being resized wakeups chan struct{} } @@ -212,7 +215,7 @@ type wlOutput struct { } // callbackMap maps Wayland native handles to corresponding Go -// references. It is necessary because the the Wayland client API +// references. It is necessary because the Wayland client API // forces the use of callbacks and storing pointers to Go values // in C is forbidden. var callbackMap sync.Map @@ -369,9 +372,8 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) { C.xdg_toplevel_add_listener(w.topLvl, &C.gio_xdg_toplevel_listener, unsafe.Pointer(w.surf)) if d.decor != nil { - // Request server side decorations. w.decor = C.zxdg_decoration_manager_v1_get_toplevel_decoration(d.decor, w.topLvl) - C.zxdg_toplevel_decoration_v1_set_mode(w.decor, C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) + C.zxdg_toplevel_decoration_v1_add_listener(w.decor, &C.gio_zxdg_toplevel_decoration_v1_listener, unsafe.Pointer(w.surf)) } w.updateOpaqueRegion() return w, nil @@ -499,6 +501,24 @@ func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, w.size = image.Pt(int(width), int(height)) w.updateOpaqueRegion() } + w.needAck = true +} + +//export gio_onToplevelDecorationConfigure +func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_toplevel_decoration_v1, mode C.uint32_t) { + w := callbackLoad(data).(*window) + decorated := w.config.Decorated + switch mode { + case C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE: + w.config.Decorated = false + case C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: + w.config.Decorated = true + } + if decorated != w.config.Decorated { + w.w.Event(ConfigEvent{Config: w.config}) + } + w.needAck = true + w.draw(true) } //export gio_onOutputMode @@ -772,15 +792,22 @@ func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer, seria s := callbackLoad(data).(*wlSeat) s.serial = serial w := callbackLoad(unsafe.Pointer(surf)).(*window) + w.seat = s s.pointerFocus = w w.setCursor(pointer, serial) w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)} } //export gio_onPointerLeave -func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.uint32_t, surface *C.struct_wl_surface) { +func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.uint32_t, surf *C.struct_wl_surface) { + w := callbackLoad(unsafe.Pointer(surf)).(*window) + w.seat = nil s := callbackLoad(data).(*wlSeat) s.serial = serial + if w.inCompositor { + w.inCompositor = false + w.w.Event(pointer.Event{Type: pointer.Cancel}) + } } //export gio_onPointerMotion @@ -818,6 +845,8 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t, case 0: w.pointerBtns &^= btn typ = pointer.Release + // Move or resize gestures no longer applies. + w.inCompositor = false case 1: w.pointerBtns |= btn typ = pointer.Press @@ -978,6 +1007,9 @@ func (w *window) Configure(options []Option) { C.xdg_toplevel_set_max_size(w.topLvl, C.int32_t(cnf.MaxSize.X), C.int32_t(cnf.MaxSize.Y)) } } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } @@ -992,6 +1024,63 @@ func (w *window) setTitle(prev, cnf Config) { } } +func (w *window) Perform(actions system.Action) { + walkActions(actions, func(action system.Action) { + switch action { + case system.ActionMinimize: + w.Configure([]Option{Minimized.Option()}) + case system.ActionMaximize: + w.Configure([]Option{Maximized.Option()}) + case system.ActionUnmaximize: + w.Configure([]Option{Windowed.Option()}) + case system.ActionClose: + w.Close() + case system.ActionMove: + w.move() + default: + w.resize(action) + } + }) +} + +func (w *window) move() { + if !w.inCompositor && w.seat != nil { + w.inCompositor = true + s := w.seat + C.xdg_toplevel_move(w.topLvl, s.seat, s.serial) + } +} + +func (w *window) resize(a system.Action) { + if w.inCompositor || w.seat == nil { + return + } + var edge int + switch a { + case system.ActionResizeNorth: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_TOP + case system.ActionResizeSouth: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM + case system.ActionResizeEast: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_LEFT + case system.ActionResizeWest: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_RIGHT + case system.ActionResizeNorthWest: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT + case system.ActionResizeNorthEast: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT + case system.ActionResizeSouthEast: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT + case system.ActionResizeSouthWest: + edge = C.XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT + default: + return + } + w.inCompositor = true + s := w.seat + C.xdg_toplevel_resize(w.topLvl, s.seat, s.serial, C.uint32_t(edge)) +} + 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 diff --git a/app/os_windows.go b/app/os_windows.go index 9a9028c77..f453c7d40 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -531,6 +531,8 @@ func (w *window) Configure(options []Option) { metric := configForDPI(dpi) w.config.apply(metric, options) windows.SetWindowText(w.hwnd, w.config.Title) + // Decorations are never disabled. + w.config.Decorated = true switch w.config.Mode { case Minimized: @@ -691,6 +693,8 @@ func (w *window) Close() { windows.PostMessage(w.hwnd, windows.WM_CLOSE, 0, 0) } +func (w *window) Perform(system.Action) {} + func (w *window) Raise() { windows.SetForegroundWindow(w.hwnd) windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST, 0, 0, 0, 0, diff --git a/app/os_x11.go b/app/os_x11.go index c95e5d95e..90e1c3943 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -164,6 +164,8 @@ func (w *x11Window) Configure(options []Option) { prev := w.config cnf := w.config cnf.apply(w.metric, options) + // Decorations are never disabled. + cnf.Decorated = true switch cnf.Mode { case Fullscreen: @@ -245,6 +247,9 @@ func (w *x11Window) Configure(options []Option) { C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y)) } } + if cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated + } if w.config != prev { w.w.Event(ConfigEvent{Config: w.config}) } @@ -268,6 +273,8 @@ func (w *x11Window) setTitle(prev, cnf Config) { } } +func (w *x11Window) Perform(system.Action) {} + func (w *x11Window) Raise() { var xev C.XEvent ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) diff --git a/app/window.go b/app/window.go index d4f450a6d..ebb3a3fa4 100644 --- a/app/window.go +++ b/app/window.go @@ -11,14 +11,18 @@ import ( "time" "gioui.org/f32" + "gioui.org/font/gofont" "gioui.org/gpu" + "gioui.org/internal/ops" "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/io/profile" "gioui.org/io/router" "gioui.org/io/system" + "gioui.org/layout" "gioui.org/op" "gioui.org/unit" + "gioui.org/widget/material" _ "gioui.org/app/internal/log" ) @@ -59,8 +63,13 @@ type Window struct { nextFrame time.Time delayedDraw *time.Timer - queue queue - cursor pointer.CursorName + queue queue + cursor pointer.CursorName + decorations struct { + op.Ops + Config + *material.Decorations + } callbacks callbacks @@ -578,9 +587,16 @@ func (w *Window) processEvent(d driver, e event.Event) { w.hasNextFrame = false e2.Frame = w.update e2.Queue = &w.queue + + // Prepare the decorations and update the frame insets. + wrapper := &w.decorations.Ops + wrapper.Reset() + size := e2.Size // save the initial window size as the decorations will change it. + e2.FrameEvent.Size = w.decorate(d, e2.FrameEvent, wrapper) w.out <- e2.FrameEvent frame, gotFrame := w.waitFrame() - err := w.validateAndProcess(d, e2.Size, e2.Sync, frame) + ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal)) + err := w.validateAndProcess(d, size, e2.Sync, wrapper) if gotFrame { // We're done with frame, let the client continue. w.frameAck <- struct{}{} @@ -606,6 +622,9 @@ func (w *Window) processEvent(d driver, e event.Event) { w.out <- e2 w.waitAck() case wakeupEvent: + case ConfigEvent: + w.decorations.Config = e2.Config + w.out <- e case event.Event: if w.queue.q.Queue(e2) { w.setNextFrame(time.Time{}) @@ -664,6 +683,59 @@ func (w *Window) updateCursor(d driver) { } } +// decorate the window if enabled and returns the corresponding Insets. +func (w *Window) decorate(d driver, e system.FrameEvent, o *op.Ops) image.Point { + if w.decorations.Config.Decorated || w.decorations.Config.Mode == Fullscreen { + return e.Size + } + deco := w.decorations.Decorations + if deco == nil { + theme := material.NewTheme(gofont.Collection()) + allActions := system.ActionMinimize | system.ActionMaximize | system.ActionUnmaximize | + system.ActionClose | system.ActionMove | + system.ActionResizeNorth | system.ActionResizeSouth | + system.ActionResizeWest | system.ActionResizeEast | + system.ActionResizeNorthWest | system.ActionResizeSouthWest | + system.ActionResizeNorthEast | system.ActionResizeSouthEast + deco = &material.Decorations{ + DecorationsStyle: material.Decorate(theme, allActions), + } + w.decorations.Decorations = deco + } + // Update the decorations based on the current window mode. + var actions system.Action + switch m := w.decorations.Config.Mode; m { + case Windowed: + actions |= system.ActionUnmaximize + case Minimized: + actions |= system.ActionMinimize + case Maximized: + actions |= system.ActionMaximize + case Fullscreen: + actions |= system.ActionFullscreen + default: + panic(fmt.Errorf("unknown WindowMode %v", m)) + } + deco.Perform(actions) + // Update the window based on the actions on the decorations. + d.Perform(deco.Actions()) + + gtx := layout.Context{ + Ops: o, + Now: e.Now, + Queue: e.Queue, + Metric: e.Metric, + Constraints: layout.Exact(e.Size), + } + rec := op.Record(o) + dims := deco.Decorate(gtx, w.decorations.Config.Title) + op.Defer(o, rec.Stop()) + // Offset to place the frame content below the decorations. + size := image.Point{Y: dims.Size.Y} + op.Offset(f32.Point{Y: float32(size.Y)}).Add(o) + return e.Size.Sub(size) +} + // Raise requests that the platform bring this window to the top of all open windows. // Some platforms do not allow this except under certain circumstances, such as when // a window from the same application already has focus. If the platform does not @@ -764,3 +836,11 @@ func CustomRenderer(custom bool) Option { cnf.CustomRenderer = custom } } + +// Decorated controls whether automatic window decorations +// are enabled. +func Decorated(enabled bool) Option { + return func(_ unit.Metric, cnf *Config) { + cnf.Decorated = enabled + } +} diff --git a/widget/material/decorations.go b/widget/material/decorations.go index 92f0861aa..edf393b42 100644 --- a/widget/material/decorations.go +++ b/widget/material/decorations.go @@ -57,7 +57,7 @@ type Decorations struct { // Decorate a window with the title and actions defined in DecorationsStyle. // The space used by the decorations is returned as an inset for the window // content. -func (d *Decorations) Decorate(gtx layout.Context, title string) layout.Inset { +func (d *Decorations) Decorate(gtx layout.Context, title string) layout.Dimensions { rec := op.Record(gtx.Ops) dims := d.layoutDecorations(gtx, title) decos := rec.Stop() @@ -65,9 +65,7 @@ func (d *Decorations) Decorate(gtx layout.Context, title string) layout.Inset { paint.FillShape(gtx.Ops, d.DecorationsStyle.Background, r.Op()) decos.Add(gtx.Ops) d.layoutResizing(gtx) - return layout.Inset{ - Top: unit.Px(float32(dims.Size.Y)), - } + return dims } func (d *Decorations) layoutResizing(gtx layout.Context) {