~whereswaldon/gio-x

e2749dd627637b3ed4162b50524ec30ded626afe — Chris Waldon 3 months ago 369cb95
component: add Menu and associated types

This commit introduces a context menu type based on the material.io
Menu component, as well as a number of auxiliary types that are useful
in conjunction with it. In particular:

- Shadow creates drop shadows for rounded rectangles (based on Egon
  Elbre's work).
- Surface is a simple rounded rectangle with a background color and
  a drop shadow.
- Divider implements the material.io divider and can easily be used
  within surfaces or Menus to separate content.
- ContextArea defines an area that will display a widget if right-clicked.
  This is useful to show context menus, but can be used to display anything.
- MenuItem provides a pre-configured menu element that respects material
  specifications. You can also provide any layout.Widget as a menu element.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
5 files changed, 720 insertions(+), 4 deletions(-)

A component/context-area.go
A component/menu.go
A component/shadow.go
M go.mod
M go.sum
A component/context-area.go => component/context-area.go +103 -0
@@ 0,0 1,103 @@
// SPDX-License-Identifier: Unlicense OR MIT

package component

import (
	"image"

	"gioui.org/f32"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"
)

// ContextArea is a region of the UI that responds to right-clicks
// with a contextual widget. The contextual widget is overlaid
// using an op.DeferOp.
type ContextArea struct {
	position f32.Point
	dims     D
	active   bool
}

// Layout renders the context area and -- if the area is activated by an
// appropriate gesture -- also the provided widget overlaid using an op.DeferOp.
func (r *ContextArea) Layout(gtx C, w layout.Widget) D {
	pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
	pointer.PassOp{Pass: true}.Add(gtx.Ops)
	pointer.InputOp{
		Tag:   r,
		Grab:  false,
		Types: pointer.Press | pointer.Release,
	}.Add(gtx.Ops)
	for _, e := range gtx.Events(r) {
		e, ok := e.(pointer.Event)
		if !ok {
			continue
		}
		if r.active {
			// Check whether we should dismiss menu.
			if e.Buttons.Contain(pointer.ButtonPrimary) {
				clickPos := e.Position.Sub(r.position)
				if !clickPos.In(f32.Rectangle{Max: layout.FPt(r.dims.Size)}) {
					r.Dismiss()
				}
			}
		}
		if e.Buttons.Contain(pointer.ButtonSecondary) {
			r.active = true
			r.position = e.Position
		}
	}
	dims := D{Size: gtx.Constraints.Min}

	if !r.active {
		return dims
	}

	for _, e := range gtx.Events(&r.active) {
		e, ok := e.(pointer.Event)
		if !ok {
			continue
		}
		if e.Type == pointer.Release {
			r.Dismiss()
		}
	}

	defer op.Save(gtx.Ops).Load()
	macro := op.Record(gtx.Ops)
	r.dims = w(gtx)
	call := macro.Stop()

	if int(r.position.X)+r.dims.Size.X > gtx.Constraints.Max.X {
		r.position.X = float32(gtx.Constraints.Max.X - r.dims.Size.X)
	}
	if int(r.position.Y)+r.dims.Size.Y > gtx.Constraints.Max.Y {
		r.position.Y = float32(gtx.Constraints.Max.Y - r.dims.Size.Y)
	}
	macro2 := op.Record(gtx.Ops)
	op.Offset(r.position).Add(gtx.Ops)
	call.Add(gtx.Ops)
	pointer.PassOp{Pass: true}.Add(gtx.Ops)
	pointer.Rect(image.Rectangle{Min: image.Point{-1e6, -1e6}, Max: image.Point{1e6, 1e6}}).Add(gtx.Ops)
	pointer.InputOp{
		Tag:   &r.active,
		Grab:  false,
		Types: pointer.Release,
	}.Add(gtx.Ops)
	call2 := macro2.Stop()
	op.Defer(gtx.Ops, call2)
	return dims
}

// Dismiss sets the ContextArea to not be active.
func (r *ContextArea) Dismiss() {
	r.active = false
}

// Active returns whether the ContextArea is currently active (whether
// it is currently displaying overlaid content or not).
func (r ContextArea) Active() bool {
	return r.active
}

A component/menu.go => component/menu.go +278 -0
@@ 0,0 1,278 @@
// SPDX-License-Identifier: Unlicense OR MIT

package component

import (
	"image"
	"image/color"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
)

// SurfaceStyle defines the visual aspects of a material design surface
// with (optionally) rounded corners and a drop shadow.
type SurfaceStyle struct {
	*material.Theme
	// The CornerRadius and Elevation fields of the embedded shadow
	// style also define the corner radius and elevation of the card.
	ShadowStyle
}

// Surface creates a Surface style for the provided theme with sensible default
// elevation and rounded corners.
func Surface(th *material.Theme) SurfaceStyle {
	return SurfaceStyle{
		Theme:       th,
		ShadowStyle: Shadow(unit.Dp(4), unit.Dp(4)),
	}
}

// Layout renders the SurfaceStyle, taking the dimensions of the surface from
// gtx.Constraints.Min.
func (c SurfaceStyle) Layout(gtx C, w layout.Widget) D {
	return layout.Stack{}.Layout(gtx,
		layout.Expanded(func(gtx C) D {
			c.ShadowStyle.Layout(gtx)
			surface := clip.UniformRRect(f32.Rectangle{Max: layout.FPt(gtx.Constraints.Min)}, float32(gtx.Px(c.ShadowStyle.CornerRadius)))
			paint.FillShape(gtx.Ops, c.Theme.Bg, surface.Op(gtx.Ops))
			return D{Size: gtx.Constraints.Min}
		}),
		layout.Stacked(w),
	)
}

// DividerStyle defines the presentation of a material divider, as specified
// here: https://material.io/components/dividers
type DividerStyle struct {
	Thickness unit.Value
	Fill      color.NRGBA
	layout.Inset

	Subheading      material.LabelStyle
	SubheadingInset layout.Inset
}

// Divider creates a simple full-bleed divider.
func Divider(th *material.Theme) DividerStyle {
	return DividerStyle{
		Thickness: unit.Dp(1),
		Fill:      WithAlpha(th.Fg, 0x60),
		Inset: layout.Inset{
			Top:    unit.Dp(8),
			Bottom: unit.Dp(8),
		},
	}
}

// SubheadingDivider creates a full-bleed divider with a subheading.
func SubheadingDivider(th *material.Theme, subheading string) DividerStyle {
	return DividerStyle{
		Thickness: unit.Dp(1),
		Fill:      WithAlpha(th.Fg, 0x60),
		Inset: layout.Inset{
			Top:    unit.Dp(8),
			Bottom: unit.Dp(4),
		},
		Subheading: DividerSubheadingText(th, subheading),
		SubheadingInset: layout.Inset{
			Left:   unit.Dp(8),
			Bottom: unit.Dp(8),
		},
	}
}

// Layout renders the divider. If gtx.Constraints.Min.X is zero, it will
// have zero size and render nothing.
func (d DividerStyle) Layout(gtx C) D {
	if gtx.Constraints.Min.X == 0 {
		return D{}
	}
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx C) D {
			return d.Inset.Layout(gtx, func(gtx C) D {
				weight := gtx.Px(d.Thickness)
				line := image.Rectangle{Max: image.Pt(gtx.Constraints.Min.X, weight)}
				paint.FillShape(gtx.Ops, d.Fill, clip.Rect(line).Op())
				return D{Size: line.Max}
			})
		}),
		layout.Rigid(func(gtx C) D {
			if d.Subheading == (material.LabelStyle{}) {
				return D{}
			}
			return d.SubheadingInset.Layout(gtx, d.Subheading.Layout)
		}),
	)
}

