summaryrefslogtreecommitdiff
path: root/gui/ui/ui.go
diff options
context:
space:
mode:
Diffstat (limited to 'gui/ui/ui.go')
-rw-r--r--gui/ui/ui.go207
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)
+}