chore(v2): vendor dependencies for offline/China builds

go mod vendor pins onnxruntime_go v1.12.1, Gio and the rest into v2/vendor
so go run/build work without hitting proxy.golang.org (blocked/slow in
China). Verified: CGO_ENABLED=1 go build -mod=vendor ./internal/spike and
GOOS=windows go build -mod=vendor ./internal/ui both pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-23 16:35:01 +08:00
co-authored by Claude Opus 4.8
parent 97c1c4a974
commit f58728cddd
972 changed files with 597802 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: Unlicense OR MIT
package layout
import (
"time"
"gioui.org/io/input"
"gioui.org/io/system"
"gioui.org/op"
"gioui.org/unit"
)
// Context carries the state needed by almost all layouts and widgets.
// A zero value Context never returns events, map units to pixels
// with a scale of 1.0, and returns the zero time from Now.
type Context struct {
// Constraints track the constraints for the active widget or
// layout.
Constraints Constraints
Metric unit.Metric
// Now is the animation time.
Now time.Time
// Locale provides information on the system's language preferences.
// BUG(whereswaldon): this field is not currently populated automatically.
// Interested users must look up and populate these values manually.
Locale system.Locale
// Values is a map of program global data associated with the context.
// It is not for use by widgets.
Values map[string]any
input.Source
*op.Ops
}
// Dp converts v to pixels.
func (c Context) Dp(v unit.Dp) int {
return c.Metric.Dp(v)
}
// Sp converts v to pixels.
func (c Context) Sp(v unit.Sp) int {
return c.Metric.Sp(v)
}
// Disabled returns a copy of this context that don't deliver any events.
func (c Context) Disabled() Context {
c.Source = c.Source.Disabled()
return c
}
+49
View File
@@ -0,0 +1,49 @@
// 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:
var gtx layout.Context
// Configure a top inset.
inset := layout.Inset{Top: 8, ...}
// Use the inset to lay out a widget.
inset.Layout(gtx, func() {
// Lay out widget and determine its size given the constraints
// in gtx.Constraints.
...
return layout.Dimensions{...}
})
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(gtx layout.Context) layout.Dimensions {
align := layout.Alignment(...)
return align.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return 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
+264
View File
@@ -0,0 +1,264 @@
// SPDX-License-Identifier: Unlicense OR MIT
package layout
import (
"image"
"gioui.org/op"
)
// 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
// WeightSum is the sum of weights used for the weighted
// size of Flexed children. If WeightSum is zero, the sum
// of all Flexed weights is used.
WeightSum float32
// Gap is the space in pixels between children.
Gap int
}
// FlexChild is the descriptor for a Flex child.
type FlexChild struct {
flex bool
weight float32
widget Widget
}
// Spacing determine the spacing mode for a Flex.
type Spacing 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
)
// Rigid returns a Flex child with a maximal constraint of the
// remaining space.
func Rigid(widget Widget) FlexChild {
return FlexChild{
widget: widget,
}
}
// Flexed returns a Flex child forced to take up weight fraction of the
// space left over from Rigid children. The fraction is weight
// divided by either the weight sum of all Flexed children or the Flex
// WeightSum if non zero.
func Flexed(weight float32, widget Widget) FlexChild {
return FlexChild{
flex: true,
weight: weight,
widget: widget,
}
}
// Layout a list of children. The position of the children are
// determined by the specified order, but Rigid children are laid out
// before Flexed children.
func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
size := 0
cs := gtx.Constraints
mainMin, mainMax := f.Axis.mainConstraint(cs)
crossMin, crossMax := f.Axis.crossConstraint(cs)
remaining := mainMax
// Reserve space for gaps between children.
if len(children) > 1 && f.Gap > 0 {
totalGap := f.Gap * (len(children) - 1)
remaining -= totalGap
if remaining < 0 {
remaining = 0
}
}
var totalWeight float32
cgtx := gtx
// Note: previously the scratch space was inside FlexChild.
// child.call.Add(gtx.Ops) confused the go escape analysis and caused the
// entired children slice to be allocated on the heap, including all widgets
// in it. This produced a lot of object allocations. Now the scratch space
// is separate from children, and for cases len(children) <= 32, we will
// allocate the scratch space on the stack. For cases len(children) > 32,
// only the scratch space gets allocated from the heap, during append.
type scratchSpace struct {
call op.CallOp
dims Dimensions
}
var scratchArray [32]scratchSpace
scratch := scratchArray[:0]
scratch = append(scratch, make([]scratchSpace, len(children))...)
// Lay out Rigid children.
for i, child := range children {
if child.flex {
totalWeight += child.weight
continue
}
macro := op.Record(gtx.Ops)
cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax)
dims := child.widget(cgtx)
c := macro.Stop()
sz := f.Axis.Convert(dims.Size).X
size += sz
remaining -= sz
if remaining < 0 {
remaining = 0
}
scratch[i].call = c
scratch[i].dims = dims
}
if w := f.WeightSum; w != 0 {
totalWeight = w
}
// fraction is the rounding error from a Flex weighting.
var fraction float32
flexTotal := remaining
// Lay out Flexed children.
for i, child := range children {
if !child.flex {
continue
}
var flexSize int
if remaining > 0 && totalWeight > 0 {
// Apply weight and add any leftover fraction from a
// previous Flexed.
childSize := float32(flexTotal) * child.weight / totalWeight
flexSize = int(childSize + fraction + .5)
fraction = childSize - float32(flexSize)
if flexSize > remaining {
flexSize = remaining
}
}
macro := op.Record(gtx.Ops)
cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin, crossMax)
dims := child.widget(cgtx)
c := macro.Stop()
sz := f.Axis.Convert(dims.Size).X
size += sz
remaining -= sz
if remaining < 0 {
remaining = 0
}
scratch[i].call = c
scratch[i].dims = dims
}
maxCross := crossMin
var maxBaseline int
for _, scratchChild := range scratch {
if c := f.Axis.Convert(scratchChild.dims.Size).Y; c > maxCross {
maxCross = c
}
if b := scratchChild.dims.Size.Y - scratchChild.dims.Baseline; b > maxBaseline {
maxBaseline = b
}
}
if len(children) > 1 && f.Gap > 0 {
size += f.Gap * (len(children) - 1)
}
var space int
if mainMin > size {
space = mainMin - size
}
var mainSize int
switch f.Spacing {
case SpaceSides:
mainSize += space / 2
case SpaceStart:
mainSize += space
case SpaceEvenly:
mainSize += space / (1 + len(children))
case SpaceAround:
if len(children) > 0 {
mainSize += space / (len(children) * 2)
}
}
for i, scratchChild := range scratch {
dims := scratchChild.dims
b := dims.Size.Y - dims.Baseline
var cross int
switch f.Alignment {
case End:
cross = maxCross - f.Axis.Convert(dims.Size).Y
case Middle:
cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2
case Baseline:
if f.Axis == Horizontal {
cross = maxBaseline - b
}
}
pt := f.Axis.Convert(image.Pt(mainSize, cross))
trans := op.Offset(pt).Push(gtx.Ops)
scratchChild.call.Add(gtx.Ops)
trans.Pop()
mainSize += f.Axis.Convert(dims.Size).X
if i < len(children)-1 {
mainSize += f.Gap
switch f.Spacing {
case SpaceEvenly:
mainSize += space / (1 + len(children))
case SpaceAround:
if len(children) > 0 {
mainSize += space / len(children)
}
case SpaceBetween:
if len(children) > 1 {
mainSize += space / (len(children) - 1)
}
}
}
}
switch f.Spacing {
case SpaceSides:
mainSize += space / 2
case SpaceEnd:
mainSize += space
case SpaceEvenly:
mainSize += space / (1 + len(children))
case SpaceAround:
if len(children) > 0 {
mainSize += space / (len(children) * 2)
}
}
sz := f.Axis.Convert(image.Pt(mainSize, maxCross))
sz = cs.Constrain(sz)
return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline}
}
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")
}
}
+344
View File
@@ -0,0 +1,344 @@
// SPDX-License-Identifier: Unlicense OR MIT
package layout
import (
"image"
"gioui.org/f32"
"gioui.org/op"
"gioui.org/unit"
)
// Constraints represent the minimum and maximum size of a widget.
//
// A widget does not have to treat its constraints as "hard". For
// example, if it's passed a constraint with a minimum size that's
// smaller than its actual minimum size, it should return its minimum
// size dimensions instead. Parent widgets should deal appropriately
// with child widgets that return dimensions that do not fit their
// constraints (for example, by clipping).
type Constraints struct {
Min, Max image.Point
}
// Dimensions are the resolved size and baseline for a widget.
//
// Baseline is the distance from the bottom of a widget to the baseline of
// any text it contains (or 0). The purpose is to be able to align text
// that span multiple widgets.
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(gtx Context) Dimensions
const (
Start Alignment = iota
End
Middle
Baseline
)
const (
NW Direction = iota
N
NE
E
SE
S
SW
W
Center
)
const (
Horizontal Axis = iota
Vertical
)
// Exact returns the Constraints with the minimum and maximum size
// set to size.
func Exact(size image.Point) Constraints {
return Constraints{
Min: size, Max: size,
}
}
// FPt converts an point to a f32.Point.
func FPt(p image.Point) f32.Point {
return f32.Point{
X: float32(p.X), Y: float32(p.Y),
}
}
// Constrain a size so each dimension is in the range [min;max].
func (c Constraints) Constrain(size image.Point) image.Point {
if min := c.Min.X; size.X < min {
size.X = min
}
if min := c.Min.Y; size.Y < min {
size.Y = min
}
if max := c.Max.X; size.X > max {
size.X = max
}
if max := c.Max.Y; size.Y > max {
size.Y = max
}
return size
}
// AddMin returns a copy of Constraints with the Min constraint enlarged by up to delta
// while still fitting within the Max constraint. The Max is unchanged, and the Min constraint
// will not go negative.
func (c Constraints) AddMin(delta image.Point) Constraints {
c.Min = c.Min.Add(delta)
if c.Min.X < 0 {
c.Min.X = 0
}
if c.Min.Y < 0 {
c.Min.Y = 0
}
c.Min = c.Constrain(c.Min)
return c
}
// SubMax returns a copy of Constraints with the Max constraint shrunk by up to delta
// while not going negative. The values of delta are expected to be positive.
// The Min constraint is adjusted to fit within the new Max constraint.
func (c Constraints) SubMax(delta image.Point) Constraints {
c.Max = c.Max.Sub(delta)
if c.Max.X < 0 {
c.Max.X = 0
}
if c.Max.Y < 0 {
c.Max.Y = 0
}
c.Min = c.Constrain(c.Min)
return c
}
// Inset adds space around a widget by decreasing its maximum
// constraints. The minimum constraints will be adjusted to ensure
// they do not exceed the maximum.
type Inset struct {
Top, Bottom, Left, Right unit.Dp
}
// Layout a widget.
func (in Inset) Layout(gtx Context, w Widget) Dimensions {
top := gtx.Dp(in.Top)
right := gtx.Dp(in.Right)
bottom := gtx.Dp(in.Bottom)
left := gtx.Dp(in.Left)
mcs := gtx.Constraints
mcs.Max.X -= left + right
if mcs.Max.X < 0 {
left = 0
right = 0
mcs.Max.X = 0
}
if mcs.Min.X > mcs.Max.X {
mcs.Min.X = mcs.Max.X
}
mcs.Max.Y -= top + bottom
if mcs.Max.Y < 0 {
bottom = 0
top = 0
mcs.Max.Y = 0
}
if mcs.Min.Y > mcs.Max.Y {
mcs.Min.Y = mcs.Max.Y
}
gtx.Constraints = mcs
trans := op.Offset(image.Pt(left, top)).Push(gtx.Ops)
dims := w(gtx)
trans.Pop()
return Dimensions{
Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
Baseline: dims.Baseline + bottom,
}
}
// UniformInset returns an Inset with a single inset applied to all
// edges.
func UniformInset(v unit.Dp) Inset {
return Inset{Top: v, Right: v, Bottom: v, Left: v}
}
// Layout a widget according to the direction.
// The widget is called with the context constraints minimum cleared.
func (d Direction) Layout(gtx Context, w Widget) Dimensions {
macro := op.Record(gtx.Ops)
csn := gtx.Constraints.Min
switch d {
case N, S:
gtx.Constraints.Min.Y = 0
case E, W:
gtx.Constraints.Min.X = 0
default:
gtx.Constraints.Min = image.Point{}
}
dims := w(gtx)
call := macro.Stop()
sz := dims.Size
if sz.X < csn.X {
sz.X = csn.X
}
if sz.Y < csn.Y {
sz.Y = csn.Y
}
p := d.Position(dims.Size, sz)
defer op.Offset(p).Push(gtx.Ops).Pop()
call.Add(gtx.Ops)
return Dimensions{
Size: sz,
Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
}
}
// Position calculates widget position according to the direction.
func (d Direction) Position(widget, bounds image.Point) image.Point {
var p image.Point
switch d {
case N, S, Center:
p.X = (bounds.X - widget.X) / 2
case NE, SE, E:
p.X = bounds.X - widget.X
}
switch d {
case W, Center, E:
p.Y = (bounds.Y - widget.Y) / 2
case SW, S, SE:
p.Y = bounds.Y - widget.Y
}
return p
}
// Spacer adds space between widgets.
type Spacer struct {
Width, Height unit.Dp
}
func (s Spacer) Layout(gtx Context) Dimensions {
return Dimensions{
Size: gtx.Constraints.Constrain(image.Point{
X: gtx.Dp(s.Width),
Y: gtx.Dp(s.Height),
}),
}
}
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")
}
}
// Convert a point in (x, y) coordinates to (main, cross) coordinates,
// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged
// for the horizontal axis, or (y, x) for the vertical axis.
func (a Axis) Convert(pt image.Point) image.Point {
if a == Horizontal {
return pt
}
return image.Pt(pt.Y, pt.X)
}
// FConvert a point in (x, y) coordinates to (main, cross) coordinates,
// or vice versa. Specifically, FConvert((x, y)) returns (x, y) unchanged
// for the horizontal axis, or (y, x) for the vertical axis.
func (a Axis) FConvert(pt f32.Point) f32.Point {
if a == Horizontal {
return pt
}
return f32.Pt(pt.Y, pt.X)
}
// mainConstraint returns the min and max main constraints for axis a.
func (a Axis) mainConstraint(cs Constraints) (int, int) {
if a == Horizontal {
return cs.Min.X, cs.Max.X
}
return cs.Min.Y, cs.Max.Y
}
// crossConstraint returns the min and max cross constraints for axis a.
func (a Axis) crossConstraint(cs Constraints) (int, int) {
if a == Horizontal {
return cs.Min.Y, cs.Max.Y
}
return cs.Min.X, cs.Max.X
}
// constraints returns the constraints for axis a.
func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints {
if a == Horizontal {
return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)}
}
return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)}
}
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")
}
}
+409
View File
@@ -0,0 +1,409 @@
// SPDX-License-Identifier: Unlicense OR MIT
package layout
import (
"image"
"math"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/op"
"gioui.org/op/clip"
)
type scrollChild struct {
size image.Point
call op.CallOp
}
// 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 reached. A List with ScrollToEnd == true and Position.BeforeEnd ==
// false draws its content with the last item at the bottom of the list
// area.
ScrollToEnd bool
// Alignment is the cross axis alignment of list elements.
Alignment Alignment
// ScrollAnyAxis allows any scroll axis to scroll the list, not just the main axis.
ScrollAnyAxis bool
// Gap is the space in pixels between children.
Gap int
cs Constraints
scroll gesture.Scroll
scrollDelta int
// Position is updated during Layout. To save the list scroll position,
// just save Position after Layout finishes. To scroll the list
// programmatically, update Position (e.g. restore it from a saved value)
// before calling Layout.
Position Position
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(gtx Context, index int) Dimensions
type iterationDir uint8
// Position is a List scroll offset represented as an offset from the top edge
// of a child element.
type Position struct {
// BeforeEnd tracks whether the List position is before the very end. We
// use "before end" instead of "at end" so that the zero value of a
// Position struct is useful.
//
// When laying out a list, if ScrollToEnd is true and BeforeEnd is false,
// then First and Offset are ignored, and the list is drawn with the last
// item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored.
BeforeEnd bool
// First is the index of the first visible child.
First int
// Offset is the distance in pixels from the leading edge to the child at index
// First.
Offset int
// OffsetLast is the signed distance in pixels from the trailing edge to the
// bottom edge of the child at index First+Count.
OffsetLast int
// Count is the number of visible children.
Count int
// Length is the estimated total size of all children, measured in pixels.
Length int
}
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.cs = gtx.Constraints
l.maxSize = 0
l.children = l.children[:0]
l.len = len
l.update(gtx)
if l.Position.First < 0 {
l.Position.Offset = 0
l.Position.First = 0
}
if l.scrollToEnd() || l.Position.First > len {
l.Position.Offset = 0
l.Position.First = len
}
}
// Layout a List of len items, where each item is implicitly defined
// by the callback w. Layout can handle very large lists because it only calls
// w to fill its viewport and the distance scrolled, if any.
func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions {
l.init(gtx, len)
crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints)
gtx.Constraints = l.Axis.constraints(0, inf, crossMin, crossMax)
macro := op.Record(gtx.Ops)
laidOutTotalLength := 0
numLaidOut := 0
for l.next(); l.more(); l.next() {
child := op.Record(gtx.Ops)
dims := w(gtx, l.index())
call := child.Stop()
l.end(dims, call)
laidOutTotalLength += l.Axis.Convert(dims.Size).X
numLaidOut++
}
if numLaidOut > 0 {
l.Position.Length = laidOutTotalLength*len/numLaidOut + l.Gap*(len-1)
} else {
l.Position.Length = 0
}
return l.layout(gtx.Ops, macro)
}
func (l *List) scrollToEnd() bool {
return l.ScrollToEnd && !l.Position.BeforeEnd
}
// Dragging reports whether the List is being dragged.
func (l *List) Dragging() bool {
return l.scroll.State() == gesture.StateDragging
}
func (l *List) update(gtx Context) {
min, max := int(-inf), int(inf)
if l.Position.First == 0 {
// Use the size of the invisible part as scroll boundary.
min = -l.Position.Offset
if min > 0 {
min = 0
}
}
if l.Position.First+l.Position.Count == l.len {
max = -l.Position.OffsetLast
if max < 0 {
max = 0
}
}
xrange := pointer.ScrollRange{Min: min, Max: max}
yrange := pointer.ScrollRange{}
axis := gesture.Axis(l.Axis)
if l.ScrollAnyAxis {
axis = gesture.Both
yrange = xrange
} else if l.Axis == Vertical {
xrange, yrange = yrange, xrange
}
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, axis, xrange, yrange)
l.scrollDelta = d
l.Position.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.Position.BeforeEnd = true
l.Position.Offset += l.scrollDelta
l.dir = l.nextDir()
}
}
// index is current child's position in the underlying list.
func (l *List) index() int {
switch l.dir {
case iterateBackward:
return l.Position.First - 1
case iterateForward:
return l.Position.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 := l.Axis.mainConstraint(l.cs)
last := l.Position.First + len(l.children)
// Clamp offset.
if l.maxSize-l.Position.Offset < vsize && last == l.len {
l.Position.Offset = l.maxSize - vsize
}
if l.Position.Offset < 0 && l.Position.First == 0 {
l.Position.Offset = 0
}
// Lay out an extra (invisible) child at each end to enable focus to
// move to them, triggering automatic scroll.
firstSize, lastSize := 0, 0
if len(l.children) > 0 {
if l.Position.First > 0 {
firstChild := l.children[0]
firstSize = l.Axis.Convert(firstChild.size).X + l.Gap
}
if last < l.len {
lastChild := l.children[len(l.children)-1]
lastSize = l.Axis.Convert(lastChild.size).X + l.Gap
}
}
switch {
case len(l.children) == l.len:
return iterateNone
case l.maxSize-l.Position.Offset-lastSize < vsize:
return iterateForward
case l.Position.Offset-firstSize < 0:
return iterateBackward
}
return iterateNone
}
// End the current child by specifying its dimensions.
func (l *List) end(dims Dimensions, call op.CallOp) {
child := scrollChild{dims.Size, call}
mainSize := l.Axis.Convert(child.size).X
if len(l.children) > 0 {
l.maxSize += l.Gap
}
l.maxSize += mainSize
switch l.dir {
case iterateForward:
l.children = append(l.children, child)
case iterateBackward:
l.children = append(l.children, scrollChild{})
copy(l.children[1:], l.children)
l.children[0] = child
l.Position.First--
l.Position.Offset += mainSize + l.Gap
default:
panic("call Next before End")
}
l.dir = iterateNone
}
// Layout the List and return its dimensions.
func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
if l.more() {
panic("unfinished child")
}
mainMin, mainMax := l.Axis.mainConstraint(l.cs)
children := l.children
var first scrollChild
// Skip invisible children.
for len(children) > 0 {
child := children[0]
sz := child.size
mainSize := l.Axis.Convert(sz).X
if l.Position.Offset < mainSize {
// First child is partially visible.
break
}
l.Position.First++
l.Position.Offset -= mainSize + l.Gap
first = child
children = children[1:]
}
size := -l.Position.Offset
var maxCross int
var last scrollChild
for i, child := range children {
sz := l.Axis.Convert(child.size)
if c := sz.Y; c > maxCross {
maxCross = c
}
if i > 0 {
size += l.Gap
}
size += sz.X
if size >= mainMax {
if i < len(children)-1 {
last = children[i+1]
}
children = children[:i+1]
break
}
}
l.Position.Count = len(children)
l.Position.OffsetLast = mainMax - size
// ScrollToEnd lists are end aligned.
if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 {
l.Position.Offset -= space
}
pos := -l.Position.Offset
layout := func(child scrollChild) {
sz := l.Axis.Convert(child.size)
var cross int
switch l.Alignment {
case End:
cross = maxCross - sz.Y
case Middle:
cross = (maxCross - sz.Y) / 2
}
childSize := sz.X
pt := l.Axis.Convert(image.Pt(pos, cross))
trans := op.Offset(pt).Push(ops)
child.call.Add(ops)
trans.Pop()
pos += childSize
}
// Lay out leading invisible child.
if first != (scrollChild{}) {
sz := l.Axis.Convert(first.size)
pos -= sz.X + l.Gap
layout(first)
pos += l.Gap
}
for i, child := range children {
if i > 0 {
pos += l.Gap
}
layout(child)
}
// Lay out trailing invisible child.
if last != (scrollChild{}) {
pos += l.Gap
layout(last)
}
atStart := l.Position.First == 0 && l.Position.Offset <= 0
atEnd := l.Position.First+len(children) == l.len && mainMax >= pos
if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
l.scroll.Stop()
}
l.Position.BeforeEnd = !atEnd
if pos < mainMin {
pos = mainMin
}
if pos > mainMax {
pos = mainMax
}
if crossMin, crossMax := l.Axis.crossConstraint(l.cs); maxCross < crossMin {
maxCross = crossMin
} else if maxCross > crossMax {
maxCross = crossMax
}
dims := l.Axis.Convert(image.Pt(pos, maxCross))
call := macro.Stop()
defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop()
l.scroll.Add(ops)
call.Add(ops)
return Dimensions{Size: dims}
}
// ScrollBy scrolls the list by a relative amount of items.
//
// Fractional scrolling may be inaccurate for items of differing
// dimensions. This includes scrolling by integer amounts if the current
// l.Position.Offset is non-zero.
func (l *List) ScrollBy(num float32) {
// Split number of items into integer and fractional parts
i, f := math.Modf(float64(num))
// Scroll by integer amount of items
l.Position.First += int(i)
// Adjust Offset to account for fractional items. If Offset gets so large that it amounts to an entire item, then
// the layout code will handle that for us and adjust First and Offset accordingly.
itemHeight := float64(l.Position.Length) / float64(l.len)
l.Position.Offset += int(math.Round(itemHeight * f))
// First and Offset can go out of bounds, but the layout code knows how to handle that.
// Ensure that the list pays attention to the Offset field when the scrollbar drag
// is started while the bar is at the end of the list. Without this, the scrollbar
// cannot be dragged away from the end.
l.Position.BeforeEnd = true
}
// ScrollTo scrolls to the specified item.
func (l *List) ScrollTo(n int) {
l.Position.First = n
l.Position.Offset = 0
l.Position.BeforeEnd = true
}
+163
View File
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: Unlicense OR MIT
package layout
import (
"image"
"gioui.org/op"
)
// 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
}
// StackChild represents a child for a Stack layout.
type StackChild struct {
expanded bool
widget Widget
}
// Stacked returns a Stack child that is laid out with no minimum
// constraints and the maximum constraints passed to Stack.Layout.
func Stacked(w Widget) StackChild {
return StackChild{
widget: w,
}
}
// Expanded returns a Stack child with the minimum constraints set
// to the largest Stacked child. The maximum constraints are set to
// the same as passed to Stack.Layout.
func Expanded(w Widget) StackChild {
return StackChild{
expanded: true,
widget: w,
}
}
// Layout a stack of children. The position of the children are
// determined by the specified order, but Stacked children are laid out
// before Expanded children.
func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
var maxSZ image.Point
// First lay out Stacked children.
cgtx := gtx
cgtx.Constraints.Min = image.Point{}
// Note: previously the scratch space was inside StackChild.
// child.call.Add(gtx.Ops) confused the go escape analysis and caused the
// entired children slice to be allocated on the heap, including all widgets
// in it. This produced a lot of object allocations. Now the scratch space
// is separate from children, and for cases len(children) <= 32, we will
// allocate the scratch space on the stack. For cases len(children) > 32,
// only the scratch space gets allocated from the heap, during append.
type scratchSpace struct {
call op.CallOp
dims Dimensions
}
var scratchArray [32]scratchSpace
scratch := scratchArray[:0]
scratch = append(scratch, make([]scratchSpace, len(children))...)
for i, w := range children {
if w.expanded {
continue
}
macro := op.Record(gtx.Ops)
dims := w.widget(cgtx)
call := macro.Stop()
if w := dims.Size.X; w > maxSZ.X {
maxSZ.X = w
}
if h := dims.Size.Y; h > maxSZ.Y {
maxSZ.Y = h
}
scratch[i].call = call
scratch[i].dims = dims
}
// Then lay out Expanded children.
for i, w := range children {
if !w.expanded {
continue
}
macro := op.Record(gtx.Ops)
cgtx.Constraints.Min = maxSZ
dims := w.widget(cgtx)
call := macro.Stop()
if w := dims.Size.X; w > maxSZ.X {
maxSZ.X = w
}
if h := dims.Size.Y; h > maxSZ.Y {
maxSZ.Y = h
}
scratch[i].call = call
scratch[i].dims = dims
}
maxSZ = gtx.Constraints.Constrain(maxSZ)
var baseline int
for _, scratchChild := range scratch {
sz := scratchChild.dims.Size
var p image.Point
switch s.Alignment {
case N, S, Center:
p.X = (maxSZ.X - sz.X) / 2
case NE, SE, E:
p.X = maxSZ.X - sz.X
}
switch s.Alignment {
case W, Center, E:
p.Y = (maxSZ.Y - sz.Y) / 2
case SW, S, SE:
p.Y = maxSZ.Y - sz.Y
}
trans := op.Offset(p).Push(gtx.Ops)
scratchChild.call.Add(gtx.Ops)
trans.Pop()
if baseline == 0 {
if b := scratchChild.dims.Baseline; b != 0 {
baseline = b + maxSZ.Y - sz.Y - p.Y
}
}
}
return Dimensions{
Size: maxSZ,
Baseline: baseline,
}
}
// Background lays out single child widget on top of a background,
// centering, if necessary.
type Background struct{}
// Layout a widget and then add a background to it.
func (Background) Layout(gtx Context, background, widget Widget) Dimensions {
macro := op.Record(gtx.Ops)
wdims := widget(gtx)
baseline := wdims.Baseline
call := macro.Stop()
cgtx := gtx
cgtx.Constraints.Min = gtx.Constraints.Constrain(wdims.Size)
bdims := background(cgtx)
if bdims.Size != wdims.Size {
p := image.Point{
X: (bdims.Size.X - wdims.Size.X) / 2,
Y: (bdims.Size.Y - wdims.Size.Y) / 2,
}
baseline += (bdims.Size.Y - wdims.Size.Y) / 2
trans := op.Offset(p).Push(gtx.Ops)
defer trans.Pop()
}
call.Add(gtx.Ops)
return Dimensions{
Size: bdims.Size,
Baseline: baseline,
}
}