// MenuItemStyle defines the presentation of a Menu element that has a label
// and optionally an icon and a hint text.
type MenuItemStyle struct {
	State      *widget.Clickable
	HoverColor color.NRGBA

	LabelInset layout.Inset
	Label      material.LabelStyle

	*widget.Icon
	IconSize  unit.Value
	IconInset layout.Inset

	Hint      material.LabelStyle
	HintInset layout.Inset
}

// MenuItem constructs a default MenuItemStyle based on the theme, state, and label.
func MenuItem(th *material.Theme, state *widget.Clickable, label string) MenuItemStyle {
	return MenuItemStyle{
		State: state,
		LabelInset: layout.Inset{
			Left:   unit.Dp(16),
			Right:  unit.Dp(16),
			Top:    unit.Dp(8),
			Bottom: unit.Dp(8),
		},
		IconSize: unit.Dp(24),
		IconInset: layout.Inset{
			Left: unit.Dp(16),
		},
		HintInset: layout.Inset{
			Right: unit.Dp(16),
		},
		Label:      material.Body1(th, label),
		HoverColor: WithAlpha(th.ContrastBg, 0x30),
	}
}

// Layout renders the MenuItemStyle. If gtx.Constraints.Min.X is zero, it will render
// itself to be as compact as possible horizontally.
func (m MenuItemStyle) Layout(gtx C) D {
	min := gtx.Constraints.Min.X
	compact := min == 0
	return material.Clickable(gtx, m.State, func(gtx C) D {
		return layout.Stack{}.Layout(gtx,
			layout.Expanded(func(gtx C) D {
				area := image.Rectangle{
					Max: gtx.Constraints.Min,
				}
				if m.State.Hovered() {
					paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect(area).Op())
				}
				return D{Size: area.Max}
			}),
			layout.Stacked(func(gtx C) D {
				gtx.Constraints.Min.X = min
				return layout.Flex{
					Alignment: layout.Middle,
				}.Layout(gtx,
					layout.Rigid(func(gtx C) D {
						if m.Icon == nil {
							return D{}
						}
						return m.IconInset.Layout(gtx, func(gtx C) D {
							return m.Icon.Layout(gtx, m.IconSize)
						})
					}),
					layout.Rigid(func(gtx C) D {
						return m.LabelInset.Layout(gtx, func(gtx C) D {
							return m.Label.Layout(gtx)
						})
					}),
					layout.Flexed(1, func(gtx C) D {
						if compact {
							return D{}
						}
						return D{Size: gtx.Constraints.Min}
					}),
					layout.Rigid(func(gtx C) D {
						if empty := (material.LabelStyle{}); m.Hint == empty {
							return D{}
						}
						return m.HintInset.Layout(gtx, func(gtx C) D {
							return m.Hint.Layout(gtx)
						})
					}),
				)
			}),
		)
	})
}

