io/input,widget: [API] replace per-widget Focused with Source.Focused

Widgets have themselves as tags, by convention, and so it's possible to
replace the per-widget Focused methods with a general-purpose Source.
Focused query.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-11-28 10:35:51 -06:00
parent c3f2abebca
commit e59f91dfd0
9 changed files with 65 additions and 68 deletions
+2 -2
View File
@@ -318,8 +318,8 @@ func TestKeyRouting(t *testing.T) {
func assertFocus(t *testing.T, router *Router, expected event.Tag) { func assertFocus(t *testing.T, router *Router, expected event.Tag) {
t.Helper() t.Helper()
if got := router.state().focus; got != expected { if !router.Source().Focused(expected) {
t.Errorf("expected %v to be focused, got %v", expected, got) t.Errorf("expected %v to be focused", expected)
} }
} }
+9
View File
@@ -184,6 +184,15 @@ func (s Source) Enabled() bool {
return s.r != nil return s.r != nil
} }
// Focused reports whether tag is focused, according to the most recent
// [key.FocusEvent] delivered.
func (s Source) Focused(tag event.Tag) bool {
if !s.Enabled() {
return false
}
return s.r.state().keyState.focus == tag
}
// Event returns the next event that matches at least one of filters. // Event returns the next event that matches at least one of filters.
func (s Source) Event(filters ...event.Filter) (event.Event, bool) { func (s Source) Event(filters ...event.Filter) (event.Event, bool) {
if !s.Enabled() { if !s.Enabled() {
+2 -7
View File
@@ -16,7 +16,7 @@ type Bool struct {
// Update the widget state and report whether Value was changed. // Update the widget state and report whether Value was changed.
func (b *Bool) Update(gtx layout.Context) bool { func (b *Bool) Update(gtx layout.Context) bool {
changed := false changed := false
for b.clk.Clicked(gtx) { for b.clk.clicked(b, gtx) {
b.Value = !b.Value b.Value = !b.Value
changed = true changed = true
} }
@@ -33,18 +33,13 @@ func (b *Bool) Pressed() bool {
return b.clk.Pressed() return b.clk.Pressed()
} }
// Focused reports whether b has focus.
func (b *Bool) Focused() bool {
return b.clk.Focused()
}
func (b *Bool) History() []Press { func (b *Bool) History() []Press {
return b.clk.History() return b.clk.History()
} }
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
b.Update(gtx) b.Update(gtx)
dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions { dims := b.clk.layout(b, gtx, func(gtx layout.Context) layout.Dimensions {
semantic.SelectedOp(b.Value).Add(gtx.Ops) semantic.SelectedOp(b.Value).Add(gtx.Ops)
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
return w(gtx) return w(gtx)
+21 -19
View File
@@ -22,7 +22,6 @@ type Clickable struct {
history []Press history []Press
requestClicks int requestClicks int
focused bool
pressedKey key.Name pressedKey key.Name
} }
@@ -52,7 +51,11 @@ func (b *Clickable) Click() {
// Clicked calls Update and reports whether a click was registered. // Clicked calls Update and reports whether a click was registered.
func (b *Clickable) Clicked(gtx layout.Context) bool { func (b *Clickable) Clicked(gtx layout.Context) bool {
_, clicked := b.Update(gtx) return b.clicked(b, gtx)
}
func (b *Clickable) clicked(t event.Tag, gtx layout.Context) bool {
_, clicked := b.update(t, gtx)
return clicked return clicked
} }
@@ -66,11 +69,6 @@ func (b *Clickable) Pressed() bool {
return b.click.Pressed() return b.click.Pressed()
} }
// Focused reports whether b has focus.
func (b *Clickable) Focused() bool {
return b.focused
}
// 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 {
@@ -79,8 +77,12 @@ func (b *Clickable) History() []Press {
// 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 {
return b.layout(b, gtx, w)
}
func (b *Clickable) layout(t event.Tag, gtx layout.Context, w layout.Widget) layout.Dimensions {
for { for {
_, ok := b.Update(gtx) _, ok := b.update(t, gtx)
if !ok { if !ok {
break break
} }
@@ -91,7 +93,7 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
b.click.Add(gtx.Ops) b.click.Add(gtx.Ops)
event.InputOp(gtx.Ops, b) event.InputOp(gtx.Ops, t)
c.Add(gtx.Ops) c.Add(gtx.Ops)
return dims return dims
} }
@@ -99,9 +101,10 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension
// Update the button state by processing events, and return the next // Update the button state by processing events, and return the next
// click, if any. // click, if any.
func (b *Clickable) Update(gtx layout.Context) (Click, bool) { func (b *Clickable) Update(gtx layout.Context) (Click, bool) {
if !gtx.Enabled() { return b.update(b, gtx)
b.focused = false }
}
func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
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 {
@@ -139,7 +142,7 @@ func (b *Clickable) Update(gtx layout.Context) (Click, bool) {
} }
case gesture.KindPress: case gesture.KindPress:
if e.Source == pointer.Mouse { if e.Source == pointer.Mouse {
gtx.Execute(key.FocusCmd{Tag: b}) gtx.Execute(key.FocusCmd{Tag: t})
} }
b.history = append(b.history, Press{ b.history = append(b.history, Press{
Position: e.Position, Position: e.Position,
@@ -149,21 +152,20 @@ func (b *Clickable) Update(gtx layout.Context) (Click, bool) {
} }
for { for {
e, ok := gtx.Event( e, ok := gtx.Event(
key.FocusFilter{Target: b}, key.FocusFilter{Target: t},
key.Filter{Focus: b, Name: key.NameReturn}, key.Filter{Focus: t, Name: key.NameReturn},
key.Filter{Focus: b, Name: key.NameSpace}, key.Filter{Focus: t, Name: key.NameSpace},
) )
if !ok { if !ok {
break break
} }
switch e := e.(type) { switch e := e.(type) {
case key.FocusEvent: case key.FocusEvent:
b.focused = e.Focus if e.Focus {
if !b.focused {
b.pressedKey = "" b.pressedKey = ""
} }
case key.Event: case key.Event:
if !b.focused { if !gtx.Focused(t) {
break break
} }
if e.Name != key.NameReturn && e.Name != key.NameSpace { if e.Name != key.NameReturn && e.Name != key.NameSpace {
+4 -4
View File
@@ -39,10 +39,10 @@ func TestClickable(t *testing.T) {
} }
gtx.Execute(key.FocusCmd{Tag: &b1}) gtx.Execute(key.FocusCmd{Tag: &b1})
frame() frame()
if !b1.Focused() { if !gtx.Focused(&b1) {
t.Error("button 1 did not gain focus") t.Error("button 1 did not gain focus")
} }
if b2.Focused() { if gtx.Focused(&b2) {
t.Error("button 2 should not have focus") t.Error("button 2 should not have focus")
} }
r.Queue( r.Queue(
@@ -73,10 +73,10 @@ func TestClickable(t *testing.T) {
frame() frame()
gtx.Execute(key.FocusCmd{Tag: &b2}) gtx.Execute(key.FocusCmd{Tag: &b2})
frame() frame()
if b1.Focused() { if gtx.Focused(&b1) {
t.Error("button 1 should not have focus") t.Error("button 1 should not have focus")
} }
if !b2.Focused() { if !gtx.Focused(&b2) {
t.Error("button 2 did not gain focus") t.Error("button 2 did not gain focus")
} }
r.Queue( r.Queue(
+22 -31
View File
@@ -72,7 +72,6 @@ type Editor struct {
// from the textView. // from the textView.
scratch []byte scratch []byte
blinkStart time.Time blinkStart time.Time
focused bool
// ime tracks the state relevant to input methods. // ime tracks the state relevant to input methods.
ime struct { ime struct {
@@ -385,14 +384,13 @@ func (e *Editor) processKey(gtx layout.Context) {
e.blinkStart = gtx.Now e.blinkStart = gtx.Now
switch ke := ke.(type) { switch ke := ke.(type) {
case key.FocusEvent: case key.FocusEvent:
e.focused = ke.Focus
// Reset IME state. // Reset IME state.
e.ime.imeState = imeState{} e.ime.imeState = imeState{}
if ke.Focus { if ke.Focus {
gtx.Execute(key.SoftKeyboardCmd{Show: true}) gtx.Execute(key.SoftKeyboardCmd{Show: true})
} }
case key.Event: case key.Event:
if !e.focused || ke.State != key.Press { if !gtx.Focused(e) || ke.State != key.Press {
break break
} }
if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
@@ -558,11 +556,6 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
} }
} }
// Focused returns whether the editor is focused or not.
func (e *Editor) Focused() bool {
return e.focused
}
// initBuffer should be invoked first in every exported function that accesses // initBuffer should be invoked first in every exported function that accesses
// text state. It ensures that the underlying text widget is both ready to use // text state. It ensures that the underlying text widget is both ready to use
// and has its fields synced with the editor. // and has its fields synced with the editor.
@@ -583,27 +576,25 @@ func (e *Editor) initBuffer() {
func (e *Editor) Update(gtx layout.Context) { func (e *Editor) Update(gtx layout.Context) {
e.initBuffer() e.initBuffer()
e.processEvents(gtx) e.processEvents(gtx)
if e.focused { // Notify IME of selection if it changed.
// Notify IME of selection if it changed. newSel := e.ime.selection
newSel := e.ime.selection start, end := e.text.Selection()
start, end := e.text.Selection() newSel.rng = key.Range{
newSel.rng = key.Range{ Start: start,
Start: start, End: end,
End: end,
}
caretPos, carAsc, carDesc := e.text.CaretInfo()
newSel.caret = key.Caret{
Pos: layout.FPt(caretPos),
Ascent: float32(carAsc),
Descent: float32(carDesc),
}
if newSel != e.ime.selection {
e.ime.selection = newSel
gtx.Execute(key.SelectionCmd{Tag: e, Range: newSel.rng, Caret: newSel.caret})
}
e.updateSnippet(gtx, e.ime.start, e.ime.end)
} }
caretPos, carAsc, carDesc := e.text.CaretInfo()
newSel.caret = key.Caret{
Pos: layout.FPt(caretPos),
Ascent: float32(carAsc),
Descent: float32(carDesc),
}
if newSel != e.ime.selection {
e.ime.selection = newSel
gtx.Execute(key.SelectionCmd{Tag: e, Range: newSel.rng, Caret: newSel.caret})
}
e.updateSnippet(gtx, e.ime.start, e.ime.end)
} }
// Layout lays out the editor using the provided textMaterial as the paint material // Layout lays out the editor using the provided textMaterial as the paint material
@@ -679,7 +670,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
e.clicker.Add(gtx.Ops) e.clicker.Add(gtx.Ops)
e.dragger.Add(gtx.Ops) e.dragger.Add(gtx.Ops)
e.showCaret = false e.showCaret = false
if e.focused { if gtx.Focused(e) {
now := gtx.Now now := gtx.Now
dt := now.Sub(e.blinkStart) dt := now.Sub(e.blinkStart)
blinking := dt < maxBlinkDuration blinking := dt < maxBlinkDuration
@@ -688,7 +679,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
if blinking { if blinking {
gtx.Execute(op.InvalidateCmd{At: nextBlink}) gtx.Execute(op.InvalidateCmd{At: nextBlink})
} }
e.showCaret = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2) e.showCaret = !blinking || dt%timePerBlink < timePerBlink/2
} }
semantic.Editor.Add(gtx.Ops) semantic.Editor.Add(gtx.Ops)
if e.Len() > 0 { if e.Len() > 0 {
@@ -705,7 +696,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
// material to set the painting material for the selection. // material to set the painting material for the selection.
func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) { func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) {
e.initBuffer() e.initBuffer()
if !e.focused { if !gtx.Focused(e) {
return return
} }
e.text.PaintSelection(gtx, material) e.text.PaintSelection(gtx, material)
+3 -3
View File
@@ -96,7 +96,7 @@ func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) la
return layout.Background{}.Layout(gtx, return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions { func(gtx layout.Context) layout.Dimensions {
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop() defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
if button.Hovered() || button.Focused() { if button.Hovered() || gtx.Focused(button) {
paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{})) paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{}))
} }
for _, c := range button.History() { for _, c := range button.History() {
@@ -135,7 +135,7 @@ func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Di
switch { switch {
case !gtx.Enabled(): case !gtx.Enabled():
background = f32color.Disabled(b.Background) background = f32color.Disabled(b.Background)
case b.Button.Hovered() || b.Button.Focused(): case b.Button.Hovered() || gtx.Focused(b.Button):
background = f32color.Hovered(b.Background) background = f32color.Hovered(b.Background)
} }
paint.Fill(gtx.Ops, background) paint.Fill(gtx.Ops, background)
@@ -167,7 +167,7 @@ func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
switch { switch {
case !gtx.Enabled(): case !gtx.Enabled():
background = f32color.Disabled(b.Background) background = f32color.Disabled(b.Background)
case b.Button.Hovered() || b.Button.Focused(): case b.Button.Hovered() || gtx.Focused(b.Button):
background = f32color.Hovered(b.Background) background = f32color.Hovered(b.Background)
} }
paint.Fill(gtx.Ops, background) paint.Fill(gtx.Ops, background)
+1 -1
View File
@@ -35,6 +35,6 @@ func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle {
func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions { func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions {
return c.CheckBox.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return c.CheckBox.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.CheckBox.Add(gtx.Ops) semantic.CheckBox.Add(gtx.Ops)
return c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered() || c.CheckBox.Focused()) return c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered() || gtx.Focused(c.CheckBox))
}) })
} }
+1 -1
View File
@@ -98,7 +98,7 @@ func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions {
return clip.Ellipse(b).Op(gtx.Ops) return clip.Ellipse(b).Op(gtx.Ops)
} }
// Draw hover. // Draw hover.
if s.Switch.Hovered() || s.Switch.Focused() { if s.Switch.Hovered() || gtx.Focused(s.Switch) {
r := thumbRadius * 10 / 17 r := thumbRadius * 10 / 17
background := f32color.MulAlpha(s.Color.Enabled, 70) background := f32color.MulAlpha(s.Color.Enabled, 70)
paint.FillShape(gtx.Ops, background, circle(thumbRadius, thumbRadius, r)) paint.FillShape(gtx.Ops, background, circle(thumbRadius, thumbRadius, r))