package material import ( "image" "image/color" "gioui.org/f32" "gioui.org/io/semantic" "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 { Decorations *widget.Decorations Actions system.Action Title LabelStyle Background color.NRGBA Foreground color.NRGBA } // 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{ Decorations: deco, Actions: actions, Title: titleStyle, Background: th.Palette.ContrastBg, Foreground: th.Palette.ContrastFg, } } // Layout a window with its title and action buttons. func (d DecorationsStyle) Layout(gtx layout.Context) layout.Dimensions { rec := op.Record(gtx.Ops) 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) return dims } func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.Y = 0 inset := layout.UniformInset(10) return layout.Flex{ Axis: layout.Horizontal, Alignment: layout.Middle, Spacing: layout.SpaceBetween, }.Layout(gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { 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 { // 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 } cl := d.Decorations.Clickable(a) dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions { semantic.Button.Add(gtx.Ops) return layout.Background{}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop() for _, c := range cl.History() { drawInk(gtx, c) } return layout.Dimensions{Size: gtx.Constraints.Min} }, func(gtx layout.Context) layout.Dimensions { paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops) return inset.Layout(gtx, w) }, ) }) size.X += dims.Size.X if size.Y < dims.Size.Y { size.Y = dims.Size.Y } op.Offset(image.Pt(dims.Size.X, 0)).Add(gtx.Ops) } return layout.Dimensions{Size: size} }), ) } const ( winIconSize = unit.Dp(20) winIconMargin = unit.Dp(4) winIconStroke = unit.Dp(2) ) // minimizeWindows draws a line icon representing the minimize action. func minimizeWindow(gtx layout.Context) layout.Dimensions { size := gtx.Dp(winIconSize) size32 := float32(size) margin := float32(gtx.Dp(winIconMargin)) width := float32(gtx.Dp(winIconStroke)) var p clip.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)} } // maximizeWindow draws a rectangle representing the maximize action. func maximizeWindow(gtx layout.Context) layout.Dimensions { size := gtx.Dp(winIconSize) margin := gtx.Dp(winIconMargin) width := gtx.Dp(winIconStroke) r := clip.RRect{ Rect: image.Rect(margin, margin, size-margin, size-margin), } st := clip.Stroke{ Path: r.Path(gtx.Ops), Width: float32(width), }.Op().Push(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) st.Pop() r.Rect.Max = image.Pt(size-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)} } // maximizedWindow draws interleaved rectangles representing the un-maximize action. func maximizedWindow(gtx layout.Context) layout.Dimensions { size := gtx.Dp(winIconSize) margin := gtx.Dp(winIconMargin) width := gtx.Dp(winIconStroke) r := clip.RRect{ Rect: image.Rect(margin, margin, size-2*margin, size-2*margin), } st := clip.Stroke{ Path: r.Path(gtx.Ops), Width: float32(width), }.Op().Push(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) st.Pop() r = clip.RRect{ Rect: image.Rect(2*margin, 2*margin, size-margin, size-margin), } st = clip.Stroke{ Path: r.Path(gtx.Ops), Width: float32(width), }.Op().Push(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) st.Pop() return layout.Dimensions{Size: image.Pt(size, size)} } // closeWindow draws a cross representing the close action. func closeWindow(gtx layout.Context) layout.Dimensions { size := gtx.Dp(winIconSize) size32 := float32(size) margin := float32(gtx.Dp(winIconMargin)) width := float32(gtx.Dp(winIconStroke)) var p clip.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)} }