From 4a4fe5a69bc01a6b195308fcd84326ae5f537b5d Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 29 Sep 2023 17:07:43 -0500 Subject: [PATCH] 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 --- widget/bool.go | 8 +-- widget/button.go | 104 ++++++++++++++++----------------- widget/button_test.go | 12 ++-- widget/decorations.go | 4 +- widget/example_test.go | 6 +- widget/material/decorations.go | 2 +- 6 files changed, 64 insertions(+), 72 deletions(-) diff --git a/widget/bool.go b/widget/bool.go index 55045dc7..740ce5b3 100644 --- a/widget/bool.go +++ b/widget/bool.go @@ -43,11 +43,11 @@ func (b *Bool) History() []Press { } 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 { - for b.clk.Clicked() { - b.Value = !b.Value - b.changed = true - } semantic.SelectedOp(b.Value).Add(gtx.Ops) semantic.EnabledOp(gtx.Queue != nil).Add(gtx.Ops) return w(gtx) diff --git a/widget/button.go b/widget/button.go index 38ba483c..da3c0acb 100644 --- a/widget/button.go +++ b/widget/button.go @@ -17,18 +17,16 @@ import ( // Clickable represents a clickable area. type Clickable struct { - click gesture.Click - clicks []Click - // prevClicks is the index into clicks that marks the clicks - // from the most recent Layout call. prevClicks is used to keep - // clicks bounded. - prevClicks int - history []Press + click gesture.Click + // clicks is for saved clicks to support Clicked. + clicks []Click + history []Press - keyTag struct{} - requestFocus bool - focused bool - pressedKey string + keyTag struct{} + requestFocus bool + requestClicks int + focused bool + pressedKey string } // Click represents a click. @@ -50,26 +48,24 @@ type Press struct { Cancelled bool } -// Click executes a simple programmatic click +// Click executes a simple programmatic click. func (b *Clickable) Click() { - b.clicks = append(b.clicks, Click{ - Modifiers: 0, - NumClicks: 1, - }) + b.requestClicks++ } // Clicked reports whether there are pending clicks as would be // reported by Clicks. If so, Clicked removes the earliest click. -func (b *Clickable) Clicked() bool { - if len(b.clicks) == 0 { - return false +func (b *Clickable) Clicked(gtx layout.Context) bool { + if len(b.clicks) > 0 { + b.clicks = b.clicks[1:] + return true } - n := copy(b.clicks, b.clicks[1:]) - b.clicks = b.clicks[:n] - if b.prevClicks > 0 { - b.prevClicks-- + b.clicks = b.Update(gtx) + if len(b.clicks) > 0 { + b.clicks = b.clicks[1:] + return true } - return true + return false } // Hovered reports whether a pointer is over the element. @@ -92,23 +88,15 @@ func (b *Clickable) Focused() bool { 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 retained for a short duration (about a second). func (b *Clickable) History() []Press { 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 { - b.update(gtx) + b.Update(gtx) m := op.Record(gtx.Ops) dims := w(gtx) c := m.Stop() @@ -122,14 +110,22 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension keys = "" } 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) + 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 { c := b.history[0] 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:]) b.history = b.history[:n] } - return dims -} - -// update the button state by processing events. -func (b *Clickable) update(gtx layout.Context) { - // Flush clicks from before the last update. - n := copy(b.clicks, b.clicks[b.prevClicks:]) - b.clicks = b.clicks[:n] - b.prevClicks = n - + var clicks []Click + if c := b.requestClicks; c > 0 { + b.requestClicks = 0 + clicks = append(clicks, Click{ + NumClicks: c, + }) + } for _, e := range b.click.Events(gtx) { switch e.Kind { case gesture.KindClick: - b.clicks = append(b.clicks, Click{ - Modifiers: e.Modifiers, - NumClicks: e.NumClicks, - }) if l := len(b.history); l > 0 { b.history[l-1].End = gtx.Now } + clicks = append(clicks, Click{ + Modifiers: e.Modifiers, + NumClicks: e.NumClicks, + }) case gesture.KindCancel: for i := range b.history { 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 b.pressedKey = "" - b.clicks = append(b.clicks, Click{ + clicks = append(clicks, Click{ Modifiers: e.Modifiers, NumClicks: 1, }) } } } + return clicks } diff --git a/widget/button_test.go b/widget/button_test.go index 9bb712f5..49f3a5ee 100644 --- a/widget/button_test.go +++ b/widget/button_test.go @@ -48,6 +48,7 @@ func TestClickable(t *testing.T) { t.Error("button 2 should not have focus") } // frame: press & release return + frame() r.Queue( key.Event{ Name: key.NameReturn, @@ -58,11 +59,10 @@ func TestClickable(t *testing.T) { State: key.Release, }, ) - frame() - if !b1.Clicked() { + if !b1.Clicked(gtx) { 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") } // frame: press return down @@ -73,7 +73,7 @@ func TestClickable(t *testing.T) { }, ) frame() - if b1.Clicked() { + if b1.Clicked(gtx) { t.Error("button 1 got clicked, even if it only got return press") } // frame: request focus for button 2 @@ -95,10 +95,10 @@ func TestClickable(t *testing.T) { }, ) frame() - if b1.Clicked() { + if b1.Clicked(gtx) { 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") } } diff --git a/widget/decorations.go b/widget/decorations.go index 38457825..184c38d9 100644 --- a/widget/decorations.go +++ b/widget/decorations.go @@ -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. -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 { 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)...) } click := &d.clicks[idx] - if click.Clicked() { + if click.Clicked(gtx) { if action == system.ActionMaximize { if d.maximized { d.maximized = false diff --git a/widget/example_test.go b/widget/example_test.go index 131936f7..2bfdbf8e 100644 --- a/widget/example_test.go +++ b/widget/example_test.go @@ -57,13 +57,11 @@ func ExampleClickable_passthrough() { 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!") } - if button2.Clicked() { + if button2.Clicked(gtx) { fmt.Println("button2 clicked!") } diff --git a/widget/material/decorations.go b/widget/material/decorations.go index d327dbbc..6e67f6fb 100644 --- a/widget/material/decorations.go +++ b/widget/material/decorations.go @@ -85,7 +85,7 @@ func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimension default: continue } - cl := d.Decorations.Clickable(a) + cl := d.Decorations.Clickable(gtx, a) dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions { semantic.Button.Add(gtx.Ops) return layout.Stack{Alignment: layout.Center}.Layout(gtx,