From 6b1ca4ca7ec8a0ee08a1fcd7b3191da84cd5d3b1 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Wed, 1 Dec 2021 13:12:00 +0100 Subject: [PATCH] widget: add semantic descriptions Some semantic information is automatically extracted, but some must be provided by UI components. This change enriches the generic and material widgets with such information. Signed-off-by: Elias Naur --- widget/bool.go | 15 ++++++--- widget/button.go | 2 ++ widget/enum.go | 3 ++ widget/label.go | 3 ++ widget/material/button.go | 7 ++++ widget/material/checkbox.go | 2 ++ widget/material/editor.go | 2 ++ widget/material/radiobutton.go | 2 ++ widget/material/switch.go | 5 +++ widget/widget_test.go | 61 ++++++++++++++++++++++++++++++++++ 10 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 widget/widget_test.go diff --git a/widget/bool.go b/widget/bool.go index 1a52ece1..28992c98 100644 --- a/widget/bool.go +++ b/widget/bool.go @@ -3,6 +3,7 @@ package widget import ( + "gioui.org/io/semantic" "gioui.org/layout" ) @@ -37,10 +38,14 @@ func (b *Bool) History() []Press { } func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { - dims := b.clk.Layout(gtx, w) - for b.clk.Clicked() { - 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.DisabledOp(gtx.Queue == nil).Add(gtx.Ops) + return w(gtx) + }) return dims } diff --git a/widget/button.go b/widget/button.go index 0775462e..b8aa30c3 100644 --- a/widget/button.go +++ b/widget/button.go @@ -9,6 +9,7 @@ import ( "gioui.org/f32" "gioui.org/gesture" "gioui.org/io/key" + "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -97,6 +98,7 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension dims := w(gtx) c := m.Stop() defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() + semantic.DisabledOp(gtx.Queue == nil).Add(gtx.Ops) b.click.Add(gtx.Ops) c.Add(gtx.Ops) for len(b.history) > 0 { diff --git a/widget/enum.go b/widget/enum.go index e4869c38..9dba0db8 100644 --- a/widget/enum.go +++ b/widget/enum.go @@ -6,6 +6,7 @@ import ( "image" "gioui.org/gesture" + "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -76,6 +77,8 @@ func (e *Enum) Layout(gtx layout.Context, key string, content layout.Widget) lay } clk.Add(gtx.Ops) } + semantic.SelectedOp(key == e.Value).Add(gtx.Ops) + semantic.DisabledOp(gtx.Queue == nil).Add(gtx.Ops) c.Add(gtx.Ops) return dims diff --git a/widget/label.go b/widget/label.go index 9bd5403f..fbcbfb74 100644 --- a/widget/label.go +++ b/widget/label.go @@ -7,6 +7,7 @@ import ( "image" "unicode/utf8" + "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -184,6 +185,8 @@ func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size un rcl.Pop() t.Pop() } + defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() + semantic.LabelOp(txt).Add(gtx.Ops) return dims } diff --git a/widget/material/button.go b/widget/material/button.go index 4be69479..4be4b21a 100644 --- a/widget/material/button.go +++ b/widget/material/button.go @@ -9,6 +9,7 @@ import ( "gioui.org/f32" "gioui.org/internal/f32color" + "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -89,6 +90,7 @@ func IconButton(th *Theme, button *widget.Clickable, icon *widget.Icon, descript // decoration. func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions { return button.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + semantic.Button.Add(gtx.Ops) return layout.Stack{}.Layout(gtx, layout.Expanded(func(gtx layout.Context) layout.Dimensions { defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop() @@ -118,6 +120,7 @@ func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions { func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { min := gtx.Constraints.Min return b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + semantic.Button.Add(gtx.Ops) return layout.Stack{Alignment: layout.Center}.Layout(gtx, layout.Expanded(func(gtx layout.Context) layout.Dimensions { rr := float32(gtx.Px(b.CornerRadius)) @@ -149,6 +152,10 @@ func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Di func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions { m := op.Record(gtx.Ops) dims := b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + semantic.Button.Add(gtx.Ops) + if d := b.Description; d != "" { + semantic.DescriptionOp(b.Description).Add(gtx.Ops) + } return layout.Stack{Alignment: layout.Center}.Layout(gtx, layout.Expanded(func(gtx layout.Context) layout.Dimensions { sizex, sizey := gtx.Constraints.Min.X, gtx.Constraints.Min.Y diff --git a/widget/material/checkbox.go b/widget/material/checkbox.go index 505966ee..76b7efb0 100644 --- a/widget/material/checkbox.go +++ b/widget/material/checkbox.go @@ -3,6 +3,7 @@ package material import ( + "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/unit" "gioui.org/widget" @@ -32,6 +33,7 @@ func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle { // Layout updates the checkBox and displays it. func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions { return c.CheckBox.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + semantic.CheckBox.Add(gtx.Ops) return c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered()) }) } diff --git a/widget/material/editor.go b/widget/material/editor.go index ff713e20..06df5386 100644 --- a/widget/material/editor.go +++ b/widget/material/editor.go @@ -6,6 +6,7 @@ import ( "image/color" "gioui.org/internal/f32color" + "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/op" "gioui.org/op/paint" @@ -59,6 +60,7 @@ func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions { gtx.Constraints.Min.Y = h } dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, func(gtx layout.Context) layout.Dimensions { + semantic.Editor.Add(gtx.Ops) disabled := gtx.Queue == nil if e.Editor.Len() > 0 { paint.ColorOp{Color: blendDisabledColor(disabled, e.SelectionColor)}.Add(gtx.Ops) diff --git a/widget/material/radiobutton.go b/widget/material/radiobutton.go index c8d2cf47..84d976d1 100644 --- a/widget/material/radiobutton.go +++ b/widget/material/radiobutton.go @@ -3,6 +3,7 @@ package material import ( + "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/unit" "gioui.org/widget" @@ -38,6 +39,7 @@ func RadioButton(th *Theme, group *widget.Enum, key, label string) RadioButtonSt func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions { hovered, hovering := r.Group.Hovered() return r.Group.Layout(gtx, r.Key, func(gtx layout.Context) layout.Dimensions { + semantic.RadioButton.Add(gtx.Ops) return r.layout(gtx, r.Group.Value == r.Key, hovering && hovered == r.Key) }) } diff --git a/widget/material/switch.go b/widget/material/switch.go index dcefaa3e..f68e63a7 100644 --- a/widget/material/switch.go +++ b/widget/material/switch.go @@ -8,6 +8,7 @@ import ( "gioui.org/f32" "gioui.org/internal/f32color" + "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -127,6 +128,10 @@ func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions { sz := image.Pt(clickSize, clickSize) defer clip.Ellipse(f32.Rectangle{Max: layout.FPt(sz)}).Push(gtx.Ops).Pop() s.Switch.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + if d := s.Description; d != "" { + semantic.DescriptionOp(d).Add(gtx.Ops) + } + semantic.Switch.Add(gtx.Ops) return layout.Dimensions{Size: sz} }) diff --git a/widget/widget_test.go b/widget/widget_test.go new file mode 100644 index 00000000..14b548be --- /dev/null +++ b/widget/widget_test.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget_test + +import ( + "image" + "testing" + + "gioui.org/f32" + "gioui.org/io/pointer" + "gioui.org/io/router" + "gioui.org/io/semantic" + "gioui.org/io/system" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/widget" +) + +func TestBool(t *testing.T) { + var ( + ops op.Ops + r router.Router + b widget.Bool + ) + gtx := layout.NewContext(&ops, system.FrameEvent{Queue: &r}) + layout := func() { + b.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + semantic.CheckBox.Add(gtx.Ops) + semantic.DescriptionOp("description").Add(gtx.Ops) + return layout.Dimensions{Size: image.Pt(100, 100)} + }) + } + layout() + r.Frame(gtx.Ops) + r.Queue( + pointer.Event{ + Source: pointer.Touch, + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + pointer.Event{ + Source: pointer.Touch, + Type: pointer.Release, + Position: f32.Pt(50, 50), + }, + ) + ops.Reset() + layout() + r.Frame(gtx.Ops) + tree := r.AppendSemantics(nil) + n := tree[0].Children[0].Desc + if n.Description != "description" { + t.Errorf("unexpected semantic description: %s", n.Description) + } + if n.Class != semantic.CheckBox { + t.Errorf("unexpected semantic class: %v", n.Class) + } + if !b.Value || !n.Selected { + t.Error("click did not select") + } +}