widget: [API] move Clickable state update from Layout to Clicks

Before this change, Clickable state updates would happen in Layout.
However, that is too late in cases where clicks affects layout that
contiains the Clickable.

This change removes state changes from Layout and moves them to Clicks,
to allow users pre-layout access. Note that Layout itself processes
events, which means users can no longer access clicks after Layout.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-09-29 17:07:43 -05:00
parent 1686874d07
commit 4a4fe5a69b
6 changed files with 64 additions and 72 deletions
+4 -4
View File
@@ -43,11 +43,11 @@ func (b *Bool) History() []Press {
} }
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
for b.clk.Clicked(gtx) {
b.Value = !b.Value
b.changed = true
}
dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions { dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
for b.clk.Clicked() {
b.Value = !b.Value
b.changed = true
}
semantic.SelectedOp(b.Value).Add(gtx.Ops) semantic.SelectedOp(b.Value).Add(gtx.Ops)
semantic.EnabledOp(gtx.Queue != nil).Add(gtx.Ops) semantic.EnabledOp(gtx.Queue != nil).Add(gtx.Ops)
return w(gtx) return w(gtx)
+49 -55
View File
@@ -17,18 +17,16 @@ import (
// Clickable represents a clickable area. // Clickable represents a clickable area.
type Clickable struct { type Clickable struct {
click gesture.Click click gesture.Click
clicks []Click // clicks is for saved clicks to support Clicked.
// prevClicks is the index into clicks that marks the clicks clicks []Click
// from the most recent Layout call. prevClicks is used to keep history []Press
// clicks bounded.
prevClicks int
history []Press
keyTag struct{} keyTag struct{}
requestFocus bool requestFocus bool
focused bool requestClicks int
pressedKey string focused bool
pressedKey string
} }
// Click represents a click. // Click represents a click.
@@ -50,26 +48,24 @@ type Press struct {
Cancelled bool Cancelled bool
} }
// Click executes a simple programmatic click // Click executes a simple programmatic click.
func (b *Clickable) Click() { func (b *Clickable) Click() {
b.clicks = append(b.clicks, Click{ b.requestClicks++
Modifiers: 0,
NumClicks: 1,
})
} }
// Clicked reports whether there are pending clicks as would be // Clicked reports whether there are pending clicks as would be
// reported by Clicks. If so, Clicked removes the earliest click. // reported by Clicks. If so, Clicked removes the earliest click.
func (b *Clickable) Clicked() bool { func (b *Clickable) Clicked(gtx layout.Context) bool {
if len(b.clicks) == 0 { if len(b.clicks) > 0 {
return false b.clicks = b.clicks[1:]
return true
} }
n := copy(b.clicks, b.clicks[1:]) b.clicks = b.Update(gtx)
b.clicks = b.clicks[:n] if len(b.clicks) > 0 {
if b.prevClicks > 0 { b.clicks = b.clicks[1:]
b.prevClicks-- return true
} }
return true return false
} }
// Hovered reports whether a pointer is over the element. // Hovered reports whether a pointer is over the element.
@@ -92,23 +88,15 @@ func (b *Clickable) Focused() bool {
return b.focused return b.focused
} }
// Clicks returns and clear the clicks since the last call to Clicks.
func (b *Clickable) Clicks() []Click {
clicks := b.clicks
b.clicks = nil
b.prevClicks = 0
return clicks
}
// History is the past pointer presses useful for drawing markers. // History is the past pointer presses useful for drawing markers.
// History is retained for a short duration (about a second). // History is retained for a short duration (about a second).
func (b *Clickable) History() []Press { func (b *Clickable) History() []Press {
return b.history return b.history
} }
// Layout and update the button state // Layout and update the button state.
func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
b.update(gtx) b.Update(gtx)
m := op.Record(gtx.Ops) m := op.Record(gtx.Ops)
dims := w(gtx) dims := w(gtx)
c := m.Stop() c := m.Stop()
@@ -122,14 +110,22 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension
keys = "" keys = ""
} }
key.InputOp{Tag: &b.keyTag, Keys: keys}.Add(gtx.Ops) key.InputOp{Tag: &b.keyTag, Keys: keys}.Add(gtx.Ops)
if b.requestFocus {
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
b.requestFocus = false
}
} else {
b.focused = false
} }
c.Add(gtx.Ops) c.Add(gtx.Ops)
return dims
}
// Update the button state by processing events, and return the resulting
// clicks, if any.
func (b *Clickable) Update(gtx layout.Context) []Click {
b.clicks = nil
if gtx.Queue == nil {
b.focused = false
}
if b.requestFocus {
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
b.requestFocus = false
}
for len(b.history) > 0 { for len(b.history) > 0 {
c := b.history[0] c := b.history[0]
if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second { if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
@@ -138,26 +134,23 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension
n := copy(b.history, b.history[1:]) n := copy(b.history, b.history[1:])
b.history = b.history[:n] b.history = b.history[:n]
} }
return dims var clicks []Click
} if c := b.requestClicks; c > 0 {
b.requestClicks = 0
// update the button state by processing events. clicks = append(clicks, Click{
func (b *Clickable) update(gtx layout.Context) { NumClicks: c,
// Flush clicks from before the last update. })
n := copy(b.clicks, b.clicks[b.prevClicks:]) }
b.clicks = b.clicks[:n]
b.prevClicks = n
for _, e := range b.click.Events(gtx) { for _, e := range b.click.Events(gtx) {
switch e.Kind { switch e.Kind {
case gesture.KindClick: case gesture.KindClick:
b.clicks = append(b.clicks, Click{
Modifiers: e.Modifiers,
NumClicks: e.NumClicks,
})
if l := len(b.history); l > 0 { if l := len(b.history); l > 0 {
b.history[l-1].End = gtx.Now b.history[l-1].End = gtx.Now
} }
clicks = append(clicks, Click{
Modifiers: e.Modifiers,
NumClicks: e.NumClicks,
})
case gesture.KindCancel: case gesture.KindCancel:
for i := range b.history { for i := range b.history {
b.history[i].Cancelled = true b.history[i].Cancelled = true
@@ -198,11 +191,12 @@ func (b *Clickable) update(gtx layout.Context) {
} }
// only register a key as a click if the key was pressed and released while this button was focused // only register a key as a click if the key was pressed and released while this button was focused
b.pressedKey = "" b.pressedKey = ""
b.clicks = append(b.clicks, Click{ clicks = append(clicks, Click{
Modifiers: e.Modifiers, Modifiers: e.Modifiers,
NumClicks: 1, NumClicks: 1,
}) })
} }
} }
} }
return clicks
} }
+6 -6
View File
@@ -48,6 +48,7 @@ func TestClickable(t *testing.T) {
t.Error("button 2 should not have focus") t.Error("button 2 should not have focus")
} }
// frame: press & release return // frame: press & release return
frame()
r.Queue( r.Queue(
key.Event{ key.Event{
Name: key.NameReturn, Name: key.NameReturn,
@@ -58,11 +59,10 @@ func TestClickable(t *testing.T) {
State: key.Release, State: key.Release,
}, },
) )
frame() if !b1.Clicked(gtx) {
if !b1.Clicked() {
t.Error("button 1 did not get clicked when it got return press & release") t.Error("button 1 did not get clicked when it got return press & release")
} }
if b2.Clicked() { if b2.Clicked(gtx) {
t.Error("button 2 got clicked when it did not have focus") t.Error("button 2 got clicked when it did not have focus")
} }
// frame: press return down // frame: press return down
@@ -73,7 +73,7 @@ func TestClickable(t *testing.T) {
}, },
) )
frame() frame()
if b1.Clicked() { if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it only got return press") t.Error("button 1 got clicked, even if it only got return press")
} }
// frame: request focus for button 2 // frame: request focus for button 2
@@ -95,10 +95,10 @@ func TestClickable(t *testing.T) {
}, },
) )
frame() frame()
if b1.Clicked() { if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it had lost focus") t.Error("button 1 got clicked, even if it had lost focus")
} }
if b2.Clicked() { if b2.Clicked(gtx) {
t.Error("button 2 should not have been clicked, as it only got return release") t.Error("button 2 should not have been clicked, as it only got return release")
} }
} }
+2 -2
View File
@@ -30,7 +30,7 @@ func (d *Decorations) LayoutMove(gtx layout.Context, w layout.Widget) layout.Dim
} }
// Clickable returns the clickable for the given single action. // Clickable returns the clickable for the given single action.
func (d *Decorations) Clickable(action system.Action) *Clickable { func (d *Decorations) Clickable(gtx layout.Context, action system.Action) *Clickable {
if bits.OnesCount(uint(action)) != 1 { if bits.OnesCount(uint(action)) != 1 {
panic(fmt.Errorf("not a single action")) panic(fmt.Errorf("not a single action"))
} }
@@ -39,7 +39,7 @@ func (d *Decorations) Clickable(action system.Action) *Clickable {
d.clicks = append(d.clicks, make([]Clickable, n+1)...) d.clicks = append(d.clicks, make([]Clickable, n+1)...)
} }
click := &d.clicks[idx] click := &d.clicks[idx]
if click.Clicked() { if click.Clicked(gtx) {
if action == system.ActionMaximize { if action == system.ActionMaximize {
if d.maximized { if d.maximized {
d.maximized = false d.maximized = false
+2 -4
View File
@@ -57,13 +57,11 @@ func ExampleClickable_passthrough() {
Position: f32.Pt(50, 50), Position: f32.Pt(50, 50),
}, },
) )
// The second layout ensures that the click event is registered by the buttons.
widget()
if button1.Clicked() { if button1.Clicked(gtx) {
fmt.Println("button1 clicked!") fmt.Println("button1 clicked!")
} }
if button2.Clicked() { if button2.Clicked(gtx) {
fmt.Println("button2 clicked!") fmt.Println("button2 clicked!")
} }
+1 -1
View File
@@ -85,7 +85,7 @@ func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimension
default: default:
continue continue
} }
cl := d.Decorations.Clickable(a) cl := d.Decorations.Clickable(gtx, a)
dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions { dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops) semantic.Button.Add(gtx.Ops)
return layout.Stack{Alignment: layout.Center}.Layout(gtx, return layout.Stack{Alignment: layout.Center}.Layout(gtx,