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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
|
// Package ui is the bits every program drawing in a rio window needs
// and none of them should write twice: borders, buttons, a button-3
// menu, hit regions, dragging the window about, selecting text.
//
// It is a kit, not a framework. Every function draws a thing and, where
// it makes sense, records where it was drawn so a click can find it.
// The program keeps its own event loop and its own idea of what to
// redraw; nothing here calls back into it except the handler you give.
package ui
import (
"os"
"fmt"
"9front/gui/draw"
)
// UI is what the drawing functions need: somewhere to draw, something
// to draw with, and a palette. A program embeds it.
type UI struct {
D *draw.Display
Win *draw.Image
F *draw.Font
Col map[string]*draw.Image
hits []Hit
}
// A Hit is a region that does something when clicked.
type Hit struct {
R draw.Rectangle
Do func()
}
// Reset forgets every hit region. Call it at the top of a redraw, or
// clicks will find things that are no longer on the screen.
func (u *UI) Reset() { u.hits = u.hits[:0] }
// On records a region. The first one that contains a point wins, so
// record the small things before the big ones that enclose them.
func (u *UI) On(r draw.Rectangle, do func()) {
u.hits = append(u.hits, Hit{r, do})
}
// Click runs the handler for the first region containing p, and says
// whether it found one.
func (u *UI) Click(p draw.Point) bool {
for _, h := range u.hits {
if In(h.R, p) {
h.Do()
return true
}
}
return false
}
// In reports whether p is inside r.
func In(r draw.Rectangle, p draw.Point) bool {
return p.X >= r.Min.X && p.X < r.Max.X && p.Y >= r.Min.Y && p.Y < r.Max.Y
}
// ---------------------------------------------------------------- paint
// Border width rio leaves around a window. rio draws the border into
// the client's own image and colours it to show which window has the
// input, so a program that fills its whole rectangle erases it and the
// window loses its edge. Draw inside Body and those pixels stay rio's.
const Selborder = 4
// Body is the part of the window a program should draw in: everything
// but rio's border.
func (u *UI) Body() draw.Rectangle {
r := u.Win.Rect()
return draw.Rect(r.Min.X+Selborder, r.Min.Y+Selborder,
r.Max.X-Selborder, r.Max.Y-Selborder)
}
// Fill paints a rectangle in a named colour.
func (u *UI) Fill(r draw.Rectangle, c string) {
if img, ok := u.Col[c]; ok {
draw.Draw(u.Win, r, img, nil, draw.ZP)
}
}
// Text draws a string at p in a named colour.
func (u *UI) Text(p draw.Point, c string, s string) {
if img, ok := u.Col[c]; ok {
u.F.String(u.Win, p, img, s)
}
}
// Border outlines a rectangle, one pixel wide. Everything that draws a
// box wants this, and everything used to write it out four times.
func (u *UI) Border(r draw.Rectangle, c string) {
u.Fill(draw.Rect(r.Min.X, r.Min.Y, r.Max.X, r.Min.Y+1), c)
u.Fill(draw.Rect(r.Min.X, r.Max.Y-1, r.Max.X, r.Max.Y), c)
u.Fill(draw.Rect(r.Min.X, r.Min.Y, r.Min.X+1, r.Max.Y), c)
u.Fill(draw.Rect(r.Max.X-1, r.Min.Y, r.Max.X, r.Max.Y), c)
}
// Button draws a labelled box and records it. on picks the lit
// background, for a button that shows the current state.
func (u *UI) Button(r draw.Rectangle, label string, on bool, do func()) {
bg := "tag"
if on {
bg = "today"
}
u.Fill(r, bg)
u.Border(r, "border")
w := u.F.Width(label)
u.Text(draw.Point{X: r.Min.X + (r.Dx()-w)/2, Y: r.Min.Y + 3}, "ink", label)
u.On(r, do)
}
// Fit truncates s to w pixels, so a long summary cannot run out of its
// box and over whatever is next to it.
func (u *UI) Fit(s string, w int32) string {
if u.F.Width(s) <= w {
return s
}
r := []rune(s)
for len(r) > 1 {
r = r[:len(r)-1]
if u.F.Width(string(r)+"...") <= w {
return string(r) + "..."
}
}
return ""
}
// Wrap breaks text into lines no wider than w.
func (u *UI) Wrap(text string, w int32) []string {
var out []string
line := ""
for _, word := range fields(text) {
try := word
if line != "" {
try = line + " " + word
}
if u.F.Width(try) <= w || line == "" {
line = try
continue
}
out = append(out, line)
line = word
}
if line != "" {
out = append(out, line)
}
return out
}
func fields(s string) []string {
var out []string
cur := ""
for _, r := range s {
if r == ' ' || r == '\t' || r == '\n' {
if cur != "" {
out = append(out, cur)
cur = ""
}
continue
}
cur += string(r)
}
if cur != "" {
out = append(out, cur)
}
return out
}
// ---------------------------------------------------------------- rio
// Wctl asks rio to do something to our window: resize, move, hide.
// Outside rio there is no /dev/wctl and this quietly does nothing,
// which is the right answer for a menu item that cannot apply.
func (u *UI) Wctl(cmd string) {
f, err := os.OpenFile("/dev/wctl", os.O_WRONLY, 0)
if err != nil {
return
}
defer f.Close()
fmt.Fprint(f, cmd)
}
// Resize asks rio for a window of a given size, keeping the corner.
func (u *UI) Resize(dx, dy int32) {
r := u.Win.Rect()
u.Wctl(fmt.Sprintf("resize -r %d %d %d %d",
r.Min.X, r.Min.Y, r.Min.X+dx, r.Min.Y+dy))
}
// Hide puts the window away.
func (u *UI) Hide() { u.Wctl("hide") }
// New starts a command in a window of its own.
//
// rio splits this line on spaces and there is no quoting, so every
// argument has to be a single token.
func (u *UI) New(dx, dy int32, argv ...string) {
cmd := fmt.Sprintf("new -dx %d -dy %d", dx, dy)
for _, a := range argv {
cmd += " " + a
}
u.Wctl(cmd)
}
|