// MenuHintText returns a LabelStyle suitable for use as hint text in a
// MenuItemStyle.
func MenuHintText(th *material.Theme, label string) material.LabelStyle {
	l := material.Body1(th, label)
	l.Color = WithAlpha(l.Color, 0xaa)
	return l
}

// DividerSubheadingText returns a LabelStyle suitable for use as a subheading
// in a divider.
func DividerSubheadingText(th *material.Theme, label string) material.LabelStyle {
	l := material.Body2(th, label)
	l.Color = WithAlpha(l.Color, 0xaa)
	return l
}

// MenuState holds the state of a menu material design component
// across frames.
type MenuState struct {
	OptionList layout.List
	Options    []func(gtx C) D
}

// MenuStyle defines the presentation of a material design menu component.
type MenuStyle struct {
	*MenuState
	*material.Theme
	// Inset applied around the rendered contents of the state's Options field.
	layout.Inset
	SurfaceStyle
}

// Menu constructs a menu with the provided state and a default Surface behind
// it.
func Menu(th *material.Theme, state *MenuState) MenuStyle {
	m := MenuStyle{
		Theme:        th,
		MenuState:    state,
		SurfaceStyle: Surface(th),
		Inset: layout.Inset{
			Top:    unit.Dp(8),
			Bottom: unit.Dp(8),
		},
	}
	m.OptionList.Axis = layout.Vertical
	return m
}

