~eliasnaur/gio-patches

gio: widget,widget/material: add Float and Slider v1 PROPOSED

~gordonklaus
~gordonklaus: 2
 widget,widget/material: add Float and Slider
 example/kitchen: add Float/Slider

 4 files changed, 279 insertions(+), 1 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/11287/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH gio 1/2] widget,widget/material: add Float and Slider Export this patch

~gordonklaus
From: Gordon Klaus <gordon.klaus@gmail.com>

Signed-off-by: Gordon Klaus <gordon.klaus@gmail.com>
---
 gesture/gesture.go        |  69 ++++++++++++++++++++++++
 widget/float.go           |  88 ++++++++++++++++++++++++++++++
 widget/material/slider.go | 109 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 266 insertions(+)
 create mode 100644 widget/float.go
 create mode 100644 widget/material/slider.go

diff --git a/gesture/gesture.go b/gesture/gesture.go
index d9e8e2e..36ecd80 100644
--- a/gesture/gesture.go
+++ b/gesture/gesture.go
@@ -62,6 +62,14 @@ type ClickEvent struct {

type ClickType uint8

// Drag detects drag gestures in the form of pointer.Drag events.
type Drag struct {
	dragging bool
	pid      pointer.ID
	start    f32.Point
	grab     bool
}

// Scroll detects scroll gestures and reduces them to
// scroll distances. Scroll recognizes mouse wheel
// movements as well as drag and fling touch gestures.
@@ -301,6 +309,67 @@ func (s *Scroll) State() ScrollState {
	}
}

// Add the handler to the operation list to receive drag events.
func (d *Drag) Add(ops *op.Ops) {
	op := pointer.InputOp{
		Tag:   d,
		Grab:  d.grab,
		Types: pointer.Press | pointer.Drag | pointer.Release,
	}
	op.Add(ops)
}

// Events returns the next drag events, if any.
func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
	var events []pointer.Event
	for _, e := range q.Events(d) {
		e, ok := e.(pointer.Event)
		if !ok {
			continue
		}

		switch e.Type {
		case pointer.Press:
			if !(e.Buttons == pointer.ButtonLeft || e.Source == pointer.Touch) {
				continue
			}
			if d.dragging {
				continue
			}
			d.dragging = true
			d.pid = e.PointerID
			d.start = e.Position
		case pointer.Drag:
			if !d.dragging || e.PointerID != d.pid {
				continue
			}
			switch axis {
			case Horizontal:
				e.Position.Y = d.start.Y
			case Vertical:
				e.Position.X = d.start.X
			}
			if e.Priority < pointer.Grabbed {
				diff := e.Position.Sub(d.start)
				slop := cfg.Px(touchSlop)
				if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
					d.grab = true
				}
			}
		case pointer.Release, pointer.Cancel:
			if !d.dragging || e.PointerID != d.pid {
				continue
			}
			d.dragging = false
			d.grab = false
		}

		events = append(events, e)
	}

	return events
}

