~eliasnaur/gio-example

be4210bc9c13a956ada2498e62a31e2eb836e6aa — Chris Waldon 2 months ago 0034e85
gio-extras/outlay/{fan,grid}: add outlay examples

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
A gio-extras/outlay/fan/cribbage/cmd/crib.go => gio-extras/outlay/fan/cribbage/cmd/crib.go +25 -0
@@ 0,0 1,25 @@
package main

import (
	"fmt"

	"gioui.org/example/gio-extras/outlay/fan/cribbage"
)

func main() {
	g := cribbage.NewGame(2)
	fmt.Println(g)
	g.DealRound()
	fmt.Println(g)
	g.Sacrifice(0, 0)
	g.Sacrifice(0, 4)
	g.Sacrifice(1, 0)
	g.Sacrifice(1, 4)
	fmt.Println(g)
	g.CutAt(10)
	fmt.Println(g)
	g.Reset()
	fmt.Println(g)
	g.DealRound()
	fmt.Println(g)
}

A gio-extras/outlay/fan/cribbage/cribbage.go => gio-extras/outlay/fan/cribbage/cribbage.go +171 -0
@@ 0,0 1,171 @@
package cribbage

import (
	"fmt"
	"math/rand"

	"gioui.org/example/gio-extras/outlay/fan/playing"
)

type Phase uint8

const (
	BetweenHands Phase = iota
	Dealing
	Sacrifice
	Cut
	CircularCount
	CountHands
	CountCrib
)

func (p Phase) String() string {
	switch p {
	case BetweenHands:
		return "between"
	case Dealing:
		return "dealing"
	case Sacrifice:
		return "sacrifice"
	case Cut:
		return "cut"
	case CircularCount:
		return "circular count"
	case CountHands:
		return "count hands"
	case CountCrib:
		return "count crib"
	default:
		return "unknown"
	}
}

type Game struct {
	Phase
	Deck    []playing.Card
	CutCard *playing.Card
	Dealer  int
	Crib    []playing.Card
	Players []Player
}

type Player struct {
	Hand, Table []playing.Card
}

func (p Player) String() string {
	return fmt.Sprintf("[Hand: %s, Table: %s]", p.Hand, p.Table)
}

func (g Game) String() string {
	return fmt.Sprintf("[Phase: %v\nDealer: %v\nCrib: %v\nCut: %v\nPlayers: %v\nDeck: %v]\n", g.Phase, g.Dealer, g.Crib, g.CutCard, g.Players, g.Deck)
}

const MinHand = 4

func NewGame(players int) Game {
	var g Game
	g.Players = make([]Player, players)
	g.Dealer = g.NumPlayers() - 1
	for i := 0; i < 4; i++ {
		for j := 0; j < 13; j++ {
			g.Deck = append(g.Deck, playing.Card{
				Suit: playing.Suit(i),
				Rank: playing.Rank(j),
			})
		}
	}
	g.Phase = Dealing
	return g
}

func (g Game) NumPlayers() int {
	return len(g.Players)
}

func (g Game) Right(player int) int {
	return (player + g.NumPlayers() - 1) % g.NumPlayers()
}

func (g Game) Left(player int) int {
	return (player + 1) % g.NumPlayers()
}

func (g *Game) CutAt(depth int) {
	g.CutCard = &g.Deck[depth]
	g.Phase = CircularCount
}

func DrainInto(src, dest *[]playing.Card) {
	for _, c := range *src {
		*dest = append(*dest, c)
	}
	*src = (*src)[:0]
}

func (g *Game) Reset() {
	for i := range g.Players {
		DrainInto(&(g.Players[i].Hand), &g.Deck)
		DrainInto(&(g.Players[i].Table), &g.Deck)
	}
	DrainInto(&(g.Crib), &g.Deck)
	g.Phase = Dealing
	g.CutCard = nil
}

func (g *Game) DealCardTo(dest *[]playing.Card) {
	card := g.Deck[0]
	g.Deck = g.Deck[1:]
	*dest = append(*dest, card)
}