// Layout renders the menu.
func (m MenuStyle) Layout(gtx C) D {
	var fakeOps op.Ops
	originalOps := gtx.Ops
	gtx.Ops = &fakeOps
	maxWidth := 0
	for _, w := range m.Options {
		dims := w(gtx)
		if dims.Size.X > maxWidth {
			maxWidth = dims.Size.X
		}
	}
	gtx.Ops = originalOps
	return m.SurfaceStyle.Layout(gtx, func(gtx C) D {
		return m.Inset.Layout(gtx, func(gtx C) D {
			return m.OptionList.Layout(gtx, len(m.Options), func(gtx C, index int) D {
				gtx.Constraints.Min.X = maxWidth
				gtx.Constraints.Max.X = maxWidth
				return m.Options[index](gtx)
			})
		})
	})
}

A component/shadow.go => component/shadow.go +334 -0
@@ 0,0 1,334 @@
// SPDX-License-Identifier: Unlicense OR MIT

/*
This file is derived from work by Egon Elbre in his gio experiments
repository available here:

https://github.com/egonelbre/expgio/tree/master/box-shadows

He generously licensed it under the Unlicense, and thus is is
reproduced here under the same terms.
*/
package component

import (
	"image"
	"image/color"
	"math"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
)

// ShadowStyle defines a shadow cast by a rounded rectangle.
//
// TODO(whereswaldon): make this support RRects that do not have
// uniform corner radii.
type ShadowStyle struct {
	// The radius of the corners of the rectangle casting the surface.
	// Non-rounded rectangles can just provide a zero.
	CornerRadius unit.Value
	// Elevation is how high the surface casting the shadow is above
	// the background, and therefore determines how diffuse and large
	// the shadow is.
	Elevation unit.Value
	// The colors of various components of the shadow. The Shadow()
	// constructor populates these with reasonable defaults.
	AmbientColor, PenumbraColor, UmbraColor color.NRGBA
}

// Shadow defines a shadow cast by a rounded rectangle with the given
// corner radius and elevation. It sets reasonable defaults for the
// shadow colors.
func Shadow(radius, elevation unit.Value) ShadowStyle {
	return ShadowStyle{
		CornerRadius:  radius,
		Elevation:     elevation,
		AmbientColor:  color.NRGBA{A: 0x10},
		PenumbraColor: color.NRGBA{A: 0x20},
		UmbraColor:    color.NRGBA{A: 0x30},
	}
}

// Layout renders the shadow into the gtx. The shadow's size will assume
// that the rectangle casting the shadow is of size gtx.Constraints.Min.
func (s ShadowStyle) Layout(gtx layout.Context) layout.Dimensions {
	sz := gtx.Constraints.Min
	rr := float32(gtx.Px(s.CornerRadius))

	r := f32.Rect(0, 0, float32(sz.X), float32(sz.Y))
	s.layoutShadow(gtx, r, rr)

	return layout.Dimensions{Size: sz}
}

func (s ShadowStyle) layoutShadow(gtx layout.Context, r f32.Rectangle, rr float32) {
	if s.Elevation.V <= 0 {
		return
	}

	offset := pxf(gtx.Metric, s.Elevation)

	ambient := r
	gradientBox(gtx.Ops, ambient, rr, offset/2, s.AmbientColor)

	penumbra := r.Add(f32.Pt(0, offset/2))
	gradientBox(gtx.Ops, penumbra, rr, offset, s.PenumbraColor)

	umbra := outset(penumbra, -offset/2)
	gradientBox(gtx.Ops, umbra, rr/4, offset/2, s.UmbraColor)
}

