// 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) }