func (g *Game) DealRound() {
	g.Dealer = g.Left(g.Dealer)
	g.Reset()
	g.Shuffle()
	for i := 0; i < g.CardsToDealPerPlayer(); i++ {
		for i := range g.Players {
			g.DealCardTo(&(g.Players[i].Hand))
		}
	}
	for i := 0; i < g.CardsDealtToCrib(); i++ {
		g.DealCardTo(&g.Crib)
	}
	g.Phase = Sacrifice
}

func (g Game) CardsToDealPerPlayer() int {
	switch g.NumPlayers() {
	case 2:
		return 6
	case 3:
		return 5
	case 4:
		return 5
	default:
		return 0
	}
}

func (g Game) CardsDealtToCrib() int {
	if g.NumPlayers() == 3 {
		return 1
	}
	return 0
}

func (g *Game) Shuffle() {
	rand.Shuffle(len(g.Deck), func(i, j int) {
		g.Deck[i], g.Deck[j] = g.Deck[j], g.Deck[i]
	})
}

func (g *Game) Sacrifice(player, card int) {
	hand := g.Players[player].Hand
	if len(hand) <= MinHand {
		return
	}
	c := hand[card]
	g.Players[player].Hand = append(hand[:card], hand[card+1:]...)
	g.Crib = append(g.Crib, c)
}

A gio-extras/outlay/fan/main.go => gio-extras/outlay/fan/main.go +163 -0
@@ 0,0 1,163 @@
package main

import (
	"log"
	"math"
	"math/rand"
	"os"
	"time"

	"gioui.org/app"
	"gioui.org/font/gofont"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"git.sr.ht/~whereswaldon/outlay"
	"gioui.org/example/gio-extras/outlay/fan/playing"
	xwidget "gioui.org/example/gio-extras/outlay/fan/widget"
	"gioui.org/example/gio-extras/outlay/fan/widget/boring"
)

type (
	C = layout.Context
	D = layout.Dimensions
)

func main() {
	go func() {
		w := app.NewWindow()
		if err := loop(w); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}()
	app.Main()
}

func genCards(th *material.Theme) []boring.HoverCard {
	cards := []boring.HoverCard{}
	max := 30
	deck := playing.Deck()
	rand.Shuffle(len(deck), func(i, j int) {
		deck[i], deck[j] = deck[j], deck[i]
	})
	for i := 0; i < max; i++ {
		cards = append(cards, boring.HoverCard{
			CardStyle: boring.CardStyle{
				Card:   deck[i],
				Theme:  th,
				Height: unit.Dp(200),
			},
			HoverState: &xwidget.HoverState{},
		})
	}
	return cards
}

