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