From 2c5daf10a2d1a3e2ec062f489bf59d887232cf83 Mon Sep 17 00:00:00 2001 From: Egon Elbre Date: Sun, 28 Feb 2021 16:54:13 +0200 Subject: [PATCH] widget: add Fit for scaling widgets Currently adds four different variants Unscaled, Contain, Cover, ScaleDown and Fill. Signed-off-by: Egon Elbre --- widget/fit.go | 106 ++++++++++++++++++++++++++++++++++++++++ widget/fit_test.go | 117 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 widget/fit.go create mode 100644 widget/fit_test.go diff --git a/widget/fit.go b/widget/fit.go new file mode 100644 index 00000000..d45259e1 --- /dev/null +++ b/widget/fit.go @@ -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 +} diff --git a/widget/fit_test.go b/widget/fit_test.go new file mode 100644 index 00000000..9d516fe3 --- /dev/null +++ b/widget/fit_test.go @@ -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[:] +}