diff --git a/app/window.go b/app/window.go index f958f848..1f63922f 100644 --- a/app/window.go +++ b/app/window.go @@ -22,6 +22,7 @@ import ( "gioui.org/layout" "gioui.org/op" "gioui.org/unit" + "gioui.org/widget" "gioui.org/widget/material" _ "gioui.org/app/internal/log" @@ -68,7 +69,9 @@ type Window struct { decorations struct { op.Ops Config - *material.DecorationsStyle + *material.Theme + *widget.Decorations + size image.Point // decorations size } callbacks callbacks @@ -620,8 +623,14 @@ func (w *Window) processEvent(d driver, e event.Event) { w.waitAck(d) case wakeupEvent: case ConfigEvent: + if w.decorations.Config.Decorated != e2.Config.Decorated && e2.Config.Decorated { + // Decorations are no longer applied. + w.decorations.Decorations = nil + w.decorations.size = image.Point{} + } w.decorations.Config = e2.Config - w.out <- e + e2.Config.Size = e2.Config.Size.Sub(w.decorations.size) + w.out <- e2 case event.Event: if w.queue.q.Queue(e2) { w.setNextFrame(time.Time{}) @@ -685,19 +694,23 @@ 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.DecorationsStyle - 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 - style := material.Decorate(theme, allActions) - deco = &style - w.decorations.DecorationsStyle = &style + theme := w.decorations.Theme + if theme == nil { + theme = material.NewTheme(gofont.Collection()) + w.decorations.Theme = theme } + deco := w.decorations.Decorations + if deco == nil { + deco = new(widget.Decorations) + w.decorations.Decorations = deco + } + 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 + style := material.Decorations(theme, deco, allActions, w.decorations.Config.Title) // Update the decorations based on the current window mode. var actions system.Action switch m := w.decorations.Config.Mode; m { @@ -712,10 +725,7 @@ func (w *Window) decorate(d driver, e system.FrameEvent, o *op.Ops) image.Point default: panic(fmt.Errorf("unknown WindowMode %v", m)) } - deco.Decorations.Perform(actions) - // Update the window based on the actions on the decorations. - d.Perform(deco.Decorations.Actions()) - + deco.Perform(actions) gtx := layout.Context{ Ops: o, Now: e.Now, @@ -724,12 +734,21 @@ func (w *Window) decorate(d driver, e system.FrameEvent, o *op.Ops) image.Point Constraints: layout.Exact(e.Size), } rec := op.Record(o) - dims := deco.Layout(gtx, w.decorations.Config.Title) + dims := style.Layout(gtx) op.Defer(o, rec.Stop()) + // Update the window based on the actions on the decorations. + d.Perform(deco.Actions()) // 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) + appSize := e.Size.Sub(size) + if w.decorations.size != size { + w.decorations.size = size + cnf := w.decorations.Config + cnf.Size = appSize + w.out <- ConfigEvent{Config: cnf} + } + return appSize } // Raise requests that the platform bring this window to the top of all open windows. diff --git a/widget/decorations.go b/widget/decorations.go new file mode 100644 index 00000000..39e033e8 --- /dev/null +++ b/widget/decorations.go @@ -0,0 +1,127 @@ +package widget + +import ( + "fmt" + "image" + "math/bits" + + "gioui.org/gesture" + "gioui.org/io/pointer" + "gioui.org/io/system" + "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/unit" +) + +// Decorations handles the states of window decorations. +type Decorations struct { + move gesture.Drag + clicks []Clickable + resize [8]struct { + gesture.Hover + gesture.Drag + } + actions system.Action + maximized bool +} + +// LayoutMove lays out the widget that makes a window movable. +func (d *Decorations) LayoutMove(gtx layout.Context, w layout.Widget) layout.Dimensions { + dims := w(gtx) + d.move.Events(gtx.Metric, gtx, gesture.Both) + st := clip.Rect{Max: dims.Size}.Push(gtx.Ops) + d.move.Add(gtx.Ops) + if d.move.Pressed() { + d.actions |= system.ActionMove + } + st.Pop() + return dims +} + +// Clickable returns the clickable for the given single action. +func (d *Decorations) Clickable(action system.Action) *Clickable { + if bits.OnesCount(uint(action)) != 1 { + panic(fmt.Errorf("not a single action")) + } + idx := bits.TrailingZeros(uint(action)) + if n := idx - len(d.clicks); n >= 0 { + d.clicks = append(d.clicks, make([]Clickable, n+1)...) + } + click := &d.clicks[idx] + if click.Clicked() { + if action == system.ActionMaximize { + if d.maximized { + d.maximized = false + d.actions |= system.ActionUnmaximize + } else { + d.maximized = true + d.actions |= system.ActionMaximize + } + } else { + d.actions |= action + } + } + return click +} + +// LayoutResize lays out the resize actions. +func (d *Decorations) LayoutResize(gtx layout.Context, actions system.Action) { + cs := gtx.Constraints.Max + wh := gtx.Px(unit.Dp(10)) + s := []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.ActionResizeNorthEast, image.Rect(cs.X-wh, 0, cs.X, wh)}, + {system.ActionResizeSouthWest, image.Rect(0, cs.Y-wh, wh, cs.Y)}, + {system.ActionResizeSouthEast, image.Rect(cs.X-wh, cs.Y-wh, cs.X, cs.Y)}, + } + for i, data := range s { + action := data.Action + if actions&action == 0 { + continue + } + rsz := &d.resize[i] + rsz.Events(gtx.Metric, gtx, gesture.Both) + if rsz.Drag.Dragging() { + d.actions |= action + } + st := clip.Rect(data.Rectangle).Push(gtx.Ops) + if rsz.Hover.Hovered(gtx) { + pointer.CursorNameOp{Name: action.CursorName()}.Add(gtx.Ops) + } + rsz.Drag.Add(gtx.Ops) + pass := pointer.PassOp{}.Push(gtx.Ops) + rsz.Hover.Add(gtx.Ops) + pass.Pop() + st.Pop() + } +} + +// Perform updates the decorations as if the specified actions were +// performed by the user. +func (d *Decorations) Perform(actions system.Action) { + if actions&system.ActionMaximize != 0 { + d.maximized = true + } + if actions&(system.ActionUnmaximize|system.ActionMinimize|system.ActionFullscreen) != 0 { + d.maximized = false + } +} + +// Actions returns the set of actions activated by the user. +func (d *Decorations) Actions() system.Action { + a := d.actions + d.actions = 0 + return a +} + +// Maximized returns whether the window is maximized. +func (d *Decorations) Maximized() bool { + return d.maximized +} diff --git a/widget/material/decorations.go b/widget/material/decorations.go index a8cea82d..91f3ed72 100644 --- a/widget/material/decorations.go +++ b/widget/material/decorations.go @@ -3,11 +3,8 @@ 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" @@ -19,95 +16,42 @@ import ( // DecorationsStyle provides the style elements for Decorations. type DecorationsStyle struct { - Decorations - Actions system.Action - Title LabelStyle - Background color.NRGBA - Foreground color.NRGBA + Decorations *widget.Decorations + 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, "") +// Decorations returns the style to decorate a window. +func Decorations(th *Theme, deco *widget.Decorations, actions system.Action, title string) DecorationsStyle { + titleStyle := Body1(th, title) titleStyle.Color = th.Palette.ContrastFg return DecorationsStyle{ - Actions: actions, - Title: titleStyle, - Background: th.Palette.ContrastBg, - Foreground: th.Palette.ContrastFg, + Decorations: deco, + Actions: actions, + Title: titleStyle, + Background: th.Palette.ContrastBg, + Foreground: th.Palette.ContrastFg, } } -// Decorations provides window decorations. -type Decorations struct { - 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 -} - -// Layout a window with the title and actions defined in DecorationsStyle. -func (d *DecorationsStyle) Layout(gtx layout.Context, title string) layout.Dimensions { +// Layout a window with its title and action buttons. +func (d *DecorationsStyle) Layout(gtx layout.Context) layout.Dimensions { rec := op.Record(gtx.Ops) - label := d.Title - label.Text = title paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops) - dims := d.Decorations.layoutDecorations(gtx, d.Actions, label.Layout) + dims := d.layoutDecorations(gtx) decos := rec.Stop() r := clip.Rect{Max: dims.Size} paint.FillShape(gtx.Ops, d.Background, r.Op()) decos.Add(gtx.Ops) - d.Decorations.layoutResizing(gtx, d.Actions) + if !d.Decorations.Maximized() { + d.Decorations.LayoutResize(gtx, d.Actions) + } return dims } -func (d *Decorations) layoutResizing(gtx layout.Context, actions system.Action) { - cs := gtx.Constraints.Max - wh := gtx.Px(unit.Dp(10)) - s := []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.ActionResizeNorthEast, image.Rect(cs.X-wh, 0, cs.X, wh)}, - {system.ActionResizeSouthWest, image.Rect(0, cs.Y-wh, wh, cs.Y)}, - {system.ActionResizeSouthEast, image.Rect(cs.X-wh, cs.Y-wh, cs.X, cs.Y)}, - } - for i, data := range s { - action := data.Action - if 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) - } - 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, actions system.Action, title layout.Widget) layout.Dimensions { +func (d *DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.Y = 0 inset := layout.UniformInset(unit.Dp(10)) return layout.Flex{ @@ -116,87 +60,49 @@ func (d *Decorations) layoutDecorations(gtx layout.Context, actions system.Actio Spacing: layout.SpaceBetween, }.Layout(gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - dims := inset.Layout(gtx, title) - if 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 := 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)...) - } - action := system.Action(1) - return d.actions.Layout(gtx, an, func(gtx layout.Context, idx int) layout.Dimensions { - defer func() { action <<= 1 }() - for actions&action == 0 { - action <<= 1 - } - 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 { - var w layout.Widget - switch actions & action { - case system.ActionMinimize: - w = minimizeWindow - case system.ActionMaximize: - if d.maximized { - w = maximizedWindow - } else { - w = maximizeWindow - } - case system.ActionClose: - w = closeWindow - default: - return layout.Dimensions{} - } - return inset.Layout(gtx, w) - }) + return d.Decorations.LayoutMove(gtx, func(gtx layout.Context) layout.Dimensions { + return inset.Layout(gtx, d.Title.Layout) }) }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + defer op.Offset(f32.Point{}).Push(gtx.Ops).Pop() + // Remove the unmaximize action as it is taken care of by maximize. + actions := d.Actions &^ system.ActionUnmaximize + var size image.Point + for a := system.Action(1); actions != 0; a <<= 1 { + if a&actions == 0 { + continue + } + actions &^= a + var w layout.Widget + switch a { + case system.ActionMinimize: + w = minimizeWindow + case system.ActionMaximize: + if d.Decorations.Maximized() { + w = maximizedWindow + } else { + w = maximizeWindow + } + case system.ActionClose: + w = closeWindow + default: + continue + } + dims := d.Decorations.Clickable(a).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return inset.Layout(gtx, w) + }) + size.X += dims.Size.X + if size.Y < dims.Size.Y { + size.Y = dims.Size.Y + } + op.Offset(f32.Point{X: float32(dims.Size.X)}).Add(gtx.Ops) + } + return layout.Dimensions{Size: size} + }), ) } -// Perform updates the decorations as if the specified actions were -// performed by the user. -func (d *Decorations) Perform(actions system.Action) { - if actions&system.ActionMaximize != 0 { - d.maximized = true - } - if actions&(system.ActionUnmaximize|system.ActionMinimize|system.ActionFullscreen) != 0 { - d.maximized = false - } -} - -// Actions returns the set of actions activated by the user. -func (d *Decorations) Actions() system.Action { - a := d.actioned - d.actioned = 0 - return a -} - var ( winIconSize = unit.Dp(20) winIconMargin = unit.Dp(4)