func loop(w *app.Window) error {
	th := material.NewTheme(gofont.Collection())
	fan := outlay.Fan{
		Animation: outlay.Animation{
			Duration: time.Second / 4,
		},
		WidthRadians:  math.Pi,
		OffsetRadians: 2 * math.Pi,
	}
	numCards := widget.Float{}
	numCards.Value = 1.0
	var width, offset, radius widget.Float
	var useRadius widget.Bool
	cardChildren := []outlay.FanItem{}
	cards := genCards(th)
	for i := range cards {
		cardChildren = append(cardChildren, outlay.Item(i == 5, cards[i].Layout))
	}
	var ops op.Ops
	for {
		e := <-w.Events()
		switch e := e.(type) {
		case system.DestroyEvent:
			return e.Err
		case system.FrameEvent:
			gtx := layout.NewContext(&ops, e)
			for i := range cards {
				cardChildren[i].Elevate = cards[i].Hovering(gtx)
			}
			visibleCards := int(math.Round(float64(numCards.Value*float32(len(cardChildren)-1)))) + 1
			fan.OffsetRadians = offset.Value * 2 * math.Pi
			fan.WidthRadians = width.Value * 2 * math.Pi
			if useRadius.Changed() || radius.Changed() {
				if useRadius.Value {
					r := cards[0].Height.Scale(radius.Value * 2)
					fan.HollowRadius = &r
				} else {
					fan.HollowRadius = nil
				}
			}
			layout.Flex{Axis: layout.Vertical}.Layout(gtx,
				layout.Rigid(func(gtx C) D {
					return layout.Flex{}.Layout(gtx,
						layout.Rigid(func(gtx C) D {
							return material.Body1(th, "1").Layout(gtx)
						}),
						layout.Flexed(1, func(gtx C) D {
							return material.Slider(th, &numCards, 0.0, 1.0).Layout(gtx)
						}),
						layout.Rigid(func(gtx C) D {
							return material.Body1(th, "10").Layout(gtx)
						}),
					)
				}),
				layout.Rigid(func(gtx C) D {
					return layout.Flex{}.Layout(gtx,
						layout.Rigid(func(gtx C) D {
							return material.Body1(th, "width 0").Layout(gtx)
						}),
						layout.Flexed(1, func(gtx C) D {
							return material.Slider(th, &width, 0.0, 1.0).Layout(gtx)
						}),
						layout.Rigid(func(gtx C) D {
							return material.Body1(th, "2pi").Layout(gtx)
						}),
					)
				}),
				layout.Rigid(func(gtx C) D {
					return layout.Flex{}.Layout(gtx,
						layout.Rigid(func(gtx C) D {
							return material.Body1(th, "offset 0").Layout(gtx)
						}),
						layout.Flexed(1, func(gtx C) D {
							return material.Slider(th, &offset, 0.0, 1.0).Layout(gtx)
						}),
						layout.Rigid(func(gtx C) D {
							return material.Body1(th, "2pi").Layout(gtx)
						}),
					)
				}),
				layout.Rigid(func(gtx C) D {
					return layout.Flex{}.Layout(gtx,
						layout.Rigid(func(gtx C) D {
							return material.CheckBox(th, &useRadius, "use").Layout(gtx)
						}),
						layout.Rigid(func(gtx C) D {
							return material.Body1(th, "radius 0%").Layout(gtx)
						}),
						layout.Flexed(1, func(gtx C) D {
							return material.Slider(th, &radius, 0.0, 1.0).Layout(gtx)
						}),
						layout.Rigid(func(gtx C) D {
							return material.Body1(th, "200%").Layout(gtx)
						}),
					)
				}),
				layout.Flexed(1, func(gtx C) D {
					return fan.Layout(gtx, cardChildren[:visibleCards]...)
				}),
			)
			e.Frame(gtx.Ops)
		}
	}
}

A gio-extras/outlay/fan/playing/card.go => gio-extras/outlay/fan/playing/card.go +116 -0
@@ 0,0 1,116 @@
/*
Package playing provides types for modeling a deck of conventional
playing cards.
*/
package playing

type Suit uint8
type Rank uint8
type Color bool

const (
	Spades Suit = iota
	Clubs
	Hearts
	Diamonds
	UnknownSuit
)

const (
	Ace Rank = iota
	Two
	Three
	Four
	Five
	Six
	Seven
	Eight
	Nine
	Ten
	Jack
	Queen
	King
	UnknownRank
)

const (
	Red   Color = true
	Black Color = false
)

type Card struct {
	Suit
	Rank
}

func Deck() []Card {
	d := make([]Card, 0, 52)
	for i := 0; i < 4; i++ {
		for k := 0; k < 13; k++ {
			d = append(d, Card{
				Suit: Suit(i),
				Rank: Rank(k),
			})
		}
	}
	return d
}

func (r Rank) String() string {
	switch r {
	case Ace:
		return "A"
	case Two:
		return "2"
	case Three:
		return "3"
	case Four:
		return "4"
	case Five:
		return "5"
	case Six:
		return "6"
	case Seven:
		return "7"
	case Eight:
		return "8"
	case Nine:
		return "9"
	case Ten:
		return "10"
	case Jack:
		return "J"
	case Queen:
		return "Q"
	case King:
		return "K"
	default:
		return "?"
	}
}

func (s Suit) String() string {
	switch s {
	case Spades:
		return "♠"
	case Hearts:
		return "♥"
	case Diamonds:
		return "♦"
	case Clubs:
		return "♣"
	default:
		return "?"
	}
}

func (s Suit) Color() Color {
	switch s {
	case Spades, Clubs:
		return Black
	case Hearts, Diamonds:
		return Red
	default:
		return Black
	}
}

