mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-04 17:05:38 +00:00
widget: add Fit for scaling widgets
Currently adds four different variants Unscaled, Contain, Cover, ScaleDown and Fill. Signed-off-by: Egon Elbre <egonelbre@gmail.com>
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package widget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"gioui.org/f32"
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/op"
|
||||||
|
"gioui.org/op/clip"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fit scales a widget to fit and clip to the constraints.
|
||||||
|
type Fit uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Unscaled does not alter the scale of a widget.
|
||||||
|
Unscaled Fit = iota
|
||||||
|
// Contain scales widget as large as possible without cropping
|
||||||
|
// and it preserves aspect-ratio.
|
||||||
|
Contain
|
||||||
|
// Cover scales the widget to cover the constraint area and
|
||||||
|
// preserves aspect-ratio.
|
||||||
|
Cover
|
||||||
|
// ScaleDown scales the widget smaller without cropping,
|
||||||
|
// when it exceeds the constraint area.
|
||||||
|
// It preserves aspect-ratio.
|
||||||
|
ScaleDown
|
||||||
|
// Fill stretches the widget to the constraints and does not
|
||||||
|
// preserve aspect-ratio.
|
||||||
|
Fill
|
||||||
|
)
|
||||||
|
|
||||||
|
// scale adds clip and scale operations to fit dims to the constraints.
|
||||||
|
// It positions the widget to the appropriate position.
|
||||||
|
// It returns dimensions modified accordingly.
|
||||||
|
func (fit Fit) scale(gtx layout.Context, pos layout.Direction, dims layout.Dimensions) layout.Dimensions {
|
||||||
|
widgetSize := dims.Size
|
||||||
|
|
||||||
|
if fit == Unscaled || dims.Size.X == 0 || dims.Size.Y == 0 {
|
||||||
|
dims.Size = gtx.Constraints.Constrain(dims.Size)
|
||||||
|
clip.Rect{Max: dims.Size}.Add(gtx.Ops)
|
||||||
|
|
||||||
|
offset := pos.Position(widgetSize, dims.Size)
|
||||||
|
op.Offset(layout.FPt(offset)).Add(gtx.Ops)
|
||||||
|
dims.Baseline += offset.Y
|
||||||
|
return dims
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := f32.Point{
|
||||||
|
X: float32(gtx.Constraints.Max.X) / float32(dims.Size.X),
|
||||||
|
Y: float32(gtx.Constraints.Max.Y) / float32(dims.Size.Y),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fit {
|
||||||
|
case Contain:
|
||||||
|
if scale.Y < scale.X {
|
||||||
|
scale.X = scale.Y
|
||||||
|
} else {
|
||||||
|
scale.Y = scale.X
|
||||||
|
}
|
||||||
|
case Cover:
|
||||||
|
if scale.Y > scale.X {
|
||||||
|
scale.X = scale.Y
|
||||||
|
} else {
|
||||||
|
scale.Y = scale.X
|
||||||
|
}
|
||||||
|
case ScaleDown:
|
||||||
|
if scale.Y < scale.X {
|
||||||
|
scale.X = scale.Y
|
||||||
|
} else {
|
||||||
|
scale.Y = scale.X
|
||||||
|
}
|
||||||
|
|
||||||
|
// The widget would need to be scaled up, no change needed.
|
||||||
|
if scale.X >= 1 {
|
||||||
|
dims.Size = gtx.Constraints.Constrain(dims.Size)
|
||||||
|
clip.Rect{Max: dims.Size}.Add(gtx.Ops)
|
||||||
|
|
||||||
|
offset := pos.Position(widgetSize, dims.Size)
|
||||||
|
op.Offset(layout.FPt(offset)).Add(gtx.Ops)
|
||||||
|
dims.Baseline += offset.Y
|
||||||
|
return dims
|
||||||
|
}
|
||||||
|
case Fill:
|
||||||
|
}
|
||||||
|
|
||||||
|
var scaledSize image.Point
|
||||||
|
scaledSize.X = int(float32(widgetSize.X) * scale.X)
|
||||||
|
scaledSize.Y = int(float32(widgetSize.Y) * scale.Y)
|
||||||
|
dims.Size = gtx.Constraints.Constrain(scaledSize)
|
||||||
|
dims.Baseline = int(float32(dims.Baseline) * scale.Y)
|
||||||
|
|
||||||
|
clip.Rect{Max: dims.Size}.Add(gtx.Ops)
|
||||||
|
|
||||||
|
offset := pos.Position(scaledSize, dims.Size)
|
||||||
|
op.Affine(f32.Affine2D{}.
|
||||||
|
Scale(f32.Point{}, scale).
|
||||||
|
Offset(layout.FPt(offset)),
|
||||||
|
).Add(gtx.Ops)
|
||||||
|
|
||||||
|
dims.Baseline += offset.Y
|
||||||
|
|
||||||
|
return dims
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package widget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gioui.org/f32"
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFit(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
Dims image.Point
|
||||||
|
Scale f32.Point
|
||||||
|
Result image.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
fittests := [...][]test{
|
||||||
|
Unscaled: {
|
||||||
|
{
|
||||||
|
Dims: image.Point{0, 0},
|
||||||
|
Scale: f32.Point{X: 1, Y: 1},
|
||||||
|
Result: image.Point{X: 0, Y: 0},
|
||||||
|
}, {
|
||||||
|
Dims: image.Point{50, 25},
|
||||||
|
Scale: f32.Point{X: 1, Y: 1},
|
||||||
|
Result: image.Point{X: 50, Y: 25},
|
||||||
|
}, {
|
||||||
|
Dims: image.Point{50, 200},
|
||||||
|
Scale: f32.Point{X: 1, Y: 1},
|
||||||
|
Result: image.Point{X: 50, Y: 100},
|
||||||
|
}},
|
||||||
|
Contain: {
|
||||||
|
{
|
||||||
|
Dims: image.Point{50, 25},
|
||||||
|
Scale: f32.Point{X: 2, Y: 2},
|
||||||
|
Result: image.Point{X: 100, Y: 50},
|
||||||
|
}, {
|
||||||
|
Dims: image.Point{50, 200},
|
||||||
|
Scale: f32.Point{X: 0.5, Y: 0.5},
|
||||||
|
Result: image.Point{X: 25, Y: 100},
|
||||||
|
}},
|
||||||
|
Cover: {
|
||||||
|
{
|
||||||
|
Dims: image.Point{50, 25},
|
||||||
|
Scale: f32.Point{X: 4, Y: 4},
|
||||||
|
Result: image.Point{X: 100, Y: 100},
|
||||||
|
}, {
|
||||||
|
Dims: image.Point{50, 200},
|
||||||
|
Scale: f32.Point{X: 2, Y: 2},
|
||||||
|
Result: image.Point{X: 100, Y: 100},
|
||||||
|
}},
|
||||||
|
ScaleDown: {
|
||||||
|
{
|
||||||
|
Dims: image.Point{50, 25},
|
||||||
|
Scale: f32.Point{X: 1, Y: 1},
|
||||||
|
Result: image.Point{X: 50, Y: 25},
|
||||||
|
}, {
|
||||||
|
Dims: image.Point{50, 200},
|
||||||
|
Scale: f32.Point{X: 0.5, Y: 0.5},
|
||||||
|
Result: image.Point{X: 25, Y: 100},
|
||||||
|
}},
|
||||||
|
Fill: {
|
||||||
|
{
|
||||||
|
Dims: image.Point{50, 25},
|
||||||
|
Scale: f32.Point{X: 2, Y: 4},
|
||||||
|
Result: image.Point{X: 100, Y: 100},
|
||||||
|
}, {
|
||||||
|
Dims: image.Point{50, 200},
|
||||||
|
Scale: f32.Point{X: 2, Y: 0.5},
|
||||||
|
Result: image.Point{X: 100, Y: 100},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for fit, tests := range fittests {
|
||||||
|
fit := Fit(fit)
|
||||||
|
for i, test := range tests {
|
||||||
|
ops := new(op.Ops)
|
||||||
|
gtx := layout.Context{
|
||||||
|
Ops: ops,
|
||||||
|
Constraints: layout.Constraints{
|
||||||
|
Max: image.Point{X: 100, Y: 100},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := fit.scale(gtx, layout.NW, layout.Dimensions{Size: test.Dims})
|
||||||
|
|
||||||
|
if test.Scale.X != 1 || test.Scale.Y != 1 {
|
||||||
|
opsdata := gtx.Ops.Data()
|
||||||
|
scaleX := float32Bytes(test.Scale.X)
|
||||||
|
scaleY := float32Bytes(test.Scale.Y)
|
||||||
|
if !bytes.Contains(opsdata, scaleX) {
|
||||||
|
t.Errorf("did not find scale.X:%v (%x) in ops: %x", test.Scale.X, scaleX, opsdata)
|
||||||
|
}
|
||||||
|
if !bytes.Contains(opsdata, scaleY) {
|
||||||
|
t.Errorf("did not find scale.Y:%v (%x) in ops: %x", test.Scale.Y, scaleY, opsdata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Size != test.Result {
|
||||||
|
t.Errorf("fit %v, #%v: expected %#v, got %#v", fit, i, test.Result, result.Size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func float32Bytes(v float32) []byte {
|
||||||
|
var dst [4]byte
|
||||||
|
binary.LittleEndian.PutUint32(dst[:], math.Float32bits(v))
|
||||||
|
return dst[:]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user