// TODO(whereswaldon): switch back to commented implementation when radial
// gradients are available in core.
func gradientBox(ops *op.Ops, r f32.Rectangle, rr, spread float32, col color.NRGBA) {
	/*
		transparent := col
		transparent.A = 0

		// ensure we are aligned to pixel grid
		r = round(r)
		rr = float32(math.Ceil(float64(rr)))
		spread = float32(math.Ceil(float64(spread)))

		// calculate inside and outside boundaries
		inside := imageRect(outset(r, -rr))
		center := imageRect(r)
		outside := imageRect(outset(r, spread))

		radialStop2 := image.Pt(0, int(spread+rr))
		radialOffset1 := rr / (spread + rr)

		corners := []func(image.Rectangle) image.Point{
			topLeft,
			topRight,
			bottomRight,
			bottomLeft,
		}

		for _, corner := range corners {
			func() {
				defer op.Save(ops).Load()
				clipr := image.Rectangle{
					Min: corner(inside),
					Max: corner(outside),
				}.Canon()
				clip.Rect(clipr).Add(ops)
				paint.RadialGradientOp{
					Color1: col, Color2: transparent,
					Stop1:   layout.FPt(corner(inside)),
					Stop2:   layout.FPt(corner(inside).Add(radialStop2)),
					Offset1: radialOffset1,
				}.Add(ops)
				paint.PaintOp{}.Add(ops)
			}()
		}

		// top
		func() {
			defer op.Save(ops).Load()
			clipr := image.Rectangle{
				Min: image.Point{
					X: inside.Min.X,
					Y: outside.Min.Y,
				},
				Max: image.Point{
					X: inside.Max.X,
					Y: center.Min.Y,
				},
			}
			clip.Rect(clipr).Add(ops)
			paint.LinearGradientOp{
				Color1: col, Color2: transparent,
				Stop1: layout.FPt(image.Point{
					X: inside.Min.X,
					Y: center.Min.Y,
				}),
				Stop2: layout.FPt(image.Point{
					X: inside.Min.X,
					Y: outside.Min.Y,
				}),
			}.Add(ops)
			paint.PaintOp{}.Add(ops)
		}()

		// right
		func() {
			defer op.Save(ops).Load()
			clipr := image.Rectangle{
				Min: image.Point{
					X: center.Max.X,
					Y: inside.Min.Y,
				},
				Max: image.Point{
					X: outside.Max.X,
					Y: inside.Max.Y,
				},
			}
			clip.Rect(clipr).Add(ops)
			paint.LinearGradientOp{
				Color1: col, Color2: transparent,
				Stop1: layout.FPt(image.Point{
					X: center.Max.X,
					Y: inside.Min.Y,
				}),
				Stop2: layout.FPt(image.Point{
					X: outside.Max.X,
					Y: inside.Min.Y,
				}),
			}.Add(ops)
			paint.PaintOp{}.Add(ops)
		}()

		// bottom
		func() {
			defer op.Save(ops).Load()
			clipr := image.Rectangle{
				Min: image.Point{
					X: inside.Min.X,
					Y: center.Max.Y,
				},
				Max: image.Point{
					X: inside.Max.X,
					Y: outside.Max.Y,
				},
			}
			clip.Rect(clipr).Add(ops)
			paint.LinearGradientOp{
				Color1: col, Color2: transparent,
				Stop1: layout.FPt(image.Point{
					X: inside.Min.X,
					Y: center.Max.Y,
				}),
				Stop2: layout.FPt(image.Point{
					X: inside.Min.X,
					Y: outside.Max.Y,
				}),
			}.Add(ops)
			paint.PaintOp{}.Add(ops)
		}()

		// left
		func() {
			defer op.Save(ops).Load()
			clipr := image.Rectangle{
				Min: image.Point{
					X: outside.Min.X,
					Y: inside.Min.Y,
				},
				Max: image.Point{
					X: center.Min.X,
					Y: inside.Max.Y,
				},
			}
			clip.Rect(clipr).Add(ops)
			paint.LinearGradientOp{
				Color1: col, Color2: transparent,
				Stop1: layout.FPt(image.Point{
					X: center.Min.X,
					Y: inside.Min.Y,
				}),
				Stop2: layout.FPt(image.Point{
					X: outside.Min.X,
					Y: inside.Min.Y,
				}),
			}.Add(ops)
			paint.PaintOp{}.Add(ops)
		}()

		func() {
			defer op.Save(ops).Load()
			var p clip.Path
			p.Begin(ops)

			inside := layout.FRect(inside)
			center := layout.FRect(center)

			p.MoveTo(inside.Min)
			p.LineTo(f32.Point{X: inside.Min.X, Y: center.Min.Y})
			p.LineTo(f32.Point{X: inside.Max.X, Y: center.Min.Y})
			p.LineTo(f32.Point{X: inside.Max.X, Y: inside.Min.Y})
			p.LineTo(f32.Point{X: center.Max.X, Y: inside.Min.Y})
			p.LineTo(f32.Point{X: center.Max.X, Y: inside.Max.Y})
			p.LineTo(f32.Point{X: inside.Max.X, Y: inside.Max.Y})
			p.LineTo(f32.Point{X: inside.Max.X, Y: center.Max.Y})
			p.LineTo(f32.Point{X: inside.Min.X, Y: center.Max.Y})
			p.LineTo(f32.Point{X: inside.Min.X, Y: inside.Max.Y})
			p.LineTo(f32.Point{X: center.Min.X, Y: inside.Max.Y})
			p.LineTo(f32.Point{X: center.Min.X, Y: inside.Min.Y})
			p.LineTo(inside.Min)

			clip.Outline{Path: p.End()}.Op().Add(ops)
			paint.ColorOp{Color: col}.Add(ops)
			paint.PaintOp{}.Add(ops)
		}()
	*/
	paint.FillShape(ops, col, clip.RRect{
		Rect: outset(r, spread),
		SE:   rr + spread, SW: rr + spread, NW: rr + spread, NE: rr + spread,
	}.Op(ops))
}