A gio-extras/outlay/fan/widget/boring/cards.go => gio-extras/outlay/fan/widget/boring/cards.go +143 -0
@@ 0,0 1,143 @@
package boring

import (
	"image/color"
	"math"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"gioui.org/example/gio-extras/outlay/fan/playing"
	xwidget "gioui.org/example/gio-extras/outlay/fan/widget"
)

type (
	C = layout.Context
	D = layout.Dimensions
)

type CardPalette struct {
	RedSuit, BlackSuit color.NRGBA
	Border, Background color.NRGBA
}

func (p CardPalette) ColorFor(s playing.Suit) color.NRGBA {
	if s.Color() == playing.Red {
		return p.RedSuit
	}
	return p.BlackSuit
}

var DefaultPalette = &CardPalette{
	RedSuit:    color.NRGBA{R: 0xa0, B: 0x20, A: 0xff},
	BlackSuit:  color.NRGBA{A: 0xff},
	Border:     color.NRGBA{R: 0x80, G: 0x80, B: 0x80, A: 0xff},
	Background: color.NRGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xff},
}

type CardStyle struct {
	*material.Theme
	playing.Card
	Height unit.Value
	*CardPalette
}

const cardHeightToWidth = 14.0 / 9.0
const cardRadiusToWidth = 1.0 / 16.0
const borderWidth = 0.005

func (c *CardStyle) Palette() *CardPalette {
	if c.CardPalette == nil {
		return DefaultPalette
	}
	return c.CardPalette
}

func (c *CardStyle) Layout(gtx C) D {
	gtx.Constraints.Max.Y = gtx.Px(c.Height)
	gtx.Constraints.Max.X = int(float32(gtx.Constraints.Max.Y) / cardHeightToWidth)
	outerRadius := float32(gtx.Constraints.Max.X) * cardRadiusToWidth
	innerRadius := (1 - borderWidth) * outerRadius

	borderWidth := c.Height.Scale(borderWidth)
	return layout.Stack{}.Layout(gtx,
		layout.Expanded(func(gtx C) D {
			return Rect{
				Color: c.Palette().Border,
				Size:  layout.FPt(gtx.Constraints.Max),
				Radii: outerRadius,
			}.Layout(gtx)
		}),
		layout.Stacked(func(gtx C) D {
			return layout.UniformInset(borderWidth).Layout(gtx, func(gtx C) D {
				return layout.Stack{}.Layout(gtx,
					layout.Expanded(func(gtx C) D {
						return Rect{
							Color: c.Palette().Background,
							Size:  layout.FPt(gtx.Constraints.Max),
							Radii: innerRadius,
						}.Layout(gtx)
					}),
					layout.Stacked(func(gtx C) D {
						return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx C) D {
							defer op.Push(gtx.Ops).Pop()
							gtx.Constraints.Min = gtx.Constraints.Max
							origin := f32.Point{
								X: float32(gtx.Constraints.Max.X / 2),
								Y: float32(gtx.Constraints.Max.Y / 2),
							}
							layout.Center.Layout(gtx, func(gtx C) D {
								face := material.H1(c.Theme, c.Rank.String())
								face.Color = c.Palette().ColorFor(c.Suit)
								return face.Layout(gtx)
							})
							c.layoutCorner(gtx)
							op.Affine(f32.Affine2D{}.Rotate(origin, math.Pi)).Add(gtx.Ops)
							c.layoutCorner(gtx)

							return D{Size: gtx.Constraints.Max}
						})
					}),
				)
			})
		}),
	)
}

func (c *CardStyle) layoutCorner(gtx layout.Context) layout.Dimensions {
	col := c.Palette().ColorFor(c.Suit)
	return layout.NW.Layout(gtx, func(gtx C) D {
		return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
			return layout.Flex{
				Axis:      layout.Vertical,
				Alignment: layout.Middle,
			}.Layout(gtx,
				layout.Rigid(func(gtx C) D {
					label := material.H6(c.Theme, c.Rank.String())
					label.Color = col
					return label.Layout(gtx)
				}),
				layout.Rigid(func(gtx C) D {
					label := material.H6(c.Theme, c.Suit.String())
					label.Color = col
					return label.Layout(gtx)
				}),
			)
		})
	})
}

