gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/widget/material/decorations.go (about)

     1  package material
     2  
     3  import (
     4  	"image"
     5  	"image/color"
     6  
     7  	"gioui.org/f32"
     8  	"gioui.org/io/semantic"
     9  	"gioui.org/io/system"
    10  	"gioui.org/layout"
    11  	"gioui.org/op"
    12  	"gioui.org/op/clip"
    13  	"gioui.org/op/paint"
    14  	"gioui.org/unit"
    15  	"gioui.org/widget"
    16  )
    17  
    18  // DecorationsStyle provides the style elements for Decorations.
    19  type DecorationsStyle struct {
    20  	Decorations *widget.Decorations
    21  	Actions     system.Action
    22  	Title       LabelStyle
    23  	Background  color.NRGBA
    24  	Foreground  color.NRGBA
    25  }
    26  
    27  // Decorations returns the style to decorate a window.
    28  func Decorations(th *Theme, deco *widget.Decorations, actions system.Action, title string) DecorationsStyle {
    29  	titleStyle := Body1(th, title)
    30  	titleStyle.Color = th.Palette.ContrastFg
    31  	return DecorationsStyle{
    32  		Decorations: deco,
    33  		Actions:     actions,
    34  		Title:       titleStyle,
    35  		Background:  th.Palette.ContrastBg,
    36  		Foreground:  th.Palette.ContrastFg,
    37  	}
    38  }
    39  
    40  // Layout a window with its title and action buttons.
    41  func (d DecorationsStyle) Layout(gtx layout.Context) layout.Dimensions {
    42  	rec := op.Record(gtx.Ops)
    43  	dims := d.layoutDecorations(gtx)
    44  	decos := rec.Stop()
    45  	r := clip.Rect{Max: dims.Size}
    46  	paint.FillShape(gtx.Ops, d.Background, r.Op())
    47  	decos.Add(gtx.Ops)
    48  	return dims
    49  }
    50  
    51  func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimensions {
    52  	gtx.Constraints.Min.Y = 0
    53  	inset := layout.UniformInset(10)
    54  	return layout.Flex{
    55  		Axis:      layout.Horizontal,
    56  		Alignment: layout.Middle,
    57  		Spacing:   layout.SpaceBetween,
    58  	}.Layout(gtx,
    59  		layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
    60  			return d.Decorations.LayoutMove(gtx, func(gtx layout.Context) layout.Dimensions {
    61  				return inset.Layout(gtx, d.Title.Layout)
    62  			})
    63  		}),
    64  		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
    65  			// Remove the unmaximize action as it is taken care of by maximize.
    66  			actions := d.Actions &^ system.ActionUnmaximize
    67  			var size image.Point
    68  			for a := system.Action(1); actions != 0; a <<= 1 {
    69  				if a&actions == 0 {
    70  					continue
    71  				}
    72  				actions &^= a
    73  				var w layout.Widget
    74  				switch a {
    75  				case system.ActionMinimize:
    76  					w = minimizeWindow
    77  				case system.ActionMaximize:
    78  					if d.Decorations.Maximized() {
    79  						w = maximizedWindow
    80  					} else {
    81  						w = maximizeWindow
    82  					}
    83  				case system.ActionClose:
    84  					w = closeWindow
    85  				default:
    86  					continue
    87  				}
    88  				cl := d.Decorations.Clickable(a)
    89  				dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
    90  					semantic.Button.Add(gtx.Ops)
    91  					return layout.Background{}.Layout(gtx,
    92  						func(gtx layout.Context) layout.Dimensions {
    93  							defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
    94  							for _, c := range cl.History() {
    95  								drawInk(gtx, c)
    96  							}
    97  							return layout.Dimensions{Size: gtx.Constraints.Min}
    98  						},
    99  						func(gtx layout.Context) layout.Dimensions {
   100  							paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
   101  							return inset.Layout(gtx, w)
   102  						},
   103  					)
   104  				})
   105  				size.X += dims.Size.X
   106  				if size.Y < dims.Size.Y {
   107  					size.Y = dims.Size.Y
   108  				}
   109  				op.Offset(image.Pt(dims.Size.X, 0)).Add(gtx.Ops)
   110  			}
   111  			return layout.Dimensions{Size: size}
   112  		}),
   113  	)
   114  }
   115  
   116  const (
   117  	winIconSize   = unit.Dp(20)
   118  	winIconMargin = unit.Dp(4)
   119  	winIconStroke = unit.Dp(2)
   120  )
   121  
   122  // minimizeWindows draws a line icon representing the minimize action.
   123  func minimizeWindow(gtx layout.Context) layout.Dimensions {
   124  	size := gtx.Dp(winIconSize)
   125  	size32 := float32(size)
   126  	margin := float32(gtx.Dp(winIconMargin))
   127  	width := float32(gtx.Dp(winIconStroke))
   128  	var p clip.Path
   129  	p.Begin(gtx.Ops)
   130  	p.MoveTo(f32.Point{X: margin, Y: size32 - margin})
   131  	p.LineTo(f32.Point{X: size32 - 2*margin, Y: size32 - margin})
   132  	st := clip.Stroke{
   133  		Path:  p.End(),
   134  		Width: width,
   135  	}.Op().Push(gtx.Ops)
   136  	paint.PaintOp{}.Add(gtx.Ops)
   137  	st.Pop()
   138  	return layout.Dimensions{Size: image.Pt(size, size)}
   139  }
   140  
   141  // maximizeWindow draws a rectangle representing the maximize action.
   142  func maximizeWindow(gtx layout.Context) layout.Dimensions {
   143  	size := gtx.Dp(winIconSize)
   144  	margin := gtx.Dp(winIconMargin)
   145  	width := gtx.Dp(winIconStroke)
   146  	r := clip.RRect{
   147  		Rect: image.Rect(margin, margin, size-margin, size-margin),
   148  	}
   149  	st := clip.Stroke{
   150  		Path:  r.Path(gtx.Ops),
   151  		Width: float32(width),
   152  	}.Op().Push(gtx.Ops)
   153  	paint.PaintOp{}.Add(gtx.Ops)
   154  	st.Pop()
   155  	r.Rect.Max = image.Pt(size-margin, 2*margin)
   156  	st = clip.Outline{
   157  		Path: r.Path(gtx.Ops),
   158  	}.Op().Push(gtx.Ops)
   159  	paint.PaintOp{}.Add(gtx.Ops)
   160  	st.Pop()
   161  	return layout.Dimensions{Size: image.Pt(size, size)}
   162  }
   163  
   164  // maximizedWindow draws interleaved rectangles representing the un-maximize action.
   165  func maximizedWindow(gtx layout.Context) layout.Dimensions {
   166  	size := gtx.Dp(winIconSize)
   167  	margin := gtx.Dp(winIconMargin)
   168  	width := gtx.Dp(winIconStroke)
   169  	r := clip.RRect{
   170  		Rect: image.Rect(margin, margin, size-2*margin, size-2*margin),
   171  	}
   172  	st := clip.Stroke{
   173  		Path:  r.Path(gtx.Ops),
   174  		Width: float32(width),
   175  	}.Op().Push(gtx.Ops)
   176  	paint.PaintOp{}.Add(gtx.Ops)
   177  	st.Pop()
   178  	r = clip.RRect{
   179  		Rect: image.Rect(2*margin, 2*margin, size-margin, size-margin),
   180  	}
   181  	st = clip.Stroke{
   182  		Path:  r.Path(gtx.Ops),
   183  		Width: float32(width),
   184  	}.Op().Push(gtx.Ops)
   185  	paint.PaintOp{}.Add(gtx.Ops)
   186  	st.Pop()
   187  	return layout.Dimensions{Size: image.Pt(size, size)}
   188  }
   189  
   190  // closeWindow draws a cross representing the close action.
   191  func closeWindow(gtx layout.Context) layout.Dimensions {
   192  	size := gtx.Dp(winIconSize)
   193  	size32 := float32(size)
   194  	margin := float32(gtx.Dp(winIconMargin))
   195  	width := float32(gtx.Dp(winIconStroke))
   196  	var p clip.Path
   197  	p.Begin(gtx.Ops)
   198  	p.MoveTo(f32.Point{X: margin, Y: margin})
   199  	p.LineTo(f32.Point{X: size32 - margin, Y: size32 - margin})
   200  	p.MoveTo(f32.Point{X: size32 - margin, Y: margin})
   201  	p.LineTo(f32.Point{X: margin, Y: size32 - margin})
   202  	st := clip.Stroke{
   203  		Path:  p.End(),
   204  		Width: width,
   205  	}.Op().Push(gtx.Ops)
   206  	paint.PaintOp{}.Add(gtx.Ops)
   207  	st.Pop()
   208  	return layout.Dimensions{Size: image.Pt(size, size)}
   209  }