diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-08-22 19:12:01 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-08-22 19:12:01 -0400 |
| commit | 77c7bc9b187450818ec068239a3ae836d01fdebc (patch) | |
| tree | 9615997000c722687f9f69bbbe38cd00a3ce8315 /gui/ui/field.go | |
| parent | ba00c7dab8ea982fc3fdbfc7062234adbe1fcc0e (diff) | |
gui: a widget layer, so the next program does not write one
cal9 had grown its own: borders drawn four fills at a time in two
places, a button-3 menu, hit regions, buttons, text wrapping. None of
that is about calendars, and a second program would have written it
again, differently. It lives in gui/ui now.
It is a kit and not a framework. Every call draws a thing and, where it
makes sense, records where it drew it; the program keeps its own event
loop and its own idea of what to redraw. Menu takes a redraw function
rather than calling back into anything. The moment the library owns the
loop it stops being usable by the next program, which was the problem
being fixed.
Body is the fix for two bugs that turned out to be one. rio draws a
window's border into the client's own image and puts corner cursors in
it, taking those clicks before the client sees them: the border is
rio's resize handle. Filling the whole rectangle erased it, so the
border vanished and so did resizing. Drawing inside Body gives both
back, and the drag-to-resize cal9 had grown -- which could only ever
shrink, since a window stops getting mouse events once the pointer
leaves it -- was deleted rather than fixed.
Field is one line of editable text with a cursor, backspace, ^U and a
paste from /dev/snarf. No selection, no mouse placement, no history: a
form asking for a summary and a time needs none of them and each is
somewhere to be subtly wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'gui/ui/field.go')
| -rw-r--r-- | gui/ui/field.go | 88 |
1 files changed, 88 insertions, 0 deletions
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 +} |
