io/semantic: add package for adding semantic descriptions to UI components

Software such as screen readers require semantic descriptions of user
interfaces to effectively present and interact with them. Package
semantic, combined with the existing package clip provide the operations
for Gio programs to describe themselves.

This change implements the semantic package and the routing changes for
accessing semantic trees; follow-ups add semantic information to widgets
and implement mapping semantic tree to platform representations.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-12-01 13:08:51 +01:00
parent 48a96305c8
commit 8a90074d04
5 changed files with 586 additions and 57 deletions
+20 -2
View File
@@ -24,7 +24,7 @@ type Ops struct {
nextStateID int nextStateID int
macroStack stack macroStack stack
stacks [4]stack stacks [5]stack
} }
type OpType byte type OpType byte
@@ -63,6 +63,11 @@ const (
TypeCursor TypeCursor
TypePath TypePath
TypeStroke TypeStroke
TypeSemanticLabel
TypeSemanticDesc
TypeSemanticClass
TypeSemanticSelected
TypeSemanticDisabled
) )
type StackID struct { type StackID struct {
@@ -98,6 +103,7 @@ const (
ClipStack StackKind = iota ClipStack StackKind = iota
TransStack TransStack
PassStack PassStack
MetaStack
) )
const ( const (
@@ -135,6 +141,11 @@ const (
TypeCursorLen = 1 + 1 TypeCursorLen = 1 + 1
TypePathLen = 8 + 1 TypePathLen = 8 + 1
TypeStrokeLen = 1 + 4 TypeStrokeLen = 1 + 4
TypeSemanticLabelLen = 1
TypeSemanticDescLen = 1
TypeSemanticClassLen = 2
TypeSemanticSelectedLen = 2
TypeSemanticDisabledLen = 2
) )
func (op *ClipOp) Decode(data []byte) { func (op *ClipOp) Decode(data []byte) {
@@ -354,12 +365,17 @@ func (t OpType) Size() int {
TypeCursorLen, TypeCursorLen,
TypePathLen, TypePathLen,
TypeStrokeLen, TypeStrokeLen,
TypeSemanticLabelLen,
TypeSemanticDescLen,
TypeSemanticClassLen,
TypeSemanticSelectedLen,
TypeSemanticDisabledLen,
}[t-firstOpIndex] }[t-firstOpIndex]
} }
func (t OpType) NumRefs() int { func (t OpType) NumRefs() int {
switch t { switch t {
case TypeKeyInput, TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeCursor: case TypeKeyInput, TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeCursor, TypeSemanticLabel, TypeSemanticDesc:
return 1 return 1
case TypeImage: case TypeImage:
return 2 return 2
@@ -426,6 +442,8 @@ func (t OpType) String() string {
return "Path" return "Path"
case TypeStroke: case TypeStroke:
return "Stroke" return "Stroke"
case TypeSemanticLabel:
return "SemanticDescription"
default: default:
panic("unknown OpType") panic("unknown OpType")
} }
+241 -22
View File
@@ -9,6 +9,7 @@ import (
"gioui.org/internal/ops" "gioui.org/internal/ops"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic"
) )
type pointerQueue struct { type pointerQueue struct {
@@ -20,6 +21,15 @@ type pointerQueue struct {
pointers []pointerInfo pointers []pointerInfo
scratch []event.Tag scratch []event.Tag
semantic struct {
idsAssigned bool
lastID SemanticID
// contentIDs maps semantic content to a list of semantic IDs
// previously assigned. It is used to maintain stable IDs across
// frames.
contentIDs map[semanticContent][]semanticID
}
} }
type hitNode struct { type hitNode struct {
@@ -64,8 +74,19 @@ type areaOp struct {
type areaNode struct { type areaNode struct {
trans f32.Affine2D trans f32.Affine2D
next int
area areaOp area areaOp
// Tree indices, with -1 being the sentinel.
parent int
firstChild int
lastChild int
sibling int
semantic struct {
valid bool
id SemanticID
content semanticContent
}
} }
type areaKind uint8 type areaKind uint8
@@ -87,6 +108,21 @@ type pointerCollector struct {
nodeStack []int nodeStack []int
} }
type semanticContent struct {
tag event.Tag
label string
desc string
class semantic.ClassOp
gestures SemanticGestures
selected bool
disabled bool
}
type semanticID struct {
id SemanticID
used bool
}
const ( const (
areaRect areaKind = iota areaRect areaKind = iota
areaEllipse areaEllipse
@@ -101,24 +137,42 @@ func (c *pointerCollector) setTrans(t f32.Affine2D) {
} }
func (c *pointerCollector) clip(op ops.ClipOp) { func (c *pointerCollector) clip(op ops.ClipOp) {
area := -1
if i := c.state.nodePlusOne - 1; i != -1 {
n := c.q.hitTree[i]
area = n.area
}
kind := areaRect kind := areaRect
if op.Shape == ops.Ellipse { if op.Shape == ops.Ellipse {
kind = areaEllipse kind = areaEllipse
} }
areaOp := areaOp{kind: kind, rect: frect(op.Bounds)} c.pushArea(kind, frect(op.Bounds))
c.q.areas = append(c.q.areas, areaNode{trans: c.state.t, next: area, area: areaOp}) }
func (c *pointerCollector) pushArea(kind areaKind, bounds f32.Rectangle) {
parentID := c.currentArea()
areaID := len(c.q.areas)
areaOp := areaOp{kind: kind, rect: bounds}
if parentID != -1 {
parent := &c.q.areas[parentID]
if parent.firstChild == -1 {
parent.firstChild = areaID
}
if siblingID := parent.lastChild; siblingID != -1 {
c.q.areas[siblingID].sibling = areaID
}
parent.lastChild = areaID
}
an := areaNode{
trans: c.state.t,
area: areaOp,
parent: parentID,
sibling: -1,
firstChild: -1,
lastChild: -1,
}
c.q.areas = append(c.q.areas, an)
c.nodeStack = append(c.nodeStack, c.state.nodePlusOne-1) c.nodeStack = append(c.nodeStack, c.state.nodePlusOne-1)
c.q.hitTree = append(c.q.hitTree, hitNode{ c.addHitNode(hitNode{
next: c.state.nodePlusOne - 1, area: areaID,
area: len(c.q.areas) - 1,
pass: true, pass: true,
}) })
c.state.nodePlusOne = len(c.q.hitTree) - 1 + 1
} }
// frect converts a rectangle to a f32.Rectangle. // frect converts a rectangle to a f32.Rectangle.
@@ -149,19 +203,33 @@ func (c *pointerCollector) popPass() {
c.state.pass-- c.state.pass--
} }
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) { func (c *pointerCollector) currentArea() int {
area := -1
if i := c.state.nodePlusOne - 1; i != -1 { if i := c.state.nodePlusOne - 1; i != -1 {
n := c.q.hitTree[i] n := c.q.hitTree[i]
area = n.area return n.area
} }
c.q.hitTree = append(c.q.hitTree, hitNode{ return -1
next: c.state.nodePlusOne - 1, }
area: area,
func (c *pointerCollector) addHitNode(n hitNode) {
n.next = c.state.nodePlusOne - 1
c.q.hitTree = append(c.q.hitTree, n)
c.state.nodePlusOne = len(c.q.hitTree) - 1 + 1
}
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.content.tag = op.Tag
if op.Types&(pointer.Press|pointer.Release) != 0 {
area.semantic.content.gestures |= ClickGesture
}
area.semantic.valid = area.semantic.content.gestures != 0
c.addHitNode(hitNode{
area: areaID,
tag: op.Tag, tag: op.Tag,
pass: c.state.pass > 0, pass: c.state.pass > 0,
}) })
c.state.nodePlusOne = len(c.q.hitTree) - 1 + 1
h, ok := c.q.handlers[op.Tag] h, ok := c.q.handlers[op.Tag]
if !ok { if !ok {
h = new(pointerHandler) h = new(pointerHandler)
@@ -171,12 +239,47 @@ func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
events.AddNoRedraw(op.Tag, pointer.Event{Type: pointer.Cancel}) events.AddNoRedraw(op.Tag, pointer.Event{Type: pointer.Cancel})
} }
h.active = true h.active = true
h.area = area h.area = areaID
h.wantsGrab = h.wantsGrab || op.Grab h.wantsGrab = h.wantsGrab || op.Grab
h.types = h.types | op.Types h.types = h.types | op.Types
h.scrollRange = op.ScrollBounds h.scrollRange = op.ScrollBounds
} }
func (c *pointerCollector) semanticLabel(lbl string) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.valid = true
area.semantic.content.label = lbl
}
func (c *pointerCollector) semanticDesc(desc string) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.valid = true
area.semantic.content.desc = desc
}
func (c *pointerCollector) semanticClass(class semantic.ClassOp) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.valid = true
area.semantic.content.class = class
}
func (c *pointerCollector) semanticSelected(selected bool) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.valid = true
area.semantic.content.selected = selected
}
func (c *pointerCollector) semanticDisabled(disabled bool) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.valid = true
area.semantic.content.disabled = disabled
}
func (c *pointerCollector) cursor(name pointer.CursorName) { func (c *pointerCollector) cursor(name pointer.CursorName) {
c.q.cursors = append(c.q.cursors, cursorNode{ c.q.cursors = append(c.q.cursors, cursorNode{
name: name, name: name,
@@ -186,9 +289,110 @@ func (c *pointerCollector) cursor(name pointer.CursorName) {
func (c *pointerCollector) reset(q *pointerQueue) { func (c *pointerCollector) reset(q *pointerQueue) {
q.reset() q.reset()
c.state = collectState{} c.resetState()
c.nodeStack = c.nodeStack[:0] c.nodeStack = c.nodeStack[:0]
c.q = q c.q = q
// Add implicit root area for semantic descriptions to hang onto.
c.pushArea(areaRect, f32.Rect(-1e6, -1e6, 1e6, 1e6))
// Make it semantic to ensure a single semantic root.
c.q.areas[0].semantic.valid = true
}
func (q *pointerQueue) assignSemIDs() {
if q.semantic.idsAssigned {
return
}
q.semantic.idsAssigned = true
for i, a := range q.areas {
if a.semantic.valid {
q.areas[i].semantic.id = q.semanticIDFor(a.semantic.content)
}
}
}
func (q *pointerQueue) AppendSemantics(nodes []SemanticNode) []SemanticNode {
q.assignSemIDs()
nodes = q.appendSemanticChildren(nodes, 0)
nodes = q.appendSemanticArea(nodes, 0, 0)
return nodes
}
func (q *pointerQueue) appendSemanticArea(nodes []SemanticNode, parentID SemanticID, nodeIdx int) []SemanticNode {
areaIdx := nodes[nodeIdx].areaIdx
a := q.areas[areaIdx]
childStart := len(nodes)
nodes = q.appendSemanticChildren(nodes, a.firstChild)
childEnd := len(nodes)
for i := childStart; i < childEnd; i++ {
nodes = q.appendSemanticArea(nodes, a.semantic.id, i)
}
n := &nodes[nodeIdx]
n.ParentID = parentID
n.Children = nodes[childStart:childEnd]
return nodes
}
func (q *pointerQueue) appendSemanticChildren(nodes []SemanticNode, areaIdx int) []SemanticNode {
if areaIdx == -1 {
return nodes
}
a := q.areas[areaIdx]
if semID := a.semantic.id; semID != 0 {
cnt := a.semantic.content
nodes = append(nodes, SemanticNode{
ID: semID,
Desc: SemanticDesc{
Bounds: f32.Rectangle{
Min: a.trans.Transform(a.area.rect.Min),
Max: a.trans.Transform(a.area.rect.Max),
},
Label: cnt.label,
Description: cnt.desc,
Class: cnt.class,
Gestures: cnt.gestures,
Selected: cnt.selected,
Disabled: cnt.disabled,
},
areaIdx: areaIdx,
})
} else {
nodes = q.appendSemanticChildren(nodes, a.firstChild)
}
return q.appendSemanticChildren(nodes, a.sibling)
}
func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
ids := q.semantic.contentIDs[content]
for i, id := range ids {
if !id.used {
ids[i].used = true
return id.id
}
}
// No prior assigned ID; allocate a new one.
q.semantic.lastID++
id := semanticID{id: q.semantic.lastID, used: true}
if q.semantic.contentIDs == nil {
q.semantic.contentIDs = make(map[semanticContent][]semanticID)
}
q.semantic.contentIDs[content] = append(q.semantic.contentIDs[content], id)
return id.id
}
func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) {
q.assignSemIDs()
for i := len(q.hitTree) - 1; i >= 0; i-- {
n := &q.hitTree[i]
hit := q.hit(n.area, pos)
if !hit {
continue
}
area := q.areas[n.area]
if area.semantic.id != 0 {
return area.semantic.id, true
}
}
return 0, false
} }
func (q *pointerQueue) opHit(handlers *[]event.Tag, pos f32.Point) { func (q *pointerQueue) opHit(handlers *[]event.Tag, pos f32.Point) {
@@ -230,7 +434,7 @@ func (q *pointerQueue) hit(areaIdx int, p f32.Point) bool {
if !a.area.Hit(p) { if !a.area.Hit(p) {
return false return false
} }
areaIdx = a.next areaIdx = a.parent
} }
return true return true
} }
@@ -248,6 +452,21 @@ func (q *pointerQueue) reset() {
q.hitTree = q.hitTree[:0] q.hitTree = q.hitTree[:0]
q.areas = q.areas[:0] q.areas = q.areas[:0]
q.cursors = q.cursors[:0] q.cursors = q.cursors[:0]
q.semantic.idsAssigned = false
for k, ids := range q.semantic.contentIDs {
for i := len(ids) - 1; i >= 0; i-- {
if !ids[i].used {
ids = append(ids[:i], ids[i+1:]...)
} else {
ids[i].used = false
}
}
if len(ids) > 0 {
q.semantic.contentIDs[k] = ids
} else {
delete(q.semantic.contentIDs, k)
}
}
} }
func (q *pointerQueue) Frame(events *handlerEvents) { func (q *pointerQueue) Frame(events *handlerEvents) {
+84 -5
View File
@@ -13,6 +13,7 @@ package router
import ( import (
"encoding/binary" "encoding/binary"
"image" "image"
"strings"
"time" "time"
"gioui.org/f32" "gioui.org/f32"
@@ -22,6 +23,7 @@ import (
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/profile" "gioui.org/io/profile"
"gioui.org/io/semantic"
"gioui.org/op" "gioui.org/op"
) )
@@ -53,6 +55,40 @@ type Router struct {
profile profile.Event profile profile.Event
} }
// SemanticNode represents a node in the tree describing the components
// contained in a frame.
type SemanticNode struct {
ID SemanticID
ParentID SemanticID
Children []SemanticNode
Desc SemanticDesc
areaIdx int
}
// SemanticDesc provides a semantic description of a UI component.
type SemanticDesc struct {
Class semantic.ClassOp
Description string
Label string
Selected bool
Disabled bool
Gestures SemanticGestures
Bounds f32.Rectangle
}
// SemanticGestures is a bit-set of supported gestures.
type SemanticGestures int
const (
ClickGesture SemanticGestures = 1 << iota
)
// SemanticID uniquely identifies a SemanticDescription.
//
// By convention, the zero value denotes the non-existent ID.
type SemanticID uint64
type handlerEvents struct { type handlerEvents struct {
handlers map[event.Tag][]event.Event handlers map[event.Tag][]event.Event
hadEvents bool hadEvents bool
@@ -137,6 +173,17 @@ func (q *Router) Cursor() pointer.CursorName {
return q.pointer.queue.cursor return q.pointer.queue.cursor
} }
// SemanticAt returns the first semantic description under pos, if any.
func (q *Router) SemanticAt(pos f32.Point) (SemanticID, bool) {
return q.pointer.queue.SemanticAt(pos)
}
// AppendSemantics appends the semantic tree to nodes, and returns the result.
// The root node is the first added.
func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
return q.pointer.queue.AppendSemantics(nodes)
}
func (q *Router) collect() { func (q *Router) collect() {
q.transStack = q.transStack[:0] q.transStack = q.transStack[:0]
pc := &q.pointer.collector pc := &q.pointer.collector
@@ -175,17 +222,12 @@ func (q *Router) collect() {
pc.resetState() pc.resetState()
pc.setTrans(t) pc.setTrans(t)
// Pointer ops.
case ops.TypeClip: case ops.TypeClip:
var op ops.ClipOp var op ops.ClipOp
op.Decode(encOp.Data) op.Decode(encOp.Data)
pc.clip(op) pc.clip(op)
case ops.TypePopClip: case ops.TypePopClip:
pc.popArea() pc.popArea()
case ops.TypePass:
pc.pass()
case ops.TypePopPass:
pc.popPass()
case ops.TypeTransform: case ops.TypeTransform:
t2, push := ops.DecodeTransform(encOp.Data) t2, push := ops.DecodeTransform(encOp.Data)
if push { if push {
@@ -198,6 +240,12 @@ func (q *Router) collect() {
t = q.transStack[n-1] t = q.transStack[n-1]
q.transStack = q.transStack[:n-1] q.transStack = q.transStack[:n-1]
pc.setTrans(t) pc.setTrans(t)
// Pointer ops.
case ops.TypePass:
pc.pass()
case ops.TypePopPass:
pc.popPass()
case ops.TypePointerInput: case ops.TypePointerInput:
bo := binary.LittleEndian bo := binary.LittleEndian
op := pointer.InputOp{ op := pointer.InputOp{
@@ -238,6 +286,29 @@ func (q *Router) collect() {
Hint: key.InputHint(encOp.Data[1]), Hint: key.InputHint(encOp.Data[1]),
} }
kc.inputOp(op) kc.inputOp(op)
// Semantic ops.
case ops.TypeSemanticLabel:
lbl := encOp.Refs[0].(*string)
pc.semanticLabel(*lbl)
case ops.TypeSemanticDesc:
desc := encOp.Refs[0].(*string)
pc.semanticDesc(*desc)
case ops.TypeSemanticClass:
class := semantic.ClassOp(encOp.Data[1])
pc.semanticClass(class)
case ops.TypeSemanticSelected:
if encOp.Data[1] != 0 {
pc.semanticSelected(true)
} else {
pc.semanticSelected(false)
}
case ops.TypeSemanticDisabled:
if encOp.Data[1] != 0 {
pc.semanticDisabled(true)
} else {
pc.semanticDisabled(false)
}
} }
} }
} }
@@ -320,3 +391,11 @@ func decodeInvalidateOp(d []byte) op.InvalidateOp {
} }
return o return o
} }
func (s SemanticGestures) String() string {
var gestures []string
if s&ClickGesture != 0 {
gestures = append(gestures, "Click")
}
return strings.Join(gestures, ",")
}
+120
View File
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: Unlicense OR MIT
package router
import (
"fmt"
"image"
"reflect"
"testing"
"gioui.org/f32"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/op"
"gioui.org/op/clip"
)
func TestSemanticTree(t *testing.T) {
var (
ops op.Ops
r Router
)
t1 := clip.Rect(image.Rect(0, 0, 75, 75)).Push(&ops)
semantic.DescriptionOp("child1").Add(&ops)
t1.Pop()
t2 := clip.Rect(image.Rect(25, 25, 100, 100)).Push(&ops)
semantic.DescriptionOp("child2").Add(&ops)
t2.Pop()
r.Frame(&ops)
tests := []struct {
x, y float32
desc string
}{
{24, 24, "child1"},
{50, 50, "child2"},
{100, 100, ""},
}
tree := r.AppendSemantics(nil)
verifyTree(t, 0, tree[0])
for _, test := range tests {
p := f32.Pt(test.x, test.y)
id, found := r.SemanticAt(p)
if !found {
t.Errorf("no semantic node at %v", p)
}
n, found := lookupNode(tree, id)
if !found {
t.Errorf("no id %d in semantic tree", id)
}
if got := n.Desc.Description; got != test.desc {
t.Errorf("got semantic description %s at %v, expected %s", got, p, test.desc)
}
}
// Verify stable IDs.
r.Frame(&ops)
tree2 := r.AppendSemantics(nil)
if !reflect.DeepEqual(tree, tree2) {
fmt.Println("First tree:")
printTree(0, tree[0])
fmt.Println("Second tree:")
printTree(0, tree2[0])
t.Error("same semantic description lead to differing trees")
}
}
func TestSemanticDescription(t *testing.T) {
var ops op.Ops
pointer.InputOp{Tag: new(int), Types: pointer.Press | pointer.Release}.Add(&ops)
semantic.DescriptionOp("description").Add(&ops)
semantic.LabelOp("label").Add(&ops)
semantic.Button.Add(&ops)
semantic.DisabledOp(true).Add(&ops)
semantic.SelectedOp(true).Add(&ops)
var r Router
r.Frame(&ops)
tree := r.AppendSemantics(nil)
got := tree[0].Desc
exp := SemanticDesc{
Class: 1,
Description: "description",
Label: "label",
Selected: true,
Disabled: true,
Gestures: ClickGesture,
Bounds: f32.Rectangle{Min: f32.Point{X: -1e+06, Y: -1e+06}, Max: f32.Point{X: 1e+06, Y: 1e+06}},
}
if got != exp {
t.Errorf("semantic description mismatch:\nGot: %+v\nWant: %+v", got, exp)
}
}
func lookupNode(tree []SemanticNode, id SemanticID) (SemanticNode, bool) {
for _, n := range tree {
if id == n.ID {
return n, true
}
}
return SemanticNode{}, false
}
func verifyTree(t *testing.T, parent SemanticID, n SemanticNode) {
t.Helper()
if n.ParentID != parent {
t.Errorf("node %d: got parent %d, want %d", n.ID, n.ParentID, parent)
}
for _, c := range n.Children {
verifyTree(t, n.ID, c)
}
}
func printTree(indent int, n SemanticNode) {
for i := 0; i < indent; i++ {
fmt.Print("\t")
}
fmt.Printf("%d: %+v\n", n.ID, n.Desc)
for _, c := range n.Children {
printTree(indent+1, c)
}
}
+93
View File
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package semantic provides operations for semantic descriptions of a user
// interface, to facilitate presentation and interaction in external software
// such as screen readers.
//
// Semantic descriptions are organized in a tree, with clip operations as
// nodes. Operations in this package are associated with the current semantic
// node, that is the most recent pushed clip operation.
package semantic
import (
"gioui.org/internal/ops"
"gioui.org/op"
)
// LabelOp provides the content of a textual component.
type LabelOp string
// DescriptionOp describes a component.
type DescriptionOp string
// ClassOp provides the component class.
type ClassOp int
const (
Unknown ClassOp = iota
Button
CheckBox
Editor
RadioButton
Switch
)
// SelectedOp describes the selected state for components that have
// boolean state.
type SelectedOp bool
// DisabledOp describes the disabled state.
type DisabledOp bool
func (l LabelOp) Add(o *op.Ops) {
s := string(l)
data := ops.Write1(&o.Internal, ops.TypeSemanticLabelLen, &s)
data[0] = byte(ops.TypeSemanticLabel)
}
func (d DescriptionOp) Add(o *op.Ops) {
s := string(d)
data := ops.Write1(&o.Internal, ops.TypeSemanticDescLen, &s)
data[0] = byte(ops.TypeSemanticDesc)
}
func (c ClassOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeSemanticClassLen)
data[0] = byte(ops.TypeSemanticClass)
data[1] = byte(c)
}
func (s SelectedOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeSemanticSelectedLen)
data[0] = byte(ops.TypeSemanticSelected)
if s {
data[1] = 1
}
}
func (d DisabledOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeSemanticDisabledLen)
data[0] = byte(ops.TypeSemanticDisabled)
if d {
data[1] = 1
}
}
func (c ClassOp) String() string {
switch c {
case Unknown:
return "Unknown"
case Button:
return "Button"
case CheckBox:
return "CheckBox"
case Editor:
return "Editor"
case RadioButton:
return "RadioButton"
case Switch:
return "Switch"
default:
panic("invalid ClassOp")
}
}