func imageRect(r f32.Rectangle) image.Rectangle {
	return image.Rectangle{
		Min: image.Point{
			X: int(math.Round(float64(r.Min.X))),
			Y: int(math.Round(float64(r.Min.Y))),
		},
		Max: image.Point{
			X: int(math.Round(float64(r.Max.X))),
			Y: int(math.Round(float64(r.Max.Y))),
		},
	}
}

func round(r f32.Rectangle) f32.Rectangle {
	return f32.Rectangle{
		Min: f32.Point{
			X: float32(math.Round(float64(r.Min.X))),
			Y: float32(math.Round(float64(r.Min.Y))),
		},
		Max: f32.Point{
			X: float32(math.Round(float64(r.Max.X))),
			Y: float32(math.Round(float64(r.Max.Y))),
		},
	}
}

func outset(r f32.Rectangle, rr float32) f32.Rectangle {
	r.Min.X -= rr
	r.Min.Y -= rr
	r.Max.X += rr
	r.Max.Y += rr
	return r
}

func pxf(c unit.Metric, v unit.Value) float32 {
	switch v.U {
	case unit.UnitPx:
		return v.V
	case unit.UnitDp:
		s := c.PxPerDp
		if s == 0 {
			s = 1
		}
		return s * v.V
	case unit.UnitSp:
		s := c.PxPerSp
		if s == 0 {
			s = 1
		}
		return s * v.V
	default:
		panic("unknown unit")
	}
}

func topLeft(r image.Rectangle) image.Point     { return r.Min }
func topRight(r image.Rectangle) image.Point    { return image.Point{X: r.Max.X, Y: r.Min.Y} }
func bottomRight(r image.Rectangle) image.Point { return r.Max }
func bottomLeft(r image.Rectangle) image.Point  { return image.Point{X: r.Min.X, Y: r.Max.Y} }

M go.mod => go.mod +1 -1
@@ 3,7 3,7 @@ module gioui.org/x
go 1.16

require (
	gioui.org v0.0.0-20210225120118-f6fba7388544
	gioui.org v0.0.0-20210316180047-ac800a9d8f26
	golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03
	golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect
	golang.org/x/text v0.3.4 // indirect

M go.sum => go.sum +4 -3
@@ 1,7 1,7 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20210225120118-f6fba7388544 h1:xQePGQEyaupU5lfPe3//VUVZLXNsjALGAppEVAEzRh8=
gioui.org v0.0.0-20210225120118-f6fba7388544/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw=
gioui.org v0.0.0-20210316180047-ac800a9d8f26 h1:ZJHBvymShDMbSOBLFGN+zPWZoA7Bz1t6hd3zALwl8kw=
gioui.org v0.0.0-20210316180047-ac800a9d8f26/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=


@@ 33,7 33,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197 h1:7+SpRyhoo46QjKkYInQXpcfxx3TYFEYkn131lwGE9/0=
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=