diff options
| -rw-r--r-- | gui/draw/snarf.go | 10 | ||||
| -rw-r--r-- | gui/ui/field.go | 88 | ||||
| -rw-r--r-- | gui/ui/menu.go | 79 | ||||
| -rw-r--r-- | gui/ui/ui.go | 207 |
4 files changed, 384 insertions, 0 deletions
diff --git a/gui/draw/snarf.go b/gui/draw/snarf.go index ae26edd..3f0e35b 100644 --- a/gui/draw/snarf.go +++ b/gui/draw/snarf.go @@ -13,3 +13,13 @@ func Snarf(dev, text string) error { _, err = f.WriteString(text) return err } + +// Snarfed reads the system snarf buffer, which is what every other +// program on the system means by paste. +func Snarfed(dev string) (string, error) { + b, err := os.ReadFile(dev + "/snarf") + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/gui/ui/field.go b/gui/ui/field.go new file mode 100644 index 0000000..6872d32 --- /dev/null +++ b/gui/ui/field.go @@ -0,0 +1,88 @@ +package ui + +import "9front/gui/draw" + +// A Field is one line of editable text. +// +// Deliberately small: a cursor at the end, backspace, and ^U to clear. +// There is no selection, no mouse cursor placement and no history, +// because a form that asks for a summary and a time does not need them +// and every one of them is a place to get subtly wrong. A program that +// needs a real editor should hand the job to one. +type Field struct { + Label string + Value string + R draw.Rectangle // set by Draw, used for hit testing +} + +// Draw paints the field and records where it landed in f.R, so the +// caller can make it clickable. It does not register a hit region +// itself: the first matching region wins a click, and a do-nothing one +// here would silently swallow the caller's. +func (u *UI) Draw(f *Field, r draw.Rectangle, focused bool) { + f.R = r + lw := u.F.Width("attendees ") + 8 + u.Text(draw.Point{X: r.Min.X, Y: r.Min.Y + 2}, "ink", f.Label) + + box := draw.Rect(r.Min.X+lw, r.Min.Y, r.Max.X, r.Max.Y) + u.Fill(box, "bg") + c := "rule" + if focused { + c = "border" + } + u.Border(box, c) + + s := f.Value + if focused { + s += "|" + } + // keep the end of the line in view: that is where you are typing + for len(s) > 0 && u.F.Width(s) > box.Dx()-8 { + s = s[1:] + } + u.Text(draw.Point{X: box.Min.X + 4, Y: box.Min.Y + 2}, "ink", s) +} + +// oneLine keeps a paste to the first line: these are one-line fields, +// and a pasted paragraph would silently lose everything after the first +// newline anyway. +func oneLine(s string) string { + for i, r := range s { + if r == '\n' || r == '\r' { + return s[:i] + } + } + return s +} + +// Key applies one typed rune and says whether it changed anything. +func (f *Field) Key(r rune) bool { + switch r { + case '\b', 0x7F: // backspace, del + if f.Value == "" { + return false + } + v := []rune(f.Value) + f.Value = string(v[:len(v)-1]) + return true + case 0x15: // ^U + if f.Value == "" { + return false + } + f.Value = "" + return true + case 0x16, 0x19: // ^V, ^Y: paste + if v, err := draw.Snarfed("/dev"); err == nil { + f.Value += oneLine(v) + return true + } + return false + case '\n', '\r', '\t', 0x1B: + return false // the form deals with these + } + if r < ' ' { + return false + } + f.Value += string(r) + return true +} diff --git a/gui/ui/menu.go b/gui/ui/menu.go new file mode 100644 index 0000000..7fe7ff8 --- /dev/null +++ b/gui/ui/menu.go @@ -0,0 +1,79 @@ +package ui + +import "9front/gui/draw" + +// An Item is one line of a menu. +type Item struct { + Label string + Do func() +} + +// Menu pops up at p and tracks until button 3 comes back up, the way +// page(1) and vdir(1) do it: press 3, drag, release on an item. +// +// It keeps itself on the screen, which is the detail everyone forgets +// until the menu opens near the bottom edge and half of it is missing. +// redraw is called if the menu is dismissed without a choice. +func (u *UI) Menu(items []Item, at draw.Point, mc *draw.Mousectl, redraw func()) { + if len(items) == 0 { + return + } + + lh := int32(u.F.Height) + 4 + var w int32 + for _, it := range items { + if x := u.F.Width(it.Label) + 20; x > w { + w = x + } + } + h := lh*int32(len(items)) + 4 + win := u.Win.Rect() + r := draw.Rect(at.X, at.Y, at.X+w, at.Y+h) + if r.Max.X > win.Max.X { + r = r.Add(draw.Point{X: win.Max.X - r.Max.X}) + } + if r.Max.Y > win.Max.Y { + r = r.Add(draw.Point{Y: win.Max.Y - r.Max.Y}) + } + + sel := -1 + paint := func() { + u.Fill(r, "tag") + u.Border(r, "border") + for i, it := range items { + ir := draw.Rect(r.Min.X+1, r.Min.Y+2+int32(i)*lh, + r.Max.X-1, r.Min.Y+2+int32(i+1)*lh) + if i == sel { + u.Fill(ir, "today") + } + u.Text(draw.Point{X: ir.Min.X + 8, Y: ir.Min.Y + 2}, "ink", it.Label) + } + u.D.Flush() + } + itemAt := func(p draw.Point) int { + if p.X < r.Min.X || p.X >= r.Max.X { + return -1 + } + i := int((p.Y - r.Min.Y - 2) / lh) + if i < 0 || i >= len(items) || p.Y < r.Min.Y+2 { + return -1 + } + return i + } + paint() + + for m := range mc.C { + if n := itemAt(m.Point); n != sel { + sel = n + paint() + } + if m.Buttons&4 == 0 { + if sel >= 0 { + items[sel].Do() + } else { + redraw() + } + return + } + } +} 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) +} |
