mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
widget,widget/material: add Float and Slider
Signed-off-by: Gordon Klaus <gordon.klaus@gmail.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
Reference in New Issue
Block a user