forked from joejulian/gio
d017c722f5
Before this change, events were typically processed twice or more per
widget: once in the Layout method for refreshing the visual state, and
once per method that queries for state changes.
One example is widget.Clickable that processed events in both its Layout
and Clicked method.
This change establishes the convention that events are processed once, in
the Layout method. There are several advantages to that approach:
- Query methods such as Clickable.Clicked no longer need a layout.Context.
- State updates from events only occur in Layout.
- Widgets are simplified because they won't need a separate processEvents
(or similar) method and won't forget to call it from methods other than Layout.
- Useless calls to gtx.Events are avoided (gtx.Events only returns events
for the first call each frame for a given event.Tag).
The disadvantage is that state updates from input events will not appear
before Layout. For example, in the call sequence
var btn *widget.Clickable
if btn.Clicked() {...}
btn.Layout(...)
the Clicked call will not detect an incoming click until the frame after it
happened.
This is ok because
- The Gio event router automatically dispatches an extra frame after events
arrive, bounding the latency from events to queries such as Clicked to
at most one frame (~17 ms).
- The potential extra frame of latency does not apply to Layout methods as long
as they process events before drawing. In other words, the visual feedback
from input events are not delayed because of this change.
Signed-off-by: Elias Naur <mail@eliasnaur.com>
133 lines
3.1 KiB
Go
133 lines
3.1 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package material
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
)
|
|
|
|
type SwitchStyle struct {
|
|
Color color.RGBA
|
|
Switch *widget.Bool
|
|
}
|
|
|
|
func Switch(th *Theme, swtch *widget.Bool) SwitchStyle {
|
|
return SwitchStyle{
|
|
Switch: swtch,
|
|
Color: th.Color.Primary,
|
|
}
|
|
}
|
|
|
|
// Layout updates the checkBox and displays it.
|
|
func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions {
|
|
trackWidth := gtx.Px(unit.Dp(36))
|
|
trackHeight := gtx.Px(unit.Dp(16))
|
|
thumbSize := gtx.Px(unit.Dp(20))
|
|
trackOff := float32(thumbSize-trackHeight) * .5
|
|
|
|
// Draw track.
|
|
var stack op.StackOp
|
|
stack.Push(gtx.Ops)
|
|
trackCorner := float32(trackHeight) / 2
|
|
trackRect := f32.Rectangle{Max: f32.Point{
|
|
X: float32(trackWidth),
|
|
Y: float32(trackHeight),
|
|
}}
|
|
op.TransformOp{}.Offset(f32.Point{Y: trackOff}).Add(gtx.Ops)
|
|
clip.Rect{
|
|
Rect: trackRect,
|
|
NE: trackCorner, NW: trackCorner, SE: trackCorner, SW: trackCorner,
|
|
}.Op(gtx.Ops).Add(gtx.Ops)
|
|
paint.ColorOp{Color: rgb(0x9b9b9b)}.Add(gtx.Ops)
|
|
paint.PaintOp{Rect: trackRect}.Add(gtx.Ops)
|
|
stack.Pop()
|
|
|
|
// Compute thumb offset and color.
|
|
stack.Push(gtx.Ops)
|
|
col := rgb(0xffffff)
|
|
if s.Switch.Value {
|
|
off := trackWidth - thumbSize
|
|
op.TransformOp{}.Offset(f32.Point{X: float32(off)}).Add(gtx.Ops)
|
|
col = s.Color
|
|
}
|
|
|
|
// Draw thumb shadow, a translucent disc slightly larger than the
|
|
// thumb itself.
|
|
var shadowStack op.StackOp
|
|
shadowStack.Push(gtx.Ops)
|
|
shadowSize := float32(2)
|
|
// Center shadow horizontally and slightly adjust its Y.
|
|
op.TransformOp{}.Offset(f32.Point{X: -shadowSize / 2, Y: -.75}).Add(gtx.Ops)
|
|
drawDisc(gtx.Ops, float32(thumbSize)+shadowSize, argb(0x55000000))
|
|
shadowStack.Pop()
|
|
|
|
// Draw thumb.
|
|
drawDisc(gtx.Ops, float32(thumbSize), col)
|
|
stack.Pop()
|
|
|
|
// Draw thumb ink.
|
|
stack.Push(gtx.Ops)
|
|
inkSize := float32(gtx.Px(unit.Dp(44)))
|
|
rr := inkSize * .5
|
|
inkOff := f32.Point{
|
|
X: float32(trackWidth)*.5 - rr,
|
|
Y: -rr + float32(trackHeight)*.5 + trackOff,
|
|
}
|
|
op.TransformOp{}.Offset(inkOff).Add(gtx.Ops)
|
|
clip.Rect{
|
|
Rect: f32.Rectangle{
|
|
Max: f32.Point{
|
|
X: inkSize,
|
|
Y: inkSize,
|
|
},
|
|
},
|
|
NE: rr, NW: rr, SE: rr, SW: rr,
|
|
}.Op(gtx.Ops).Add(gtx.Ops)
|
|
drawInk(gtx, s.Switch.Last)
|
|
stack.Pop()
|
|
|
|
// Set up click area.
|
|
stack.Push(gtx.Ops)
|
|
clickSize := gtx.Px(unit.Dp(40))
|
|
clickOff := f32.Point{
|
|
X: (float32(trackWidth) - float32(clickSize)) * .5,
|
|
Y: (float32(trackHeight)-float32(clickSize))*.5 + trackOff,
|
|
}
|
|
op.TransformOp{}.Offset(clickOff).Add(gtx.Ops)
|
|
pointer.Ellipse(image.Rectangle{
|
|
Max: image.Point{
|
|
X: clickSize, Y: clickSize,
|
|
},
|
|
}).Add(gtx.Ops)
|
|
s.Switch.Layout(gtx)
|
|
stack.Pop()
|
|
|
|
return layout.Dimensions{
|
|
Size: image.Point{X: trackWidth, Y: trackHeight},
|
|
}
|
|
}
|
|
|
|
func drawDisc(ops *op.Ops, sz float32, col color.RGBA) {
|
|
var stack op.StackOp
|
|
stack.Push(ops)
|
|
rr := sz / 2
|
|
r := f32.Rectangle{Max: f32.Point{X: sz, Y: sz}}
|
|
clip.Rect{
|
|
Rect: r,
|
|
NE: rr, NW: rr, SE: rr, SW: rr,
|
|
}.Op(ops).Add(ops)
|
|
paint.ColorOp{Color: col}.Add(ops)
|
|
paint.PaintOp{Rect: r}.Add(ops)
|
|
stack.Pop()
|
|
}
|