From 5368743478e0f3be301e003f59da9109845176d6 Mon Sep 17 00:00:00 2001 From: Gordon Klaus Date: Wed, 17 Jun 2020 19:24:25 +0200 Subject: [PATCH] widget,widget/material: add Float and Slider Signed-off-by: Gordon Klaus --- 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 d9e8e2e5..36ecd807 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 00000000..a439f325 --- /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 00000000..fa2cbd08 --- /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.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} +}