type HoverCard struct {
	CardStyle
	*xwidget.HoverState
}

func (h HoverCard) Layout(gtx C) D {
	dims := h.CardStyle.Layout(gtx)
	gtx.Constraints.Max = dims.Size
	gtx.Constraints.Min = dims.Size
	h.HoverState.Layout(gtx)
	return dims
}

A gio-extras/outlay/fan/widget/boring/rect.go => gio-extras/outlay/fan/widget/boring/rect.go +34 -0
@@ 0,0 1,34 @@
package boring

import (
	"image"
	"image/color"

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

// Rect creates a rectangle of the provided background color with
// Dimensions specified by size and a corner radius (on all corners)
// specified by radii.
type Rect struct {
	Color color.NRGBA
	Size  f32.Point
	Radii float32
}

// Layout renders the Rect into the provided context
func (r Rect) Layout(gtx C) D {
	return DrawRect(gtx, r.Color, r.Size, r.Radii)
}

// DrawRect creates a rectangle of the provided background color with
// Dimensions specified by size and a corner radius (on all corners)
// specified by radii.
func DrawRect(gtx C, background color.NRGBA, size f32.Point, radii float32) D {
	bounds := f32.Rectangle{Max: size}
	paint.FillShape(gtx.Ops, background, clip.UniformRRect(bounds, radii).Op(gtx.Ops))
	return layout.Dimensions{Size: image.Pt(int(size.X), int(size.Y))}
}

A gio-extras/outlay/fan/widget/state.go => gio-extras/outlay/fan/widget/state.go +49 -0
@@ 0,0 1,49 @@
package widget

import (
	"image"

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

type (
	C = layout.Context
	D = layout.Dimensions
)

type HoverState struct {
	hovering bool
}

func (c *HoverState) Hovering(gtx C) bool {
	start := c.hovering
	for _, ev := range gtx.Events(c) {
		switch ev := ev.(type) {
		case pointer.Event:
			switch ev.Type {
			case pointer.Enter:
				c.hovering = true
			case pointer.Leave:
				c.hovering = false
			case pointer.Cancel:
				c.hovering = false
			}
		}
	}
	if c.hovering != start {
		op.InvalidateOp{}.Add(gtx.Ops)
	}
	return c.hovering
}

func (c *HoverState) Layout(gtx C) D {
	defer op.Push(gtx.Ops).Pop()
	pointer.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Add(gtx.Ops)
	pointer.InputOp{
		Tag:   c,
		Types: pointer.Enter | pointer.Leave,
	}.Add(gtx.Ops)
	return D{Size: gtx.Constraints.Max}
}

A gio-extras/outlay/grid/main.go => gio-extras/outlay/grid/main.go +223 -0
@@ 0,0 1,223 @@
package main

import (
	"fmt"
	"image"
	"image/color"
	"log"
	"os"
	"strconv"

	"gioui.org/app"
	"gioui.org/f32"
	"gioui.org/font/gofont"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"

	"git.sr.ht/~whereswaldon/outlay"
)

func main() {
	go func() {
		w := app.NewWindow(
			app.Size(unit.Dp(800), unit.Dp(400)),
			app.Title("Gio layouts"),
		)
		if err := loop(w); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}()
	app.Main()
}

func loop(w *app.Window) error {
	ui := newUI()

	var ops op.Ops
	for e := range w.Events() {
		switch e := e.(type) {
		case system.DestroyEvent:
			return e.Err

		case system.FrameEvent:
			gtx := layout.NewContext(&ops, e)
			ui.Layout(gtx)
			e.Frame(gtx.Ops)
		}
	}
	return nil
}

type UI struct {
	theme  *material.Theme
	active int
	tabs   []uiTab
	list   layout.List
}

type uiTab struct {
	name  string
	click widget.Clickable
	text  string
	w     func(tab *uiTab, gtx layout.Context) layout.Dimensions
	num   int
	ed    widget.Editor
}

var (
	vWrap = outlay.GridWrap{
		Axis:      layout.Vertical,
		Alignment: layout.End,
	}
	hWrap = outlay.GridWrap{
		Axis:      layout.Horizontal,
		Alignment: layout.End,
	}
	vGrid = outlay.Grid{
		Num:  11,
		Axis: layout.Vertical,
	}
	hGrid = outlay.Grid{
		Num:  11,
		Axis: layout.Horizontal,
	}
)

func newUI() *UI {
	ui := &UI{
		theme: material.NewTheme(gofont.Collection()),
		list: layout.List{
			Axis:      layout.Horizontal,
			Alignment: layout.Baseline,
		},
	}
	ui.tabs = append(ui.tabs,
		uiTab{
			name: "V wrap",
			text: "Lay out items vertically before wrapping to the next column.",
			w: func(tab *uiTab, gtx layout.Context) layout.Dimensions {
				return vWrap.Layout(gtx, tab.num, func(gtx layout.Context, i int) layout.Dimensions {
					s := fmt.Sprintf("item %d", i)
					return material.Body1(ui.theme, s).Layout(gtx)
				})
			},
		},
		uiTab{
			name: "H wrap",
			text: "Lay out items horizontally before wrapping to the next row.",
			w: func(tab *uiTab, gtx layout.Context) layout.Dimensions {
				return hWrap.Layout(gtx, tab.num, func(gtx layout.Context, i int) layout.Dimensions {
					s := fmt.Sprintf("item %d", i)
					return material.Body1(ui.theme, s).Layout(gtx)
				})
			},
		},
		uiTab{
			name: "V grid",
			text: fmt.Sprintf("Lay out %d items vertically before going to the next column.", vGrid.Num),
			w: func(tab *uiTab, gtx layout.Context) layout.Dimensions {
				return vGrid.Layout(gtx, tab.num, func(gtx layout.Context, i int) layout.Dimensions {
					s := fmt.Sprintf("item %d", i)
					return material.Body1(ui.theme, s).Layout(gtx)
				})
			},
		},
		uiTab{
			name: "H grid",
			text: fmt.Sprintf("Lay out %d items horizontally before going to the next row.", hGrid.Num),
			w: func(tab *uiTab, gtx layout.Context) layout.Dimensions {
				return hGrid.Layout(gtx, tab.num, func(gtx layout.Context, i int) layout.Dimensions {
					s := fmt.Sprintf("item %d", i)
					return material.Body1(ui.theme, s).Layout(gtx)
				})
			},
		},
	)
	for i := range ui.tabs {
		tab := &ui.tabs[i]
		tab.ed = widget.Editor{
			SingleLine: true,
			Submit:     true,
		}
		tab.num = 99
		tab.ed.SetText(strconv.Itoa(tab.num))
	}
	return ui
}

func (ui *UI) Layout(gtx layout.Context) layout.Dimensions {
	for i := range ui.tabs {
		for ui.tabs[i].click.Clicked() {
			ui.active = i
		}
	}
	activeTab := &ui.tabs[ui.active]
	return layout.Flex{
		Axis: layout.Vertical,
	}.Layout(gtx,
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			return ui.list.Layout(gtx, len(ui.tabs), func(gtx layout.Context, idx int) layout.Dimensions {
				tab := &ui.tabs[idx]
				title := func(gtx layout.Context) layout.Dimensions {
					return layout.UniformInset(unit.Dp(6)).Layout(gtx, material.H6(ui.theme, tab.name).Layout)
				}
				if idx != ui.active {
					return material.Clickable(gtx, &tab.click, title)
				}
				return layout.Stack{}.Layout(gtx,
					layout.Expanded(func(gtx layout.Context) layout.Dimensions {
						clip.UniformRRect(f32.Rectangle{
							Max: layout.FPt(gtx.Constraints.Min),
						}, 0).Add(gtx.Ops)
						paint.Fill(gtx.Ops, color.NRGBA{A: 64})
						return layout.Dimensions{}
					}),
					layout.Stacked(title),
				)
			})
		}),
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			pt := image.Point{X: gtx.Constraints.Max.X, Y: 4}
			clip.UniformRRect(f32.Rectangle{
				Max: layout.FPt(pt),
			}, 0).Add(gtx.Ops)
			paint.Fill(gtx.Ops, ui.theme.Palette.ContrastBg)
			return layout.Dimensions{Size: pt}
		}),
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			return layout.Stack{}.Layout(gtx,
				layout.Expanded(func(gtx layout.Context) layout.Dimensions {
					clip.UniformRRect(f32.Rectangle{
						Max: layout.FPt(image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Min.Y)),
					}, 0).Add(gtx.Ops)
					paint.Fill(gtx.Ops, color.NRGBA{A: 24})
					return layout.Dimensions{}
				}),
				layout.Stacked(func(gtx layout.Context) layout.Dimensions {
					return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
						if x, _ := strconv.Atoi(activeTab.ed.Text()); x != activeTab.num {
							activeTab.num = x
						}
						return layout.Flex{
							Alignment: layout.Baseline,
						}.Layout(gtx,
							layout.Rigid(material.Body1(ui.theme, activeTab.text).Layout),
							layout.Rigid(material.Body1(ui.theme, " Num = ").Layout),
							layout.Rigid(material.Editor(ui.theme, &activeTab.ed, "").Layout),
						)
					})
				}),
			)
		}),
		layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
			return activeTab.w(activeTab, gtx)
		}),
	)
}

