~pierrec: 1 widget/material: add Decorations app: use material.Decorations as default 15 files changed, 736 insertions(+), 17 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/28454/mbox | git am -3Learn more about email & git
From: Pierre Curto <pierre.curto@gmail.com> This patch implements a mechanism for customizing window decorations. If a window is configured with app.Decorated(true), then the decorations provided by the app.Decorate(app.Decorations) option are applied. Custom decorations can be provided but by default it uses the material.Decorations ones. Decorations are automatically applied on Wayland if the Wayland server does not provide window decorations. Signed-off-by: Pierre Curto <pierre.curto@gmail.com> --- app/os.go | 63 ++++++- app/os_android.go | 12 ++ app/os_ios.go | 12 ++ app/os_js.go | 12 ++ app/os_macos.go | 12 ++ app/os_wayland.go | 162 ++++++++++++++++-- app/os_windows.go | 13 +- app/os_x11.go | 12 ++ app/window.go | 47 +++++- internal/ops/ops.go | 7 + io/pointer/pointer.go | 9 + io/system/decoration.go | 95 +++++++++++ layout/context.go | 4 +- op/op.go | 4 + widget/material/decorations.go | 289 +++++++++++++++++++++++++++++++++ 15 files changed, 736 insertions(+), 17 deletions(-) create mode 100644 io/system/decoration.go create mode 100644 widget/material/decorations.go diff --git a/app/os.go b/app/os.go index 329f86d0..6b8c7476 100644 --- a/app/os.go +++ b/app/os.go @@ -7,11 +7,11 @@ import ( "image" "image/color" - "gioui.org/io/key" - "gioui.org/gpu" + "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/system" + "gioui.org/op" "gioui.org/unit" ) @@ -43,6 +43,9 @@ 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 active. + Decorated bool + decorations Decorations } // ConfigEvent is sent whenever the configuration of a Window changes. @@ -177,6 +180,62 @@ type driver interface { // Wakeup wakes up the event loop and sends a WakeupEvent. Wakeup() + + // Decorated reports whether decorations are active. + Decorated() bool + // Decorate the window. + Decorate() decorations + // Move initiates a window move gesture, usually in response to + // a mouse button press on a custom window title. + Move() + // Resize initiates a window resize gesture, usually in response to + // a mouse drag on window borders or corners. + Resize(system.Action) +} + +// Decorations applied to a window. +type Decorations interface { + Decorate(e system.FrameEvent, ops *op.Ops, title string) system.Insets + Actioned() system.Action +} + +type decorations struct { + Decorations + title string +} + +var allActions system.Action + +func init() { + for a := range actions { + allActions |= a + } +} + +var actions = map[system.Action]func(driver, system.Action){ + system.ActionMinimize: optionAction(Minimized.Option()), + system.ActionMaximize: optionAction(Maximized.Option()), + system.ActionUnmaximize: optionAction(Windowed.Option()), + system.ActionClose: func(d driver, a system.Action) { d.Close() }, + system.ActionMove: func(d driver, a system.Action) { d.Move() }, + system.ActionResizeNorth: resizeAction, + system.ActionResizeSouth: resizeAction, + system.ActionResizeWest: resizeAction, + system.ActionResizeEast: resizeAction, + system.ActionResizeNorthWest: resizeAction, + system.ActionResizeSouthWest: resizeAction, + system.ActionResizeNorthEast: resizeAction, + system.ActionResizeSouthEast: resizeAction, +} + +func optionAction(option Option) func(d driver, a system.Action) { + return func(d driver, a system.Action) { + d.Configure([]Option{option}) + } +} + +func resizeAction(d driver, a system.Action) { + d.Resize(a) } type windowRendezvous struct { diff --git a/app/os_android.go b/app/os_android.go index 5540a6c3..901a2bc4 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -1190,6 +1190,18 @@ func (w *window) Configure(options []Option) { }) } +func (w *window) Decorated() bool { + return false +} + +func (w *window) Decorate() decorations { + return decorations{nil, ""} +} + +func (w *window) Move() {} + +func (w *window) Resize(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 df45fde9..f512018e 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -275,6 +275,18 @@ func (w *window) WriteClipboard(s string) { func (w *window) Configure([]Option) {} +func (w *window) Decorated() bool { + return false +} + +func (w *window) Decorate() decorations { + return decorations{nil, ""} +} + +func (w *window) Move() {} + +func (w *window) Resize(system.Action) {} + func (w *window) Raise() {} func (w *window) SetAnimating(anim bool) { diff --git a/app/os_js.go b/app/os_js.go index 55ec2694..d05c5aa6 100644 --- a/app/os_js.go +++ b/app/os_js.go @@ -533,6 +533,18 @@ func (w *window) Configure(options []Option) { } } +func (w *window) Decorated() bool { + return false +} + +func (w *window) Decorate() decorations { + return decorations{nil, ""} +} + +func (w *window) Move() {} + +func (w *window) Resize(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 3cbbcc3c..c3f1a34a 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -283,6 +283,18 @@ func (w *window) Configure(options []Option) { } } +func (w *window) Decorated() bool { + return false +} + +func (w *window) Decorate() decorations { + return decorations{nil, ""} +} + +func (w *window) Move() {} + +func (w *window) Resize(system.Action) {} + func (w *window) SetCursor(name pointer.CursorName) { w.cursor = windowSetCursor(w.cursor, name) } diff --git a/app/os_wayland.go b/app/os_wayland.go index 157e5aba..99b34fe1 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -149,6 +149,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,8 +189,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 + 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{} } @@ -211,7 +214,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 @@ -240,6 +243,9 @@ func newWLWindow(callbacks *callbacks, options []Option) error { return err } w.w = callbacks + if w.decor == nil { + options = append(options, Decorated(true)) + } go func() { defer d.destroy() defer w.destroy() @@ -496,9 +502,11 @@ func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, w := callbackLoad(data).(*window) if width != 0 && height != 0 { w.size = image.Pt(int(width), int(height)) - w.config.Size = w.size w.updateOpaqueRegion() } + //TODO need to send an ack right? + //https://wayland.app/protocols/xdg-shell#xdg_toplevel:event:configure + C.xdg_surface_ack_configure(w.wmSurf, w.serial) } //export gio_onOutputMode @@ -552,6 +560,9 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output * conf.windows = append(conf.windows, w) } w.updateOutputs() + if w.config.Mode == Minimized { + w.Configure([]Option{Windowed.Option()}) + } } //export gio_onSurfaceLeave @@ -767,15 +778,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 @@ -813,6 +831,7 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t, case 0: w.pointerBtns &^= btn typ = pointer.Release + w.inCompositor = false case 1: w.pointerBtns |= btn typ = pointer.Press @@ -920,22 +939,127 @@ 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 + + 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) + w.setTitle(prev, cnf) + } + 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 + 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 cnf.Decorated != prev.Decorated { + w.config.Decorated = cnf.Decorated } + if cnf.decorations != prev.decorations { + w.config.decorations = cnf.decorations + } + 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) C.xdg_toplevel_set_title(w.topLvl, title) C.free(unsafe.Pointer(title)) } - if w.config != prev { - w.w.Event(ConfigEvent{Config: w.config}) +} + +func (w *window) Decorated() bool { + return w.config.Decorated +} + +func (w *window) Decorate() decorations { + return decorations{w.config.decorations, w.config.Title} +} + +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) Raise() {} +func (w *window) Resize(a system.Action) { + if w.inCompositor || w.seat == nil { + return + } + var edge int + switch a { + case system.ActionResizeNorth: // top + edge = 1 + case system.ActionResizeSouth: // bottom + edge = 2 + case system.ActionResizeEast: // left + edge = 4 + case system.ActionResizeWest: // right + edge = 8 + case system.ActionResizeNorthWest: // top_left + edge = 5 + case system.ActionResizeNorthEast: // bottom_left + edge = 6 + case system.ActionResizeSouthEast: // bottom_right + edge = 10 + case system.ActionResizeSouthWest: // top_right + edge = 9 + } + 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 +} func (w *window) SetCursor(name pointer.CursorName) { ptr := w.disp.seat.pointer @@ -963,6 +1087,22 @@ func (w *window) SetCursor(name pointer.CursorName) { name = "left_side" case pointer.CursorGrab: name = "hand1" + case pointer.CursorTopLeftResize: + name = "top_left_corner" + case pointer.CursorTopRightResize: + name = "bottom_left_corner" + case pointer.CursorBottomLeftResize: + name = "top_right_corner" + case pointer.CursorBottomRightResize: + name = "bottom_right_corner" + case pointer.CursorLeftResize: + name = "right_side" + case pointer.CursorRightResize: + name = "left_side" + case pointer.CursorTopResize: + name = "top_side" + case pointer.CursorBottomResize: + name = "bottom_side" } cname := C.CString(string(name)) defer C.free(unsafe.Pointer(cname)) diff --git a/app/os_windows.go b/app/os_windows.go index 42594bd2..7ebbb8bc 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -569,13 +569,24 @@ func (w *window) Configure(options []Option) { ) 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}) } } +func (w *window) Decorated() bool { + return false +} + +func (w *window) Decorate() decorations { + return decorations{nil, ""} +} + +func (w *window) Move() {} + +func (w *window) Resize(system.Action) {} + func (w *window) WriteClipboard(s string) { w.writeClipboard(s) } diff --git a/app/os_x11.go b/app/os_x11.go index 1c752473..35918a2a 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -205,6 +205,18 @@ func (w *x11Window) Configure(options []Option) { } } +func (w *x11Window) Decorated() bool { + return w.config.Decorated +} + +func (w *x11Window) Decorate() decorations { + return decorations{nil, ""} +} + +func (w *x11Window) Move() {} + +func (w *x11Window) Resize(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 7433ba3f..66c58b6f 100644 --- a/app/window.go +++ b/app/window.go @@ -11,6 +11,7 @@ import ( "time" "gioui.org/f32" + "gioui.org/font/gofont" "gioui.org/gpu" "gioui.org/io/event" "gioui.org/io/pointer" @@ -19,6 +20,7 @@ import ( "gioui.org/io/system" "gioui.org/op" "gioui.org/unit" + "gioui.org/widget/material" _ "gioui.org/app/internal/log" ) @@ -59,8 +61,9 @@ type Window struct { nextFrame time.Time delayedDraw *time.Timer - queue queue - cursor pointer.CursorName + queue queue + cursor pointer.CursorName + decoOps op.Ops callbacks callbacks @@ -105,9 +108,14 @@ var ackEvent event.Event // Calling NewWindow more than once is not supported on // iOS, Android, WebAssembly. func NewWindow(options ...Option) *Window { + theme := material.NewTheme(gofont.Collection()) + deco := &material.Decorations{ + DecorationsStyle: material.Decorate(theme, allActions), + } defaultOptions := []Option{ Size(unit.Dp(800), unit.Dp(600)), Title("Gio"), + Decorate(deco), } options = append(defaultOptions, options...) var cnf Config @@ -574,8 +582,10 @@ func (w *Window) processEvent(d driver, e event.Event) { w.hasNextFrame = false e2.Frame = w.update e2.Queue = &w.queue + deco := w.decorate(d, e2.FrameEvent) w.out <- e2.FrameEvent frame, gotFrame := w.waitFrame() + deco.Add(frame) err := w.validateAndProcess(d, e2.Size, e2.Sync, frame) if gotFrame { // We're done with frame, let the client continue. @@ -660,6 +670,25 @@ func (w *Window) updateCursor(d driver) { } } +func (w *Window) decorate(d driver, e system.FrameEvent) op.CallOp { + deco := d.Decorate() + if deco.Decorations == nil { + return op.CallOp{} + } + o := &w.decoOps + o.Reset() + rec := op.Record(o) + e.Insets = deco.Decorate(e, o, deco.title) + actioned := deco.Actioned() + for a := system.Action(1); actioned != 0; a <<= 1 { + if actioned&a != 0 { + actions[a](d, a) + actioned &^= a + } + } + return rec.Stop() +} + // 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 @@ -760,3 +789,17 @@ func CustomRenderer(custom bool) Option { cnf.CustomRenderer = custom } } + +// Decorated turns window decorations on. +func Decorated(flag bool) Option { + return func(_ unit.Metric, cnf *Config) { + cnf.Decorated = flag + } +} + +// Decorate a window. +func Decorate(deco Decorations) Option { + return func(_ unit.Metric, cnf *Config) { + cnf.decorations = deco + } +} diff --git a/internal/ops/ops.go b/internal/ops/ops.go index ce076947..af6cfc45 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -25,6 +25,7 @@ type Ops struct { // multipOp indicates a multi-op such as clip.Path is being added. multipOp bool + recording bool macroStack stack stacks [5]stack } @@ -224,11 +225,17 @@ func WriteMulti(o *Ops, n int) []byte { } func PushMacro(o *Ops) StackID { + o.recording = true return o.macroStack.push() } func PopMacro(o *Ops, id StackID) { o.macroStack.pop(id) + o.recording = false +} + +func Recording(o *Ops) bool { + return o.recording } func FillMacro(o *Ops, startPC PC) { diff --git a/io/pointer/pointer.go b/io/pointer/pointer.go index 97987f7c..b6ce9eb9 100644 --- a/io/pointer/pointer.go +++ b/io/pointer/pointer.go @@ -110,6 +110,15 @@ const ( CursorGrab CursorName = "grab" // CursorNone hides the cursor. To show it again, use any other cursor. CursorNone CursorName = "none" + + CursorTopLeftResize = "top-left-resize" + CursorTopRightResize = "top-right-resize" + CursorBottomLeftResize = "bottom-left-resize" + CursorBottomRightResize = "bottom-right-resize" + CursorLeftResize = "left-resize" + CursorRightResize = "right-resize" + CursorTopResize = "top-resize" + CursorBottomResize = "bottom-resize" ) const ( diff --git a/io/system/decoration.go b/io/system/decoration.go new file mode 100644 index 00000000..71196c2d --- /dev/null +++ b/io/system/decoration.go @@ -0,0 +1,95 @@ +package system + +import ( + "strings" + + "gioui.org/io/pointer" +) + +// Action is a set of window decoration actions. +type Action uint + +const ( + ActionMinimize Action = 1 << iota + ActionMaximize + ActionUnmaximize + ActionClose + ActionMove + ActionResizeNorth + ActionResizeSouth + ActionResizeWest + ActionResizeEast + ActionResizeNorthWest + ActionResizeSouthWest + ActionResizeNorthEast + ActionResizeSouthEast +) + +func (a Action) CursorName() pointer.CursorName { + var name pointer.CursorName + switch a { + case ActionResizeNorthWest: + name = pointer.CursorTopLeftResize + case ActionResizeSouthEast: + name = pointer.CursorBottomRightResize + case ActionResizeNorthEast: + name = pointer.CursorTopRightResize + case ActionResizeSouthWest: + name = pointer.CursorBottomLeftResize + case ActionResizeWest: + name = pointer.CursorLeftResize + case ActionResizeEast: + name = pointer.CursorRightResize + case ActionResizeNorth: + name = pointer.CursorTopResize + case ActionResizeSouth: + name = pointer.CursorBottomResize + } + return name +} + +func (a Action) String() string { + var buf strings.Builder + for b := Action(1); a != 0; b <<= 1 { + if a&b != 0 { + if buf.Len() > 0 { + buf.WriteByte('|') + } + buf.WriteString(b.string()) + a &^= b + } + } + return buf.String() +} + +func (a Action) string() string { + switch a { + case ActionMinimize: + return "ActionMinimize" + case ActionMaximize: + return "ActionMaximize" + case ActionUnmaximize: + return "ActionUnmaximize" + case ActionClose: + return "ActionClose" + case ActionMove: + return "ActionMove" + case ActionResizeNorth: + return "ActionResizeNorth" + case ActionResizeSouth: + return "ActionResizeSouth" + case ActionResizeWest: + return "ActionResizeWest" + case ActionResizeEast: + return "ActionResizeEast" + case ActionResizeNorthWest: + return "ActionResizeNorthWest" + case ActionResizeSouthWest: + return "ActionResizeSouthWest" + case ActionResizeNorthEast: + return "ActionResizeNorthEast" + case ActionResizeSouthEast: + return "ActionResizeSouthEast" + } + return "" +} diff --git a/layout/context.go b/layout/context.go index 5d314961..2f39df2d 100644 --- a/layout/context.go +++ b/layout/context.go @@ -42,7 +42,9 @@ type Context struct { // // NewContext calls ops.Reset and adjusts ops for e.Insets. func NewContext(ops *op.Ops, e system.FrameEvent) Context { - ops.Reset() + if !ops.Recording() { + ops.Reset() + } size := e.Size diff --git a/op/op.go b/op/op.go index f0b2620c..c4b4a9af 100644 --- a/op/op.go +++ b/op/op.go @@ -145,6 +145,10 @@ func (o *Ops) Reset() { ops.Reset(&o.Internal) } +func (o *Ops) Recording() bool { + return ops.Recording(&o.Internal) +} + // Record a macro of operations. func Record(o *Ops) MacroOp { m := MacroOp{ diff --git a/widget/material/decorations.go b/widget/material/decorations.go new file mode 100644 index 00000000..64ecd5f8 --- /dev/null +++ b/widget/material/decorations.go @@ -0,0 +1,289 @@ +package material + +import ( + "image" + "image/color" + "math/bits" + + "gioui.org/f32" + "gioui.org/gesture" + "gioui.org/io/pointer" + "gioui.org/io/system" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" +) + +// DecorationsStyle provides the style elements for Decorations. +type DecorationsStyle struct { + Actions system.Action + Title LabelStyle + Background color.NRGBA + Foreground color.NRGBA +} + +// Decorate a window. +func Decorate(th *Theme, actions system.Action) DecorationsStyle { + titleStyle := Body1(th, "") + titleStyle.Color = th.Palette.ContrastFg + return DecorationsStyle{ + Actions: actions, + Title: titleStyle, + Background: th.Palette.ContrastBg, + Foreground: th.Palette.ContrastFg, + } +} + +// Decorations provides window decorations. +type Decorations struct { + DecorationsStyle + actions struct { + layout.List + clicks []widget.Clickable + move gesture.Drag + resize [8]struct { + gesture.Hover + gesture.Drag + } + } + actioned system.Action + path clip.Path + maximized bool +} + +func (d *Decorations) Decorate(e system.FrameEvent, ops *op.Ops, title string) system.Insets { + gtx := layout.NewContext(ops, e) + + rec := op.Record(gtx.Ops) + dims := d.layoutDecorations(gtx, title) + decos := rec.Stop() + r := clip.Rect{Max: dims.Size} + paint.FillShape(gtx.Ops, d.DecorationsStyle.Background, r.Op()) + decos.Add(gtx.Ops) + d.layoutResizing(gtx) + + return system.Insets{ + Top: unit.Px(float32(dims.Size.Y)), + Bottom: unit.Value{}, + Left: unit.Value{}, + Right: unit.Value{}, + } +} + +func (d *Decorations) layoutResizing(gtx layout.Context) { + cs := gtx.Constraints.Min + wh := gtx.Px(unit.Dp(10)) + for i, data := range []struct { + system.Action + image.Rectangle + }{ + {system.ActionResizeNorth, image.Rect(0, 0, cs.X, wh)}, + {system.ActionResizeSouth, image.Rect(0, cs.Y-wh, cs.X, cs.Y)}, + {system.ActionResizeWest, image.Rect(cs.X-wh, 0, cs.X, cs.Y)}, + {system.ActionResizeEast, image.Rect(0, 0, wh, cs.Y)}, + {system.ActionResizeNorthWest, image.Rect(0, 0, wh, wh)}, + {system.ActionResizeSouthWest, image.Rect(cs.X-wh, 0, cs.X, wh)}, + {system.ActionResizeNorthEast, image.Rect(0, cs.Y-wh, wh, cs.Y)}, + {system.ActionResizeSouthEast, image.Rect(cs.X-wh, cs.Y-wh, cs.X, cs.Y)}, + } { + action := data.Action + if d.DecorationsStyle.Actions&action == 0 { + continue + } + rsz := &d.actions.resize[i] + rsz.Events(gtx.Metric, gtx, gesture.Both) + if rsz.Drag.Dragging() { + d.actioned |= action + } + st := clip.Rect(data.Rectangle).Push(gtx.Ops) + if rsz.Hover.Hovered(gtx) { + pointer.CursorNameOp{Name: action.CursorName()}.Add(gtx.Ops) + } + //paint.Fill(gtx.Ops, color.NRGBA{A: 100}) + rsz.Drag.Add(gtx.Ops) + pass := pointer.PassOp{}.Push(gtx.Ops) + rsz.Hover.Add(gtx.Ops) + pass.Pop() + st.Pop() + } +} + +func (d *Decorations) layoutDecorations(gtx layout.Context, title string) layout.Dimensions { + gtx.Constraints.Min.X = gtx.Constraints.Max.X + gtx.Constraints.Min.Y = 0 + inset := layout.UniformInset(unit.Dp(10)) + return layout.Flex{ + Axis: layout.Horizontal, + Alignment: layout.Middle, + Spacing: layout.SpaceBetween, + }.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + d.DecorationsStyle.Title.Text = title + dims := inset.Layout(gtx, d.DecorationsStyle.Title.Layout) + if d.DecorationsStyle.Actions&system.ActionMove != 0 { + d.actions.move.Events(gtx.Metric, gtx, gesture.Both) + + st := clip.Rect{Max: dims.Size}.Push(gtx.Ops) + d.actions.move.Add(gtx.Ops) + if d.actions.move.Pressed() { + d.actioned |= system.ActionMove + } + st.Pop() + } + return dims + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + // Remove the unmaximize action as it is taken care of by maximize. + actions := d.DecorationsStyle.Actions &^ system.ActionUnmaximize + an := bits.OnesCount(uint(actions)) + if n := len(d.actions.clicks); n < an { + d.actions.clicks = append(d.actions.clicks, make([]widget.Clickable, an-n)...) + } + return d.actions.Layout(gtx, an, func(gtx layout.Context, idx int) layout.Dimensions { + action := system.Action(1 << idx) + var w layout.Widget + switch actions & action { + case system.ActionMinimize: + w = d.minimizeWindow + case system.ActionMaximize: + if d.maximized { + w = d.maximizedWindow + } else { + w = d.maximizeWindow + } + case system.ActionClose: + w = d.closeWindow + default: + return layout.Dimensions{} + } + click := &d.actions.clicks[idx] + if click.Clicked() { + if action == system.ActionMaximize { + if d.maximized { + d.maximized = false + d.actioned |= system.ActionUnmaximize + } else { + d.maximized = true + d.actioned |= system.ActionMaximize + } + } else { + d.actioned |= action + } + } + return Clickable(gtx, click, func(gtx layout.Context) layout.Dimensions { + return inset.Layout(gtx, w) + }) + }) + }), + ) +} + +func (d *Decorations) Actioned() system.Action { + a := d.actioned + d.actioned = 0 + return a +} + +var ( + winIconSize = unit.Dp(20) + winIconMargin = unit.Dp(4) + winIconStroke = unit.Dp(2) +) + +// line +func (d *Decorations) minimizeWindow(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: d.DecorationsStyle.Foreground}.Add(gtx.Ops) + size := gtx.Px(winIconSize) + size32 := float32(size) + margin := float32(gtx.Px(winIconMargin)) + width := float32(gtx.Px(winIconStroke)) + p := &d.path + p.Begin(gtx.Ops) + p.MoveTo(f32.Point{X: margin, Y: size32 - margin}) + p.LineTo(f32.Point{X: size32 - 2*margin, Y: size32 - margin}) + st := clip.Stroke{ + Path: p.End(), + Width: width, + }.Op().Push(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Pop() + return layout.Dimensions{Size: image.Pt(size, size)} +} + +// rectangle +func (d *Decorations) maximizeWindow(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: d.DecorationsStyle.Foreground}.Add(gtx.Ops) + size := gtx.Px(winIconSize) + size32 := float32(size) + margin := float32(gtx.Px(winIconMargin)) + width := float32(gtx.Px(winIconStroke)) + r := clip.RRect{ + Rect: f32.Rect(margin, margin, size32-margin, size32-margin), + } + st := clip.Stroke{ + Path: r.Path(gtx.Ops), + Width: width, + }.Op().Push(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Pop() + r.Rect.Max = f32.Pt(size32-margin, 2*margin) + st = clip.Outline{ + Path: r.Path(gtx.Ops), + }.Op().Push(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Pop() + return layout.Dimensions{Size: image.Pt(size, size)} +} + +// rectangles +func (d *Decorations) maximizedWindow(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: d.DecorationsStyle.Foreground}.Add(gtx.Ops) + size := gtx.Px(winIconSize) + size32 := float32(size) + margin := float32(gtx.Px(winIconMargin)) + width := float32(gtx.Px(winIconStroke)) + r := clip.RRect{ + Rect: f32.Rect(margin, margin, size32-2*margin, size32-2*margin), + } + st := clip.Stroke{ + Path: r.Path(gtx.Ops), + Width: width, + }.Op().Push(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Pop() + r = clip.RRect{ + Rect: f32.Rect(2*margin, 2*margin, size32-margin, size32-margin), + } + st = clip.Stroke{ + Path: r.Path(gtx.Ops), + Width: width, + }.Op().Push(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Pop() + return layout.Dimensions{Size: image.Pt(size, size)} +} + +// cross +func (d *Decorations) closeWindow(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: d.DecorationsStyle.Foreground}.Add(gtx.Ops) + size := gtx.Px(winIconSize) + size32 := float32(size) + margin := float32(gtx.Px(winIconMargin)) + width := float32(gtx.Px(winIconStroke)) + p := &d.path + p.Begin(gtx.Ops) + p.MoveTo(f32.Point{X: margin, Y: margin}) + p.LineTo(f32.Point{X: size32 - margin, Y: size32 - margin}) + p.MoveTo(f32.Point{X: size32 - margin, Y: margin}) + p.LineTo(f32.Point{X: margin, Y: size32 - margin}) + st := clip.Stroke{ + Path: p.End(), + Width: width, + }.Op().Push(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Pop() + return layout.Dimensions{Size: image.Pt(size, size)} +} -- 2.32.0
builds.sr.ht <builds@sr.ht>gio/patches: FAILED in 1m53s [widget/material: add Decorations app: use material.Decorations as default][0] from [~pierrec][1] [0]: https://lists.sr.ht/~eliasnaur/gio-patches/patches/28454 [1]: mailto:pierre.curto@gmail.com ✗ #675090 FAILED gio/patches/apple.yml https://builds.sr.ht/~eliasnaur/job/675090 ✗ #675092 FAILED gio/patches/linux.yml https://builds.sr.ht/~eliasnaur/job/675092 ✗ #675091 FAILED gio/patches/freebsd.yml https://builds.sr.ht/~eliasnaur/job/675091 ✗ #675093 FAILED gio/patches/openbsd.yml https://builds.sr.ht/~eliasnaur/job/675093