func (a Axis) String() string {
	switch a {
	case Horizontal:
diff --git a/widget/float.go b/widget/float.go
new file mode 100644
index 0000000..a439f32
--- /dev/null
+++ b/widget/float.go
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: Unlicense OR MIT

package widget

import (
	"image"

	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"
)

// Float is for selecting a value in a range.
type Float struct {
	Value float32

	drag    gesture.Drag
	pos     float32 // position normalized to [0, 1]
	length  float32
	changed bool
}

// Layout processes events.
func (f *Float) Layout(gtx layout.Context, pointerMargin int, min, max float32) layout.Dimensions {
	size := gtx.Constraints.Min
	f.length = float32(size.X)

	var de *pointer.Event
	for _, e := range f.drag.Events(gtx.Metric, gtx, gesture.Horizontal) {
		if e.Type == pointer.Press || e.Type == pointer.Drag {
			de = &e
		}
	}

	value := f.Value
	if de != nil {
		f.pos = de.Position.X / f.length
		value = min + (max-min)*f.pos
	} else if min != max {
		f.pos = value/(max-min) - min
	}
	// Unconditionally call setValue in case min, max, or value changed.
	f.setValue(value, min, max)

	if f.pos < 0 {
		f.pos = 0
	} else if f.pos > 1 {
		f.pos = 1
	}

	defer op.Push(gtx.Ops).Pop()
	rect := image.Rectangle{Max: size}
	rect.Min.X -= pointerMargin
	rect.Max.X += pointerMargin
	pointer.Rect(rect).Add(gtx.Ops)
	f.drag.Add(gtx.Ops)

	return layout.Dimensions{Size: size}
}

func (f *Float) setValue(value, min, max float32) {
	if min > max {
		min, max = max, min
	}
	if value < min {
		value = min
	} else if value > max {
		value = max
	}
	if f.Value != value {
		f.Value = value
		f.changed = true
	}
}

// Pos reports the selected position.
func (f *Float) Pos() float32 {
	return f.pos * f.length
}

// Changed reports whether the value has changed since
// the last call to Changed.
func (f *Float) Changed() bool {
	changed := f.changed
	f.changed = false
	return changed
}
diff --git a/widget/material/slider.go b/widget/material/slider.go
new file mode 100644
index 0000000..e1fcc5b
--- /dev/null
+++ b/widget/material/slider.go
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: Unlicense OR MIT

package material

import (
	"image"
	"image/color"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
)

// Slider is for selecting a value in a range.
func Slider(th *Theme, float *widget.Float, min, max float32) SliderStyle {
	return SliderStyle{
		Min:   min,
		Max:   max,
		Color: th.Color.Primary,
		Float: float,
	}
}

type SliderStyle struct {
	Min, Max float32
	Color    color.RGBA
	Float    *widget.Float
}

func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
	thumbRadiusInt := gtx.Px(unit.Dp(6))
	trackWidth := float32(gtx.Px(unit.Dp(2)))
	thumbRadius := float32(thumbRadiusInt)
	halfWidthInt := 2 * thumbRadiusInt
	halfWidth := float32(halfWidthInt)

	size := gtx.Constraints.Min
	// Keep a minimum length so that the track is always visible.
	minLength := halfWidthInt + 3*thumbRadiusInt + halfWidthInt
	if size.X < minLength {
		size.X = minLength
	}
	size.Y = 2 * halfWidthInt

	st := op.Push(gtx.Ops)
	op.TransformOp{}.Offset(f32.Pt(halfWidth, 0)).Add(gtx.Ops)
	gtx.Constraints.Min = image.Pt(size.X-2*halfWidthInt, size.Y)
	s.Float.Layout(gtx, halfWidthInt, s.Min, s.Max)
	thumbPos := halfWidth + s.Float.Pos()
	st.Pop()

	color := s.Color
	if gtx.Queue == nil {
		color = mulAlpha(color, 150)
	}

	// Draw track before thumb.
	st = op.Push(gtx.Ops)
	track := f32.Rectangle{
		Min: f32.Point{
			X: halfWidth,
			Y: halfWidth - trackWidth/2,
		},
		Max: f32.Point{
			X: thumbPos,
			Y: halfWidth + trackWidth/2,
		},
	}
	clip.Rect{Rect: track}.Op(gtx.Ops).Add(gtx.Ops)
	paint.ColorOp{Color: color}.Add(gtx.Ops)
	paint.PaintOp{Rect: track}.Add(gtx.Ops)
	st.Pop()

	// Draw track after thumb.
	st = op.Push(gtx.Ops)
	track.Min.X = thumbPos
	track.Max.X = float32(size.X) - halfWidth
	clip.Rect{Rect: track}.Op(gtx.Ops).Add(gtx.Ops)
	paint.ColorOp{Color: mulAlpha(color, 96)}.Add(gtx.Ops)
	paint.PaintOp{Rect: track}.Add(gtx.Ops)
	st.Pop()

	// Draw thumb.
	st = op.Push(gtx.Ops)
	thumb := f32.Rectangle{
		Min: f32.Point{
			X: thumbPos - thumbRadius,
			Y: halfWidth - thumbRadius,
		},
		Max: f32.Point{
			X: thumbPos + thumbRadius,
			Y: halfWidth + thumbRadius,
		},
	}
	rr := thumbRadius
	clip.Rect{
		Rect: thumb,
		NE:   rr, NW: rr, SE: rr, SW: rr,
	}.Op(gtx.Ops).Add(gtx.Ops)
	paint.ColorOp{Color: color}.Add(gtx.Ops)
	paint.PaintOp{Rect: thumb}.Add(gtx.Ops)
	st.Pop()

	return layout.Dimensions{Size: size}
}
-- 
2.26.2

[PATCH gio 2/2] example/kitchen: add Float/Slider Export this patch

~gordonklaus
From: Gordon Klaus <gordon.klaus@gmail.com>

Signed-off-by: Gordon Klaus <gordon.klaus@gmail.com>
---
 example/kitchen/kitchen.go | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/example/kitchen/kitchen.go b/example/kitchen/kitchen.go
index 86e6385..50506b3 100644
--- a/example/kitchen/kitchen.go
+++ b/example/kitchen/kitchen.go
@@ -13,6 +13,7 @@ import (
	"image/png"
	"io/ioutil"
	"log"
	"math"
	"os"
	"time"

@@ -66,7 +67,7 @@ func main() {
	}()

	go func() {
		w := app.NewWindow(app.Size(unit.Dp(800), unit.Dp(650)))
		w := app.NewWindow(app.Size(unit.Dp(800), unit.Dp(700)))
		if err := loop(w); err != nil {
			log.Fatal(err)
		}
@@ -163,6 +164,7 @@ var (
	icon                *widget.Icon
	checkbox            = new(widget.Bool)
	swtch               = new(widget.Bool)
	float               = new(widget.Float)
)

type (
@@ -295,6 +297,16 @@ func kitchen(gtx layout.Context, th *material.Theme) layout.Dimensions {
				layout.Rigid(material.RadioButton(th, radioButtonsGroup, "r3", "RadioButton3").Layout),
			)
		},
		func(gtx C) D {
			return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
				layout.Flexed(1, material.Slider(th, float, 0, 2*math.Pi).Layout),
				layout.Rigid(func(gtx C) D {
					return layout.UniformInset(unit.Dp(8)).Layout(gtx,
						material.Body1(th, fmt.Sprintf("%.2f", float.Value)).Layout,
					)
				}),
			)
		},
	}

	return list.Layout(gtx, len(widgets), func(gtx C, i int) D {
-- 
2.26.2