M go.mod => go.mod +1 -0
@@ 7,6 7,7 @@ require (
	git.sr.ht/~whereswaldon/colorpicker v0.0.0-20201207220634-905cd7cc7248
	git.sr.ht/~whereswaldon/haptic v0.0.0-20201207220958-78675dee81dd
	git.sr.ht/~whereswaldon/niotify v0.0.3
	git.sr.ht/~whereswaldon/outlay v0.0.0-20201207220906-cbe824700857
	github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7
	github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4
	github.com/google/go-github/v24 v24.0.1

M go.sum => go.sum +7 -0
@@ 10,6 10,8 @@ git.sr.ht/~whereswaldon/haptic v0.0.0-20201207220958-78675dee81dd h1:xTijdESZL/k
git.sr.ht/~whereswaldon/haptic v0.0.0-20201207220958-78675dee81dd/go.mod h1:lFvegCF1P7IXfv5FpnnvKFdoAQWTgJZhx8aWOBgE0yg=
git.sr.ht/~whereswaldon/niotify v0.0.3 h1:EWRqPOzqTLU92A9h207LkS/U/nQxuawJ0PF7UEDApi0=
git.sr.ht/~whereswaldon/niotify v0.0.3/go.mod h1:itJ9vAQqq8+liURizx7mAdIY4o8gRDF6SAVfswYVg1U=
git.sr.ht/~whereswaldon/outlay v0.0.0-20201207220906-cbe824700857 h1:Sc+1cZRrwGyiBYgqIto5OlK+RTea1T7FYmZj+JC6RZI=
git.sr.ht/~whereswaldon/outlay v0.0.0-20201207220906-cbe824700857/go.mod h1:basiujMeRXbgNAivgbiWlxy+gsS0oO70suSRSZ96Nw4=
git.wow.st/gmp/jni v0.0.0-20200619201040-d0d3f316ae09/go.mod h1:lfKZKu2afBKJODTFgDNIaBfhq6qmQS0xsS/phLO5Urk=
git.wow.st/gmp/jni v0.0.0-20200827154156-014cd5c7c4c0 h1:Ynp3h+TC8k1clvf45D28VFQlmy0bPx8M/MG5bB24Vj8=
git.wow.st/gmp/jni v0.0.0-20200827154156-014cd5c7c4c0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=


@@ 38,6 40,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
golang.org/x/exp v0.0.0-20201203231725-fa01524bc59d h1:FscZqdyN/qhN9in1p2FLXl6vsrWY792O5bak6GHqVs0=
golang.org/x/exp v0.0.0-20201203231725-fa01524bc59d/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=


@@ 68,10 71,14 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88 h1:KmZPnMocC93w341XZp26yTJg8Za7lhb2KhkYmixoeso=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=