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:
+53
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user