mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
all: rename the gioui.org/ui module to gioui.org
The "ui" is redundant and stutters. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
/*
|
||||
Package layout implements layouts common to GUI programs.
|
||||
|
||||
Constraints and dimensions
|
||||
|
||||
Constraints and dimensions form the interface between layouts and
|
||||
interface child elements. This package operates on Widgets, functions
|
||||
that compute Dimensions from a a set of constraints for acceptable
|
||||
widths and heights. Both the constraints and dimensions are maintained
|
||||
in an implicit Context to keep the Widget declaration short.
|
||||
|
||||
For example, to add space above a widget:
|
||||
|
||||
gtx := &layout.Context{...}
|
||||
gtx.Reset(...)
|
||||
|
||||
// Configure a top inset.
|
||||
inset := layout.Inset{Top: ui.Dp(8), ...}
|
||||
// Use the inset to lay out a widget.
|
||||
inset.Layout(gtx, func() {
|
||||
// Lay out widget and determine its size given the constraints.
|
||||
...
|
||||
dims := layout.Dimensions{...}
|
||||
gtx.Dimensions = dims
|
||||
})
|
||||
|
||||
Note that the example does not generate any garbage even though the
|
||||
Inset is transient. Layouts that don't accept user input are designed
|
||||
to not escape to the heap during their use.
|
||||
|
||||
Layout operations are recursive: a child in a layout operation can
|
||||
itself be another layout. That way, complex user interfaces can
|
||||
be created from a few generic layouts.
|
||||
|
||||
This example both aligns and insets a child:
|
||||
|
||||
inset := layout.Inset{...}
|
||||
inset.Layout(gtx, func() {
|
||||
align := layout.Align(...)
|
||||
align.Layout(gtx, func() {
|
||||
widget.Layout(gtx, ...)
|
||||
})
|
||||
})
|
||||
|
||||
More complex layouts such as Stack and Flex lay out multiple children,
|
||||
and stateful layouts such as List accept user input.
|
||||
|
||||
*/
|
||||
package layout
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package layout
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/ui"
|
||||
)
|
||||
|
||||
// Flex lays out child elements along an axis,
|
||||
// according to alignment and weights.
|
||||
type Flex struct {
|
||||
// Axis is the main axis, either Horizontal or Vertical.
|
||||
Axis Axis
|
||||
// Spacing controls the distribution of space left after
|
||||
// layout.
|
||||
Spacing Spacing
|
||||
// Alignment is the alignment in the cross axis.
|
||||
Alignment Alignment
|
||||
|
||||
ctx *Context
|
||||
macro ui.MacroOp
|
||||
mode flexMode
|
||||
size int
|
||||
rigidSize int
|
||||
// fraction is the rounding error from a Flexible weighting.
|
||||
fraction float32
|
||||
maxCross int
|
||||
maxBaseline int
|
||||
}
|
||||
|
||||
// FlexChild is the layout result of a call End.
|
||||
type FlexChild struct {
|
||||
macro ui.MacroOp
|
||||
dims Dimensions
|
||||
}
|
||||
|
||||
// Spacing determine the spacing mode for a Flex.
|
||||
type Spacing uint8
|
||||
|
||||
type flexMode uint8
|
||||
|
||||
const (
|
||||
// SpaceEnd leaves space at the end.
|
||||
SpaceEnd Spacing = iota
|
||||
// SpaceStart leaves space at the start.
|
||||
SpaceStart
|
||||
// SpaceSides shares space between the start and end.
|
||||
SpaceSides
|
||||
// SpaceAround distributes space evenly between children,
|
||||
// with half as much space at the start and end.
|
||||
SpaceAround
|
||||
// SpaceBetween distributes space evenly between children,
|
||||
// leaving no space at the start and end.
|
||||
SpaceBetween
|
||||
// SpaceEvenly distributes space evenly between children and
|
||||
// at the start and end.
|
||||
SpaceEvenly
|
||||
)
|
||||
|
||||
const (
|
||||
modeNone flexMode = iota
|
||||
modeBegun
|
||||
modeRigid
|
||||
modeFlex
|
||||
)
|
||||
|
||||
// Init must be called before Rigid or Flexible.
|
||||
func (f *Flex) Init(gtx *Context) *Flex {
|
||||
if f.mode > modeBegun {
|
||||
panic("must End the current child before calling Init again")
|
||||
}
|
||||
f.mode = modeBegun
|
||||
f.ctx = gtx
|
||||
f.size = 0
|
||||
f.rigidSize = 0
|
||||
f.maxCross = 0
|
||||
f.maxBaseline = 0
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *Flex) begin(mode flexMode) {
|
||||
switch {
|
||||
case f.mode == modeNone:
|
||||
panic("must Init before adding a child")
|
||||
case f.mode > modeBegun:
|
||||
panic("must End before adding a child")
|
||||
}
|
||||
f.mode = mode
|
||||
f.macro.Record(f.ctx.Ops)
|
||||
}
|
||||
|
||||
// Rigid lays out a widget with the main axis constrained to the range
|
||||
// from 0 to the remaining space.
|
||||
func (f *Flex) Rigid(w Widget) FlexChild {
|
||||
f.begin(modeRigid)
|
||||
cs := f.ctx.Constraints
|
||||
mainc := axisMainConstraint(f.Axis, cs)
|
||||
mainMax := mainc.Max - f.size
|
||||
if mainMax < 0 {
|
||||
mainMax = 0
|
||||
}
|
||||
cs = axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, cs))
|
||||
dims := f.ctx.Layout(cs, w)
|
||||
return f.end(dims)
|
||||
}
|
||||
|
||||
// Flexible is like Rigid, where the main axis size is also constrained to a
|
||||
// fraction of the space not taken up by Rigid children.
|
||||
func (f *Flex) Flexible(weight float32, w Widget) FlexChild {
|
||||
f.begin(modeFlex)
|
||||
cs := f.ctx.Constraints
|
||||
mainc := axisMainConstraint(f.Axis, cs)
|
||||
var flexSize int
|
||||
if mainc.Max > f.size {
|
||||
flexSize = mainc.Max - f.rigidSize
|
||||
// Apply weight and add any leftover fraction from a
|
||||
// previous Flexible.
|
||||
size := float32(flexSize)*weight + f.fraction
|
||||
flexSize = int(size + .5)
|
||||
f.fraction = size - float32(flexSize)
|
||||
if max := mainc.Max - f.size; flexSize > max {
|
||||
flexSize = max
|
||||
}
|
||||
}
|
||||
submainc := Constraint{Max: flexSize}
|
||||
cs = axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, cs))
|
||||
dims := f.ctx.Layout(cs, w)
|
||||
return f.end(dims)
|
||||
}
|
||||
|
||||
// End a child by specifying its dimensions. Pass the returned layout result
|
||||
// to Layout.
|
||||
func (f *Flex) end(dims Dimensions) FlexChild {
|
||||
if f.mode <= modeBegun {
|
||||
panic("End called without an active child")
|
||||
}
|
||||
f.macro.Stop()
|
||||
sz := axisMain(f.Axis, dims.Size)
|
||||
f.size += sz
|
||||
if f.mode == modeRigid {
|
||||
f.rigidSize += sz
|
||||
}
|
||||
f.mode = modeBegun
|
||||
if c := axisCross(f.Axis, dims.Size); c > f.maxCross {
|
||||
f.maxCross = c
|
||||
}
|
||||
if b := dims.Baseline; b > f.maxBaseline {
|
||||
f.maxBaseline = b
|
||||
}
|
||||
return FlexChild{f.macro, dims}
|
||||
}
|
||||
|
||||
// Layout a list of children. The order of the children determines their laid
|
||||
// out order.
|
||||
func (f *Flex) Layout(children ...FlexChild) {
|
||||
cs := f.ctx.Constraints
|
||||
mainc := axisMainConstraint(f.Axis, cs)
|
||||
crossSize := axisCrossConstraint(f.Axis, cs).Constrain(f.maxCross)
|
||||
var space int
|
||||
if mainc.Min > f.size {
|
||||
space = mainc.Min - f.size
|
||||
}
|
||||
var mainSize int
|
||||
var baseline int
|
||||
switch f.Spacing {
|
||||
case SpaceSides:
|
||||
mainSize += space / 2
|
||||
case SpaceStart:
|
||||
mainSize += space
|
||||
case SpaceEvenly:
|
||||
mainSize += space / (1 + len(children))
|
||||
case SpaceAround:
|
||||
mainSize += space / (len(children) * 2)
|
||||
}
|
||||
for i, child := range children {
|
||||
dims := child.dims
|
||||
b := dims.Baseline
|
||||
var cross int
|
||||
switch f.Alignment {
|
||||
case End:
|
||||
cross = crossSize - axisCross(f.Axis, dims.Size)
|
||||
case Middle:
|
||||
cross = (crossSize - axisCross(f.Axis, dims.Size)) / 2
|
||||
case Baseline:
|
||||
if f.Axis == Horizontal {
|
||||
cross = f.maxBaseline - b
|
||||
}
|
||||
}
|
||||
var stack ui.StackOp
|
||||
stack.Push(f.ctx.Ops)
|
||||
ui.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(f.ctx.Ops)
|
||||
child.macro.Add(f.ctx.Ops)
|
||||
stack.Pop()
|
||||
mainSize += axisMain(f.Axis, dims.Size)
|
||||
if i < len(children)-1 {
|
||||
switch f.Spacing {
|
||||
case SpaceEvenly:
|
||||
mainSize += space / (1 + len(children))
|
||||
case SpaceAround:
|
||||
mainSize += space / len(children)
|
||||
case SpaceBetween:
|
||||
mainSize += space / (len(children) - 1)
|
||||
}
|
||||
}
|
||||
if b != dims.Size.Y {
|
||||
baseline = b
|
||||
}
|
||||
}
|
||||
switch f.Spacing {
|
||||
case SpaceSides:
|
||||
mainSize += space / 2
|
||||
case SpaceEnd:
|
||||
mainSize += space
|
||||
case SpaceEvenly:
|
||||
mainSize += space / (1 + len(children))
|
||||
case SpaceAround:
|
||||
mainSize += space / (len(children) * 2)
|
||||
}
|
||||
sz := axisPoint(f.Axis, mainSize, crossSize)
|
||||
if baseline == 0 {
|
||||
baseline = sz.Y
|
||||
}
|
||||
f.ctx.Dimensions = Dimensions{Size: sz, Baseline: baseline}
|
||||
}
|
||||
|
||||
func axisPoint(a Axis, main, cross int) image.Point {
|
||||
if a == Horizontal {
|
||||
return image.Point{main, cross}
|
||||
} else {
|
||||
return image.Point{cross, main}
|
||||
}
|
||||
}
|
||||
|
||||
func axisMain(a Axis, sz image.Point) int {
|
||||
if a == Horizontal {
|
||||
return sz.X
|
||||
} else {
|
||||
return sz.Y
|
||||
}
|
||||
}
|
||||
|
||||
func axisCross(a Axis, sz image.Point) int {
|
||||
if a == Horizontal {
|
||||
return sz.Y
|
||||
} else {
|
||||
return sz.X
|
||||
}
|
||||
}
|
||||
|
||||
func axisMainConstraint(a Axis, cs Constraints) Constraint {
|
||||
if a == Horizontal {
|
||||
return cs.Width
|
||||
} else {
|
||||
return cs.Height
|
||||
}
|
||||
}
|
||||
|
||||
func axisCrossConstraint(a Axis, cs Constraints) Constraint {
|
||||
if a == Horizontal {
|
||||
return cs.Height
|
||||
} else {
|
||||
return cs.Width
|
||||
}
|
||||
}
|
||||
|
||||
func axisConstraints(a Axis, mainc, crossc Constraint) Constraints {
|
||||
if a == Horizontal {
|
||||
return Constraints{Width: mainc, Height: crossc}
|
||||
} else {
|
||||
return Constraints{Width: crossc, Height: mainc}
|
||||
}
|
||||
}
|
||||
|
||||
func toPointF(p image.Point) f32.Point {
|
||||
return f32.Point{X: float32(p.X), Y: float32(p.Y)}
|
||||
}
|
||||
|
||||
func (s Spacing) String() string {
|
||||
switch s {
|
||||
case SpaceEnd:
|
||||
return "SpaceEnd"
|
||||
case SpaceStart:
|
||||
return "SpaceStart"
|
||||
case SpaceSides:
|
||||
return "SpaceSides"
|
||||
case SpaceAround:
|
||||
return "SpaceAround"
|
||||
case SpaceBetween:
|
||||
return "SpaceAround"
|
||||
case SpaceEvenly:
|
||||
return "SpaceEvenly"
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package layout
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/ui"
|
||||
)
|
||||
|
||||
// Constraints represent a set of acceptable ranges for
|
||||
// a widget's width and height.
|
||||
type Constraints struct {
|
||||
Width Constraint
|
||||
Height Constraint
|
||||
}
|
||||
|
||||
// Constraint is a range of acceptable sizes in a single
|
||||
// dimension.
|
||||
type Constraint struct {
|
||||
Min, Max int
|
||||
}
|
||||
|
||||
// Dimensions are the resolved size and baseline for a widget.
|
||||
type Dimensions struct {
|
||||
Size image.Point
|
||||
Baseline int
|
||||
}
|
||||
|
||||
// Axis is the Horizontal or Vertical direction.
|
||||
type Axis uint8
|
||||
|
||||
// Alignment is the mutual alignment of a list of widgets.
|
||||
type Alignment uint8
|
||||
|
||||
// Direction is the alignment of widgets relative to a containing
|
||||
// space.
|
||||
type Direction uint8
|
||||
|
||||
// Widget is a function scope for drawing, processing events and
|
||||
// computing dimensions for a user interface element.
|
||||
type Widget func()
|
||||
|
||||
// Context carry the state needed by almost all layouts and widgets.
|
||||
type Context struct {
|
||||
// Constraints track the constraints for the active widget or
|
||||
// layout.
|
||||
Constraints Constraints
|
||||
// Dimensions track the result of the most recent layout
|
||||
// operation.
|
||||
Dimensions Dimensions
|
||||
|
||||
ui.Config
|
||||
ui.Queue
|
||||
*ui.Ops
|
||||
}
|
||||
|
||||
const (
|
||||
Start Alignment = iota
|
||||
End
|
||||
Middle
|
||||
Baseline
|
||||
)
|
||||
|
||||
const (
|
||||
NW Direction = iota
|
||||
N
|
||||
NE
|
||||
E
|
||||
SE
|
||||
S
|
||||
SW
|
||||
W
|
||||
Center
|
||||
)
|
||||
|
||||
const (
|
||||
Horizontal Axis = iota
|
||||
Vertical
|
||||
)
|
||||
|
||||
// Layout a widget with a set of constraints and return its
|
||||
// dimensions. The previous constraints are restored after layout.
|
||||
func (s *Context) Layout(cs Constraints, w Widget) Dimensions {
|
||||
saved := s.Constraints
|
||||
s.Constraints = cs
|
||||
s.Dimensions = Dimensions{}
|
||||
w()
|
||||
s.Constraints = saved
|
||||
return s.Dimensions
|
||||
}
|
||||
|
||||
// Reset the context.
|
||||
func (c *Context) Reset(cfg ui.Config, cs Constraints) {
|
||||
c.Constraints = cs
|
||||
c.Dimensions = Dimensions{}
|
||||
c.Config = cfg
|
||||
if c.Ops == nil {
|
||||
c.Ops = new(ui.Ops)
|
||||
}
|
||||
c.Ops.Reset()
|
||||
}
|
||||
|
||||
// Constrain a value to the range [Min; Max].
|
||||
func (c Constraint) Constrain(v int) int {
|
||||
if v < c.Min {
|
||||
return c.Min
|
||||
} else if v > c.Max {
|
||||
return c.Max
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Constrain a size to the Width and Height ranges.
|
||||
func (c Constraints) Constrain(size image.Point) image.Point {
|
||||
return image.Point{X: c.Width.Constrain(size.X), Y: c.Height.Constrain(size.Y)}
|
||||
}
|
||||
|
||||
// RigidConstraints returns the constraints that can only be
|
||||
// satisfied by the given dimensions.
|
||||
func RigidConstraints(size image.Point) Constraints {
|
||||
return Constraints{
|
||||
Width: Constraint{Min: size.X, Max: size.X},
|
||||
Height: Constraint{Min: size.Y, Max: size.Y},
|
||||
}
|
||||
}
|
||||
|
||||
// Inset adds space around a widget.
|
||||
type Inset struct {
|
||||
Top, Right, Bottom, Left ui.Value
|
||||
}
|
||||
|
||||
// Align aligns a widget in the available space.
|
||||
type Align Direction
|
||||
|
||||
// Layout a widget.
|
||||
func (in Inset) Layout(gtx *Context, w Widget) {
|
||||
top := gtx.Px(in.Top)
|
||||
right := gtx.Px(in.Right)
|
||||
bottom := gtx.Px(in.Bottom)
|
||||
left := gtx.Px(in.Left)
|
||||
mcs := gtx.Constraints
|
||||
mcs.Width.Min -= left + right
|
||||
mcs.Width.Max -= left + right
|
||||
if mcs.Width.Min < 0 {
|
||||
mcs.Width.Min = 0
|
||||
}
|
||||
if mcs.Width.Max < mcs.Width.Min {
|
||||
mcs.Width.Max = mcs.Width.Min
|
||||
}
|
||||
mcs.Height.Min -= top + bottom
|
||||
mcs.Height.Max -= top + bottom
|
||||
if mcs.Height.Min < 0 {
|
||||
mcs.Height.Min = 0
|
||||
}
|
||||
if mcs.Height.Max < mcs.Height.Min {
|
||||
mcs.Height.Max = mcs.Height.Min
|
||||
}
|
||||
var stack ui.StackOp
|
||||
stack.Push(gtx.Ops)
|
||||
ui.TransformOp{}.Offset(toPointF(image.Point{X: left, Y: top})).Add(gtx.Ops)
|
||||
dims := gtx.Layout(mcs, w)
|
||||
stack.Pop()
|
||||
gtx.Dimensions = Dimensions{
|
||||
Size: gtx.Constraints.Constrain(dims.Size.Add(image.Point{X: right + left, Y: top + bottom})),
|
||||
Baseline: dims.Baseline + top,
|
||||
}
|
||||
}
|
||||
|
||||
// UniformInset returns an Inset with a single inset applied to all
|
||||
// edges.
|
||||
func UniformInset(v ui.Value) Inset {
|
||||
return Inset{Top: v, Right: v, Bottom: v, Left: v}
|
||||
}
|
||||
|
||||
// Layout a widget.
|
||||
func (a Align) Layout(gtx *Context, w Widget) {
|
||||
var macro ui.MacroOp
|
||||
macro.Record(gtx.Ops)
|
||||
cs := gtx.Constraints
|
||||
mcs := cs
|
||||
mcs.Width.Min = 0
|
||||
mcs.Height.Min = 0
|
||||
dims := gtx.Layout(mcs, w)
|
||||
macro.Stop()
|
||||
sz := dims.Size
|
||||
if sz.X < cs.Width.Min {
|
||||
sz.X = cs.Width.Min
|
||||
}
|
||||
if sz.Y < cs.Height.Min {
|
||||
sz.Y = cs.Height.Min
|
||||
}
|
||||
var p image.Point
|
||||
switch Direction(a) {
|
||||
case N, S, Center:
|
||||
p.X = (sz.X - dims.Size.X) / 2
|
||||
case NE, SE, E:
|
||||
p.X = sz.X - dims.Size.X
|
||||
}
|
||||
switch Direction(a) {
|
||||
case W, Center, E:
|
||||
p.Y = (sz.Y - dims.Size.Y) / 2
|
||||
case SW, S, SE:
|
||||
p.Y = sz.Y - dims.Size.Y
|
||||
}
|
||||
var stack ui.StackOp
|
||||
stack.Push(gtx.Ops)
|
||||
ui.TransformOp{}.Offset(toPointF(p)).Add(gtx.Ops)
|
||||
macro.Add(gtx.Ops)
|
||||
stack.Pop()
|
||||
gtx.Dimensions = Dimensions{
|
||||
Size: sz,
|
||||
Baseline: dims.Baseline,
|
||||
}
|
||||
}
|
||||
|
||||
func (a Alignment) String() string {
|
||||
switch a {
|
||||
case Start:
|
||||
return "Start"
|
||||
case End:
|
||||
return "End"
|
||||
case Middle:
|
||||
return "Middle"
|
||||
case Baseline:
|
||||
return "Baseline"
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func (a Axis) String() string {
|
||||
switch a {
|
||||
case Horizontal:
|
||||
return "Horizontal"
|
||||
case Vertical:
|
||||
return "Vertical"
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func (d Direction) String() string {
|
||||
switch d {
|
||||
case NW:
|
||||
return "NW"
|
||||
case N:
|
||||
return "N"
|
||||
case NE:
|
||||
return "NE"
|
||||
case E:
|
||||
return "E"
|
||||
case SE:
|
||||
return "SE"
|
||||
case S:
|
||||
return "S"
|
||||
case SW:
|
||||
return "SW"
|
||||
case W:
|
||||
return "W"
|
||||
case Center:
|
||||
return "Center"
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package layout_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/ui"
|
||||
)
|
||||
|
||||
type queue struct{}
|
||||
|
||||
type config struct{}
|
||||
|
||||
var q queue
|
||||
var cfg = new(config)
|
||||
|
||||
func ExampleInset() {
|
||||
gtx := &layout.Context{Queue: q}
|
||||
// Loose constraints with no minimal size.
|
||||
var cs layout.Constraints
|
||||
cs.Width.Max = 100
|
||||
cs.Height.Max = 100
|
||||
gtx.Reset(cfg, cs)
|
||||
|
||||
// Inset all edges by 10.
|
||||
inset := layout.UniformInset(ui.Dp(10))
|
||||
inset.Layout(gtx, func() {
|
||||
// Lay out a 50x50 sized widget.
|
||||
layoutWidget(gtx, 50, 50)
|
||||
fmt.Println(gtx.Dimensions.Size)
|
||||
})
|
||||
|
||||
fmt.Println(gtx.Dimensions.Size)
|
||||
|
||||
// Output:
|
||||
// (50,50)
|
||||
// (70,70)
|
||||
}
|
||||
|
||||
func ExampleAlign() {
|
||||
gtx := &layout.Context{Queue: q}
|
||||
// Rigid constraints with both minimum and maximum set.
|
||||
cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
|
||||
gtx.Reset(cfg, cs)
|
||||
|
||||
align := layout.Align(layout.Center)
|
||||
align.Layout(gtx, func() {
|
||||
// Lay out a 50x50 sized widget.
|
||||
layoutWidget(gtx, 50, 50)
|
||||
fmt.Println(gtx.Dimensions.Size)
|
||||
})
|
||||
|
||||
fmt.Println(gtx.Dimensions.Size)
|
||||
|
||||
// Output:
|
||||
// (50,50)
|
||||
// (100,100)
|
||||
}
|
||||
|
||||
func ExampleFlex() {
|
||||
gtx := &layout.Context{Queue: q}
|
||||
cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
|
||||
gtx.Reset(cfg, cs)
|
||||
|
||||
flex := layout.Flex{}
|
||||
flex.Init(gtx)
|
||||
|
||||
// Rigid 10x10 widget.
|
||||
child1 := flex.Rigid(func() {
|
||||
fmt.Printf("Rigid: %v\n", gtx.Constraints.Width)
|
||||
layoutWidget(gtx, 10, 10)
|
||||
})
|
||||
|
||||
// Child with 50% space allowance.
|
||||
child2 := flex.Flexible(0.5, func() {
|
||||
fmt.Printf("50%%: %v\n", gtx.Constraints.Width)
|
||||
layoutWidget(gtx, 10, 10)
|
||||
})
|
||||
|
||||
flex.Layout(child1, child2)
|
||||
|
||||
// Output:
|
||||
// Rigid: {0 100}
|
||||
// 50%: {0 45}
|
||||
}
|
||||
|
||||
func ExampleStack() {
|
||||
gtx := &layout.Context{Queue: q}
|
||||
cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
|
||||
gtx.Reset(cfg, cs)
|
||||
|
||||
stack := layout.Stack{}
|
||||
stack.Init(gtx)
|
||||
|
||||
// Rigid 50x50 widget.
|
||||
child1 := stack.Rigid(func() {
|
||||
layoutWidget(gtx, 50, 50)
|
||||
})
|
||||
|
||||
// Force widget to the same size as the first.
|
||||
child2 := stack.Expand(func() {
|
||||
fmt.Printf("Expand: %v\n", gtx.Constraints)
|
||||
layoutWidget(gtx, 10, 10)
|
||||
})
|
||||
|
||||
stack.Layout(child1, child2)
|
||||
|
||||
// Output:
|
||||
// Expand: {{50 50} {50 50}}
|
||||
}
|
||||
|
||||
func ExampleList() {
|
||||
gtx := &layout.Context{Queue: q}
|
||||
cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
|
||||
gtx.Reset(cfg, cs)
|
||||
|
||||
// The list is 1e6 elements, but only 5 fit the constraints.
|
||||
const listLen = 1e6
|
||||
|
||||
var list layout.List
|
||||
count := 0
|
||||
list.Layout(gtx, listLen, func(i int) {
|
||||
count++
|
||||
layoutWidget(gtx, 20, 20)
|
||||
})
|
||||
|
||||
fmt.Println(count)
|
||||
|
||||
// Output:
|
||||
// 5
|
||||
}
|
||||
|
||||
func layoutWidget(ctx *layout.Context, width, height int) {
|
||||
ctx.Dimensions = layout.Dimensions{
|
||||
Size: image.Point{
|
||||
X: width,
|
||||
Y: height,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (config) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (config) Px(v ui.Value) int {
|
||||
return int(v.V + .5)
|
||||
}
|
||||
|
||||
func (queue) Events(k ui.Key) []ui.Event {
|
||||
return nil
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package layout
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/ui"
|
||||
"gioui.org/gesture"
|
||||
"gioui.org/paint"
|
||||
"gioui.org/pointer"
|
||||
)
|
||||
|
||||
type scrollChild struct {
|
||||
size image.Point
|
||||
macro ui.MacroOp
|
||||
}
|
||||
|
||||
// List displays a subsection of a potentially infinitely
|
||||
// large underlying list. List accepts user input to scroll
|
||||
// the subsection.
|
||||
type List struct {
|
||||
Axis Axis
|
||||
// ScrollToEnd instructs the list to stay scrolled to the far end position
|
||||
// once reahed. A List with ScrollToEnd enabled also align its content to
|
||||
// the end.
|
||||
ScrollToEnd bool
|
||||
// Alignment is the cross axis alignment of list elements.
|
||||
Alignment Alignment
|
||||
|
||||
// beforeEnd tracks whether the List position is before
|
||||
// the very end.
|
||||
beforeEnd bool
|
||||
|
||||
ctx *Context
|
||||
macro ui.MacroOp
|
||||
child ui.MacroOp
|
||||
scroll gesture.Scroll
|
||||
scrollDelta int
|
||||
|
||||
// first is the index of the first visible child.
|
||||
first int
|
||||
// offset is the signed distance from the top edge
|
||||
// to the child with index first.
|
||||
offset int
|
||||
|
||||
len int
|
||||
|
||||
// maxSize is the total size of visible children.
|
||||
maxSize int
|
||||
children []scrollChild
|
||||
dir iterationDir
|
||||
}
|
||||
|
||||
// ListElement is a function that computes the dimensions of
|
||||
// a list element.
|
||||
type ListElement func(index int)
|
||||
|
||||
type iterationDir uint8
|
||||
|
||||
const (
|
||||
iterateNone iterationDir = iota
|
||||
iterateForward
|
||||
iterateBackward
|
||||
)
|
||||
|
||||
const inf = 1e6
|
||||
|
||||
// init prepares the list for iterating through its children with next.
|
||||
func (l *List) init(gtx *Context, len int) {
|
||||
if l.more() {
|
||||
panic("unfinished child")
|
||||
}
|
||||
l.ctx = gtx
|
||||
l.maxSize = 0
|
||||
l.children = l.children[:0]
|
||||
l.len = len
|
||||
l.update()
|
||||
if l.scrollToEnd() {
|
||||
l.offset = 0
|
||||
l.first = len
|
||||
}
|
||||
if l.first > len {
|
||||
l.offset = 0
|
||||
l.first = len
|
||||
}
|
||||
l.macro.Record(gtx.Ops)
|
||||
l.next()
|
||||
}
|
||||
|
||||
// Layout the List and return its dimensions.
|
||||
func (l *List) Layout(gtx *Context, len int, w ListElement) {
|
||||
for l.init(gtx, len); l.more(); l.next() {
|
||||
cs := axisConstraints(l.Axis, Constraint{Max: inf}, axisCrossConstraint(l.Axis, l.ctx.Constraints))
|
||||
i := l.index()
|
||||
l.end(gtx.Layout(cs, func() {
|
||||
w(i)
|
||||
}))
|
||||
}
|
||||
gtx.Dimensions = l.layout()
|
||||
}
|
||||
|
||||
func (l *List) scrollToEnd() bool {
|
||||
return l.ScrollToEnd && !l.beforeEnd
|
||||
}
|
||||
|
||||
// Dragging reports whether the List is being dragged.
|
||||
func (l *List) Dragging() bool {
|
||||
return l.scroll.State() == gesture.StateDragging
|
||||
}
|
||||
|
||||
func (l *List) update() {
|
||||
d := l.scroll.Scroll(l.ctx.Config, l.ctx.Queue, gesture.Axis(l.Axis))
|
||||
l.scrollDelta = d
|
||||
l.offset += d
|
||||
}
|
||||
|
||||
// next advances to the next child.
|
||||
func (l *List) next() {
|
||||
l.dir = l.nextDir()
|
||||
// The user scroll offset is applied after scrolling to
|
||||
// list end.
|
||||
if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 {
|
||||
l.beforeEnd = true
|
||||
l.offset += l.scrollDelta
|
||||
l.dir = l.nextDir()
|
||||
}
|
||||
if l.more() {
|
||||
l.child.Record(l.ctx.Ops)
|
||||
}
|
||||
}
|
||||
|
||||
// index is current child's position in the underlying list.
|
||||
func (l *List) index() int {
|
||||
switch l.dir {
|
||||
case iterateBackward:
|
||||
return l.first - 1
|
||||
case iterateForward:
|
||||
return l.first + len(l.children)
|
||||
default:
|
||||
panic("Index called before Next")
|
||||
}
|
||||
}
|
||||
|
||||
// more reports whether more children are needed.
|
||||
func (l *List) more() bool {
|
||||
return l.dir != iterateNone
|
||||
}
|
||||
|
||||
func (l *List) nextDir() iterationDir {
|
||||
vsize := axisMainConstraint(l.Axis, l.ctx.Constraints).Max
|
||||
last := l.first + len(l.children)
|
||||
// Clamp offset.
|
||||
if l.maxSize-l.offset < vsize && last == l.len {
|
||||
l.offset = l.maxSize - vsize
|
||||
}
|
||||
if l.offset < 0 && l.first == 0 {
|
||||
l.offset = 0
|
||||
}
|
||||
switch {
|
||||
case len(l.children) == l.len:
|
||||
return iterateNone
|
||||
case l.maxSize-l.offset < vsize:
|
||||
return iterateForward
|
||||
case l.offset < 0:
|
||||
return iterateBackward
|
||||
}
|
||||
return iterateNone
|
||||
}
|
||||
|
||||
// End the current child by specifying its dimensions.
|
||||
func (l *List) end(dims Dimensions) {
|
||||
l.child.Stop()
|
||||
child := scrollChild{dims.Size, l.child}
|
||||
mainSize := axisMain(l.Axis, child.size)
|
||||
l.maxSize += mainSize
|
||||
switch l.dir {
|
||||
case iterateForward:
|
||||
l.children = append(l.children, child)
|
||||
case iterateBackward:
|
||||
l.children = append([]scrollChild{child}, l.children...)
|
||||
l.first--
|
||||
l.offset += mainSize
|
||||
default:
|
||||
panic("call Next before End")
|
||||
}
|
||||
l.dir = iterateNone
|
||||
}
|
||||
|
||||
// Layout the List and return its dimensions.
|
||||
func (l *List) layout() Dimensions {
|
||||
if l.more() {
|
||||
panic("unfinished child")
|
||||
}
|
||||
mainc := axisMainConstraint(l.Axis, l.ctx.Constraints)
|
||||
children := l.children
|
||||
// Skip invisible children
|
||||
for len(children) > 0 {
|
||||
sz := children[0].size
|
||||
mainSize := axisMain(l.Axis, sz)
|
||||
if l.offset <= mainSize {
|
||||
break
|
||||
}
|
||||
l.first++
|
||||
l.offset -= mainSize
|
||||
children = children[1:]
|
||||
}
|
||||
size := -l.offset
|
||||
var maxCross int
|
||||
for i, child := range children {
|
||||
sz := child.size
|
||||
if c := axisCross(l.Axis, sz); c > maxCross {
|
||||
maxCross = c
|
||||
}
|
||||
size += axisMain(l.Axis, sz)
|
||||
if size >= mainc.Max {
|
||||
children = children[:i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
ops := l.ctx.Ops
|
||||
pos := -l.offset
|
||||
// ScrollToEnd lists lists are end aligned.
|
||||
if space := mainc.Max - size; l.ScrollToEnd && space > 0 {
|
||||
pos += space
|
||||
}
|
||||
for _, child := range children {
|
||||
sz := child.size
|
||||
var cross int
|
||||
switch l.Alignment {
|
||||
case End:
|
||||
cross = maxCross - axisCross(l.Axis, sz)
|
||||
case Middle:
|
||||
cross = (maxCross - axisCross(l.Axis, sz)) / 2
|
||||
}
|
||||
childSize := axisMain(l.Axis, sz)
|
||||
max := childSize + pos
|
||||
if max > mainc.Max {
|
||||
max = mainc.Max
|
||||
}
|
||||
min := pos
|
||||
if min < 0 {
|
||||
min = 0
|
||||
}
|
||||
r := image.Rectangle{
|
||||
Min: axisPoint(l.Axis, min, -inf),
|
||||
Max: axisPoint(l.Axis, max, inf),
|
||||
}
|
||||
var stack ui.StackOp
|
||||
stack.Push(ops)
|
||||
paint.RectClip(r).Add(ops)
|
||||
ui.TransformOp{}.Offset(toPointF(axisPoint(l.Axis, pos, cross))).Add(ops)
|
||||
child.macro.Add(ops)
|
||||
stack.Pop()
|
||||
pos += childSize
|
||||
}
|
||||
atStart := l.first == 0 && l.offset <= 0
|
||||
atEnd := l.first+len(children) == l.len && mainc.Max >= pos
|
||||
if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
|
||||
l.scroll.Stop()
|
||||
}
|
||||
l.beforeEnd = !atEnd
|
||||
dims := axisPoint(l.Axis, mainc.Constrain(pos), maxCross)
|
||||
l.macro.Stop()
|
||||
pointer.RectAreaOp{Rect: image.Rectangle{Max: dims}}.Add(ops)
|
||||
l.scroll.Add(ops)
|
||||
l.macro.Add(ops)
|
||||
return Dimensions{Size: dims}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package layout
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/ui"
|
||||
)
|
||||
|
||||
// Stack lays out child elements on top of each other,
|
||||
// according to an alignment direction.
|
||||
type Stack struct {
|
||||
// Alignment is the direction to align children
|
||||
// smaller than the available space.
|
||||
Alignment Direction
|
||||
|
||||
macro ui.MacroOp
|
||||
constrained bool
|
||||
ctx *Context
|
||||
maxSZ image.Point
|
||||
baseline int
|
||||
}
|
||||
|
||||
// StackChild is the layout result of a call to End.
|
||||
type StackChild struct {
|
||||
macro ui.MacroOp
|
||||
dims Dimensions
|
||||
}
|
||||
|
||||
// Init a stack before calling Rigid or Expand.
|
||||
func (s *Stack) Init(gtx *Context) *Stack {
|
||||
s.ctx = gtx
|
||||
s.constrained = true
|
||||
s.maxSZ = image.Point{}
|
||||
s.baseline = 0
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Stack) begin() {
|
||||
if !s.constrained {
|
||||
panic("must Init before adding a child")
|
||||
}
|
||||
s.macro.Record(s.ctx.Ops)
|
||||
}
|
||||
|
||||
// Rigid lays out a widget with the same constraints that were
|
||||
// passed to Init.
|
||||
func (s *Stack) Rigid(w Widget) StackChild {
|
||||
s.begin()
|
||||
dims := s.ctx.Layout(s.ctx.Constraints, w)
|
||||
return s.end(dims)
|
||||
}
|
||||
|
||||
// Expand lays out a widget with constraints that exactly match
|
||||
// the biggest child previously added.
|
||||
func (s *Stack) Expand(w Widget) StackChild {
|
||||
s.begin()
|
||||
cs := Constraints{
|
||||
Width: Constraint{Min: s.maxSZ.X, Max: s.maxSZ.X},
|
||||
Height: Constraint{Min: s.maxSZ.Y, Max: s.maxSZ.Y},
|
||||
}
|
||||
dims := s.ctx.Layout(cs, w)
|
||||
return s.end(dims)
|
||||
}
|
||||
|
||||
// End a child by specifying its dimensions.
|
||||
func (s *Stack) end(dims Dimensions) StackChild {
|
||||
s.macro.Stop()
|
||||
if w := dims.Size.X; w > s.maxSZ.X {
|
||||
s.maxSZ.X = w
|
||||
}
|
||||
if h := dims.Size.Y; h > s.maxSZ.Y {
|
||||
s.maxSZ.Y = h
|
||||
}
|
||||
if s.baseline == 0 {
|
||||
if b := dims.Baseline; b != dims.Size.Y {
|
||||
s.baseline = b
|
||||
}
|
||||
}
|
||||
return StackChild{s.macro, dims}
|
||||
}
|
||||
|
||||
// Layout a list of children. The order of the children determines their laid
|
||||
// out order.
|
||||
func (s *Stack) Layout(children ...StackChild) {
|
||||
for _, ch := range children {
|
||||
sz := ch.dims.Size
|
||||
var p image.Point
|
||||
switch s.Alignment {
|
||||
case N, S, Center:
|
||||
p.X = (s.maxSZ.X - sz.X) / 2
|
||||
case NE, SE, E:
|
||||
p.X = s.maxSZ.X - sz.X
|
||||
}
|
||||
switch s.Alignment {
|
||||
case W, Center, E:
|
||||
p.Y = (s.maxSZ.Y - sz.Y) / 2
|
||||
case SW, S, SE:
|
||||
p.Y = s.maxSZ.Y - sz.Y
|
||||
}
|
||||
var stack ui.StackOp
|
||||
stack.Push(s.ctx.Ops)
|
||||
ui.TransformOp{}.Offset(toPointF(p)).Add(s.ctx.Ops)
|
||||
ch.macro.Add(s.ctx.Ops)
|
||||
stack.Pop()
|
||||
}
|
||||
b := s.baseline
|
||||
if b == 0 {
|
||||
b = s.maxSZ.Y
|
||||
}
|
||||
s.ctx.Dimensions = Dimensions{
|
||||
Size: s.maxSZ,
|
||||
Baseline: b,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user