diff options
Diffstat (limited to 'gui/ui/ui.go')
| -rw-r--r-- | gui/ui/ui.go | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/gui/ui/ui.go b/gui/ui/ui.go new file mode 100644 index 0000000..5288828 --- /dev/null +++ b/gui/ui/ui.go @@ -0,0 +1,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) +} |
