/
widget.go
154 lines (131 loc) · 4.06 KB
/
widget.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package widget provides graphical user interface widgets.
//
// TODO: give an overview and some example code.
package widget // import "golang.org/x/exp/shiny/widget"
import (
"image"
"golang.org/x/exp/shiny/gesture"
"golang.org/x/exp/shiny/screen"
"golang.org/x/exp/shiny/unit"
"golang.org/x/exp/shiny/widget/node"
"golang.org/x/exp/shiny/widget/theme"
"golang.org/x/image/math/f64"
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/mouse"
"golang.org/x/mobile/event/paint"
"golang.org/x/mobile/event/size"
)
// Axis is zero, one or both of the horizontal and vertical axes. For example,
// a widget may be scrollable in one of the four AxisXxx values.
type Axis uint8
const (
AxisNone = Axis(0)
AxisHorizontal = Axis(1)
AxisVertical = Axis(2)
AxisBoth = Axis(3) // AxisBoth equals AxisHorizontal | AxisVertical.
)
func (a Axis) Horizontal() bool { return a&AxisHorizontal != 0 }
func (a Axis) Vertical() bool { return a&AxisVertical != 0 }
// WithLayoutData returns the given node after setting its embedded LayoutData
// field.
func WithLayoutData(n node.Node, layoutData interface{}) node.Node {
n.Wrappee().LayoutData = layoutData
return n
}
// RunWindowOptions are optional arguments to RunWindow.
type RunWindowOptions struct {
NewWindowOptions screen.NewWindowOptions
Theme theme.Theme
// TODO: some mechanism to process, filter and inject events. Perhaps a
// screen.EventFilter interface, and note that the zero value in this
// RunWindowOptions implicitly includes the gesture.EventFilter?
}
// TODO: how does RunWindow's caller inject or process events (whether general
// like lifecycle events or app-specific)? How does it stop the event loop when
// the app's work is done?
// TODO: how do widgets signal that they need repaint or relayout?
// TODO: propagate keyboard / mouse / touch events.
// RunWindow creates a new window for s, with the given widget tree, and runs
// its event loop.
//
// A nil opts is valid and means to use the default option values.
func RunWindow(s screen.Screen, root node.Node, opts *RunWindowOptions) error {
var (
nwo *screen.NewWindowOptions
t *theme.Theme
)
if opts != nil {
nwo = &opts.NewWindowOptions
t = &opts.Theme
}
w, err := s.NewWindow(nwo)
if err != nil {
return err
}
defer w.Release()
// paintPending batches up multiple NeedsPaint observations so that we
// paint only once (which can be relatively expensive) even when there are
// multiple input events in the queue, such as from a rapidly moving mouse
// or from the user typing many keys.
//
// TODO: determine somehow if there's an external paint event in the queue,
// not just internal paint events?
//
// TODO: if every package that uses package screen should basically
// throttle like this, should it be provided at a lower level?
paintPending := false
gef := gesture.EventFilter{EventDeque: w}
for {
e := w.NextEvent()
if e = gef.Filter(e); e == nil {
continue
}
switch e := e.(type) {
case lifecycle.Event:
root.OnLifecycleEvent(e)
if e.To == lifecycle.StageDead {
return nil
}
case gesture.Event, mouse.Event:
root.OnInputEvent(e, image.Point{})
case paint.Event:
ctx := &node.PaintContext{
Theme: t,
Screen: s,
Drawer: w,
Src2Dst: f64.Aff3{
1, 0, 0,
0, 1, 0,
},
}
if err := root.Paint(ctx, image.Point{}); err != nil {
return err
}
w.Publish()
paintPending = false
case size.Event:
if dpi := float64(e.PixelsPerPt) * unit.PointsPerInch; dpi != t.GetDPI() {
newT := new(theme.Theme)
if t != nil {
*newT = *t
}
newT.DPI = dpi
t = newT
}
size := e.Size()
root.Measure(t, size.X, size.Y)
root.Wrappee().Rect = e.Bounds()
root.Layout(t)
// TODO: call Mark(node.MarkNeedsPaint)?
case error:
return e
}
if !paintPending && root.Wrappee().Marks.NeedsPaint() {
paintPending = true
w.Send(paint.Event{})
}
}
}