From 9aacce8b3b54060d0037eca897856d7273e6e5e8 Mon Sep 17 00:00:00 2001 From: Calvin Morrison Date: Sat, 22 Aug 2026 13:16:35 -0400 Subject: gui: a /dev/draw layer in go, with no cgo and no devdraw 9fans.net/go/draw builds for plan9 but shells out to plan9port's devdraw, which 9front does not have. This talks to /dev/draw itself: the protocol is file i/o, so a pure-go client is a few hundred lines under an already-complete idea. draw/keyboard.go opens /dev/cons and turns the console raw, as libdraw's initkeyboard does. Without it rio keeps its line editor on the window and paints what you type over the drawing. draw/snarf.go writes /dev/snarf, which is what every other program on the system means by copy. Co-Authored-By: Claude Opus 5 --- gui/draw/font.go | 336 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 gui/draw/font.go (limited to 'gui/draw/font.go') diff --git a/gui/draw/font.go b/gui/draw/font.go new file mode 100644 index 0000000..b94f2fd --- /dev/null +++ b/gui/draw/font.go @@ -0,0 +1,336 @@ +package draw + +import ( + "bufio" + "fmt" + "io" + "os" + "path" + "strconv" + "strings" +) + +// Fontchar is one glyph's metrics, unpacked from the 6-byte on-disk form. +type Fontchar struct { + X int32 // left edge in the subfont image + Top, Bottom, Left uint8 + Width uint8 // advance +} + +// Subfont is one image of glyphs plus their metrics -- the unit a .font file +// is assembled from. +type Subfont struct { + Bits *Image + N int + Height, Ascent int + Info []Fontchar // N+1 entries; the last one bounds the last glyph +} + +type frange struct { + min, max rune + offset int + file string + sub *Subfont // loaded on first use +} + +// Font is a .font file: a height, an ascent, and a set of rune ranges each +// served by a subfont. +type Font struct { + d *Display + Name string + Height, Ascent int + ranges []frange +} + +// OpenFont reads a .font file. Subfonts are loaded lazily, because a unicode +// font names dozens of them and a calendar touches two. +func (d *Display) OpenFont(name string) (*Font, error) { + f, err := os.Open(name) + if err != nil { + return nil, fmt.Errorf("open font: %w", err) + } + defer f.Close() + + fnt := &Font{d: d, Name: name} + dir := path.Dir(name) + sc := bufio.NewScanner(f) + first := true + for sc.Scan() { + fld := strings.Fields(sc.Text()) + if len(fld) == 0 { + continue + } + if first { + if len(fld) < 2 { + return nil, fmt.Errorf("font %s: bad first line", name) + } + h, err1 := strconv.ParseInt(fld[0], 0, 32) + a, err2 := strconv.ParseInt(fld[1], 0, 32) + if err1 != nil || err2 != nil || h <= 0 || a <= 0 { + return nil, fmt.Errorf("font %s: bad height/ascent", name) + } + fnt.Height, fnt.Ascent = int(h), int(a) + first = false + continue + } + // min max [offset] file + if len(fld) < 3 { + continue + } + min, err1 := strconv.ParseInt(fld[0], 0, 32) + max, err2 := strconv.ParseInt(fld[1], 0, 32) + if err1 != nil || err2 != nil || min > max { + return nil, fmt.Errorf("font %s: bad range %q", name, sc.Text()) + } + r := frange{min: rune(min), max: rune(max)} + rest := fld[2:] + // The offset is optional and sits before the filename, so it is + // only an offset if it parses as a number and something follows. + if len(rest) >= 2 { + if off, err := strconv.ParseInt(rest[0], 0, 32); err == nil { + r.offset = int(off) + rest = rest[1:] + } + } + r.file = rest[0] + if !path.IsAbs(r.file) { + r.file = path.Join(dir, r.file) + } + fnt.ranges = append(fnt.ranges, r) + } + if err := sc.Err(); err != nil { + return nil, err + } + if first { + return nil, fmt.Errorf("font %s: empty", name) + } + return fnt, nil +} + +// lookup finds the subfont holding r and r's index within it. +func (f *Font) lookup(r rune) (*Subfont, int) { + for i := range f.ranges { + g := &f.ranges[i] + if r < g.min || r > g.max { + continue + } + if g.sub == nil { + s, err := f.d.readSubfont(g.file) + if err != nil { + return nil, 0 + } + g.sub = s + } + n := int(r-g.min) + g.offset + if n < 0 || n >= g.sub.N { + return nil, 0 + } + return g.sub, n + } + return nil, 0 +} + +// String draws s at p (p is the top-left, as in libdraw) in colour src, and +// returns the point just past the last glyph. +func (f *Font) String(dst *Image, p Point, src *Image, s string) Point { + for _, r := range s { + sub, n := f.lookup(r) + if sub == nil { + continue + } + i, i1 := sub.Info[n], sub.Info[n+1] + w := i1.X - i.X + if w > 0 { + Draw(dst, + Rect(p.X+int32(i.Left), p.Y+int32(i.Top), + p.X+int32(i.Left)+w, p.Y+int32(i.Bottom)), + src, sub.Bits, Point{i.X, int32(i.Top)}) + } + p.X += int32(i.Width) + } + return p +} + +// Width is the advance of s, without drawing it. +func (f *Font) Width(s string) int32 { + var w int32 + for _, r := range s { + sub, n := f.lookup(r) + if sub == nil { + continue + } + w += int32(sub.Info[n].Width) + } + return w +} + +// readSubfont loads one subfont file: a Plan 9 image, then the glyph table. +// +// The image is never decompressed here. Plan 9's compressed image format is +// exactly what the draw device's 'Y' message accepts, so the blocks go +// straight to the kernel -- which is all libdraw's creadimage does too. +func (d *Display) readSubfont(name string) (*Subfont, error) { + f, err := os.Open(name) + if err != nil { + return nil, fmt.Errorf("open subfont: %w", err) + } + defer f.Close() + r := bufio.NewReader(f) + + var hdr [5 * 12]byte + if _, err := io.ReadFull(r, hdr[:11]); err != nil { + return nil, fmt.Errorf("%s: short header: %w", name, err) + } + compressed := string(hdr[:11]) == "compressed\n" + if compressed { + if _, err := io.ReadFull(r, hdr[:]); err != nil { + return nil, fmt.Errorf("%s: short header: %w", name, err) + } + } else if _, err := io.ReadFull(r, hdr[11:]); err != nil { + return nil, fmt.Errorf("%s: short header: %w", name, err) + } + + fld := func(i int) int32 { + v, _ := strconv.Atoi(trim(string(hdr[i*12 : i*12+12]))) + return int32(v) + } + chn := strToChan(trim(string(hdr[0:12]))) + ir := Rect(fld(1), fld(2), fld(3), fld(4)) + img, err := d.Alloc(ir, chn, false, 0) + if err != nil { + return nil, fmt.Errorf("%s: %w", name, err) + } + + if compressed { + err = img.loadCompressed(r, ir) + } else { + err = img.loadRaw(r, ir, chn) + } + if err != nil { + return nil, fmt.Errorf("%s: %w", name, err) + } + + // n, height, ascent, then 6 bytes per glyph for n+1 glyphs. + var sh [3 * 12]byte + if _, err := io.ReadFull(r, sh[:]); err != nil { + return nil, fmt.Errorf("%s: short subfont header: %w", name, err) + } + n, _ := strconv.Atoi(trim(string(sh[0:12]))) + if n <= 0 || n > 0x7fff { + return nil, fmt.Errorf("%s: bad glyph count %d", name, n) + } + sf := &Subfont{ + Bits: img, + N: n, + Height: mustAtoi(trim(string(sh[12:24]))), + Ascent: mustAtoi(trim(string(sh[24:36]))), + Info: make([]Fontchar, n+1), + } + pk := make([]byte, 6*(n+1)) + if _, err := io.ReadFull(r, pk); err != nil { + return nil, fmt.Errorf("%s: short glyph table: %w", name, err) + } + for i := 0; i <= n; i++ { + b := pk[i*6:] + sf.Info[i] = Fontchar{ + X: int32(b[0]) | int32(b[1])<<8, + Top: b[2], + Bottom: b[3], + Left: b[4], + Width: b[5], + } + } + return sf, nil +} + +func mustAtoi(s string) int { v, _ := strconv.Atoi(s); return v } + +// loadCompressed forwards each compressed block to the draw device verbatim. +func (i *Image) loadCompressed(r io.Reader, ir Rectangle) error { + var bh [2 * 12]byte + miny := ir.Min.Y + for miny != ir.Max.Y { + if _, err := io.ReadFull(r, bh[:]); err != nil { + return fmt.Errorf("short block header: %w", err) + } + maxy := int32(mustAtoi(trim(string(bh[0:12])))) + nb := mustAtoi(trim(string(bh[12:24]))) + if maxy <= miny || maxy > ir.Max.Y || nb <= 0 { + return fmt.Errorf("bad block: maxy=%d nb=%d", maxy, nb) + } + buf := make([]byte, 21+nb) + buf[0] = 'Y' + put32(buf, 1, i.id) + put32(buf, 5, uint32(ir.Min.X)) + put32(buf, 9, uint32(miny)) + put32(buf, 13, uint32(ir.Max.X)) + put32(buf, 17, uint32(maxy)) + if _, err := io.ReadFull(r, buf[21:]); err != nil { + return fmt.Errorf("short block: %w", err) + } + if err := i.d.write(buf); err != nil { + return err + } + miny = maxy + } + return nil +} + +// loadRaw uploads an uncompressed image a stripe at a time with 'y'. +func (i *Image) loadRaw(r io.Reader, ir Rectangle, chn uint32) error { + depth := chanDepth(chn) + if depth == 0 { + return fmt.Errorf("bad channel %#x", chn) + } + bpl := bytesPerLine(ir, depth) + // Keep each write comfortably inside the device's iounit. + rows := 8000 / bpl + if rows < 1 { + rows = 1 + } + for y := ir.Min.Y; y < ir.Max.Y; { + y1 := y + int32(rows) + if y1 > ir.Max.Y { + y1 = ir.Max.Y + } + n := bpl * int(y1-y) + buf := make([]byte, 21+n) + buf[0] = 'y' + put32(buf, 1, i.id) + put32(buf, 5, uint32(ir.Min.X)) + put32(buf, 9, uint32(y)) + put32(buf, 13, uint32(ir.Max.X)) + put32(buf, 17, uint32(y1)) + if _, err := io.ReadFull(r, buf[21:]); err != nil { + return fmt.Errorf("short pixel data: %w", err) + } + if err := i.d.write(buf); err != nil { + return err + } + y = y1 + } + return nil +} + +// chanDepth is bits per pixel for a channel descriptor. +func chanDepth(c uint32) int { + d := 0 + for ; c != 0; c >>= 8 { + d += int(c & 15) + } + return d +} + +// bytesPerLine mirrors libdraw's unitsperline(r, d, 8), including the +// negative-min.x case. +func bytesPerLine(r Rectangle, d int) int { + if d <= 0 || d > 32 { + return 0 + } + if r.Min.X >= 0 { + l := (int(r.Max.X)*d + 7) / 8 + return l - (int(r.Min.X)*d)/8 + } + l := (int(r.Max.X)*d + 7) / 8 + return l + (int(-r.Min.X)*d+7)/8 +} -- cgit v1.2.3