diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-08-22 13:16:35 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-08-22 13:16:35 -0400 |
| commit | 9aacce8b3b54060d0037eca897856d7273e6e5e8 (patch) | |
| tree | a98ea0cb151a7ded56fe681d6782986b2807dfcf /gui/draw/draw.go | |
| parent | 5ef4699d05bc919255449a9af780f216a0589a72 (diff) | |
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 <noreply@anthropic.com>
Diffstat (limited to 'gui/draw/draw.go')
| -rw-r--r-- | gui/draw/draw.go | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/gui/draw/draw.go b/gui/draw/draw.go new file mode 100644 index 0000000..0aa7911 --- /dev/null +++ b/gui/draw/draw.go @@ -0,0 +1,250 @@ +// Package draw speaks the Plan 9 /dev/draw protocol directly. +// +// There is no C, no cgo and no libdraw here: /dev/draw is a file, its +// protocol is a handful of little-endian messages, and this file is the +// whole of what libdraw's init.c, alloc.c and draw.c actually do. +// See draw(3) and /sys/src/libdraw for the authority. +package draw + +import ( + "encoding/binary" + "fmt" + "os" + "strconv" + "sync" +) + +// Channel descriptors, from /sys/include/draw.h. +const ( + chGrey1 = 0x31 + chRGB24 = 0x081828 + chRGBA32 = 0x08182848 +) + +type Point struct{ X, Y int32 } + +type Rectangle struct{ Min, Max Point } + +func Rect(x0, y0, x1, y1 int32) Rectangle { + return Rectangle{Point{x0, y0}, Point{x1, y1}} +} + +func (r Rectangle) Dx() int32 { return r.Max.X - r.Min.X } +func (r Rectangle) Dy() int32 { return r.Max.Y - r.Min.Y } + +// Add offsets a rectangle by p. +func (r Rectangle) Add(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X + p.X, r.Min.Y + p.Y}, + Point{r.Max.X + p.X, r.Max.Y + p.Y}, + } +} + +var ZP Point + +// Colors are RGBA, as draw(2) writes them. +const ( + White = 0xFFFFFFFF + Black = 0x000000FF + Red = 0xFF0000FF + Green = 0x00FF00FF + Blue = 0x0000FFFF +) + +type Image struct { + d *Display + id uint32 + R Rectangle + Clipr Rectangle + chn uint32 + repl bool +} + +func (i *Image) Rect() Rectangle { return i.R } + +type Display struct { + ctl, data *os.File + dirno int + + // Screen is the display's own image, id 0. Without a window system + // that is the physical screen; under rio it is the whole screen and + // the window comes from /dev/winname instead. + Screen *Image + + // opaque is libdraw's display->opaque: a replicated all-ones GREY1 + // pixel. Draw with a nil mask means "mask with this", not "mask with + // image 0" -- image 0 is the screen. + opaque *Image + + mu sync.Mutex + nextid uint32 +} + +// Init attaches to the draw device under dev (normally "/dev"). +func Init(dev string) (*Display, error) { + if dev == "" { + dev = "/dev" + } + ctl, err := os.OpenFile(dev+"/draw/new", os.O_RDWR, 0) + if err != nil { + return nil, fmt.Errorf("open draw/new: %w", err) + } + // 12 fields of 12 bytes: id, imageid, chan, repl, r[4], clipr[4]. + var info [12 * 12]byte + n, err := ctl.Read(info[:]) + if err != nil || n < 12 { + ctl.Close() + return nil, fmt.Errorf("read draw/new: short read %d: %w", n, err) + } + fld := func(i int) int32 { + s := string(info[i*12 : i*12+12]) + v, _ := strconv.Atoi(trim(s)) + return int32(v) + } + d := &Display{ctl: ctl, dirno: int(fld(0))} + data, err := os.OpenFile(fmt.Sprintf("%s/draw/%d/data", dev, d.dirno), os.O_RDWR, 0) + if err != nil { + ctl.Close() + return nil, fmt.Errorf("open draw/%d/data: %w", d.dirno, err) + } + d.data = data + if n >= len(info) { + d.Screen = &Image{ + d: d, + id: 0, + chn: strToChan(trim(string(info[2*12 : 3*12]))), + repl: fld(3) != 0, + R: Rect(fld(4), fld(5), fld(6), fld(7)), + Clipr: Rect(fld(8), fld(9), fld(10), fld(11)), + } + } + if d.opaque, err = d.Alloc(Rect(0, 0, 1, 1), chGrey1, true, White); err != nil { + d.Close() + return nil, err + } + return d, nil +} + +func (d *Display) Close() error { + d.data.Close() + return d.ctl.Close() +} + +func trim(s string) string { + i, j := 0, len(s) + for i < j && s[i] == ' ' { + i++ + } + for j > i && (s[j-1] == ' ' || s[j-1] == '\n') { + j-- + } + return s[i:j] +} + +// strToChan parses "r8g8b8" and friends. Only what the screen actually +// reports is needed, so unknown names fall back to RGB24. +func strToChan(s string) uint32 { + typ := map[byte]uint32{'r': 0, 'g': 1, 'b': 2, 'k': 3, 'a': 4, 'm': 5, 'x': 6} + var c uint32 + for i := 0; i+1 < len(s); i += 2 { + t, ok := typ[s[i]] + if !ok { + return chRGB24 + } + nb := uint32(s[i+1] - '0') + c = c<<8 | (t&15)<<4 | nb&15 + } + if c == 0 { + return chRGB24 + } + return c +} + +func put32(b []byte, off int, v uint32) { + binary.LittleEndian.PutUint32(b[off:], v) +} + +func (d *Display) write(msg []byte) error { + d.mu.Lock() + defer d.mu.Unlock() + _, err := d.data.Write(msg) + return err +} + +// Alloc creates a new image on the server. A 1x1 replicated image is how +// you make a solid colour: it tiles to fill whatever you draw it into. +func (d *Display) Alloc(r Rectangle, chn uint32, repl bool, col uint32) (*Image, error) { + d.mu.Lock() + d.nextid++ + id := d.nextid + d.mu.Unlock() + + clipr := r + if repl { + // Huge but not infinite, so offsets stay huge instead of overflowing. + clipr = Rect(-0x3FFFFFFF, -0x3FFFFFFF, 0x3FFFFFFF, 0x3FFFFFFF) + } + b := make([]byte, 51) + b[0] = 'b' + put32(b, 1, id) + put32(b, 5, 0) // screenid: not a window + b[9] = 0 // refresh: Refbackup + put32(b, 10, chn) + if repl { + b[14] = 1 + } + put32(b, 15, uint32(r.Min.X)) + put32(b, 19, uint32(r.Min.Y)) + put32(b, 23, uint32(r.Max.X)) + put32(b, 27, uint32(r.Max.Y)) + put32(b, 31, uint32(clipr.Min.X)) + put32(b, 35, uint32(clipr.Min.Y)) + put32(b, 39, uint32(clipr.Max.X)) + put32(b, 43, uint32(clipr.Max.Y)) + put32(b, 47, col) + if err := d.write(b); err != nil { + return nil, fmt.Errorf("alloc image: %w", err) + } + return &Image{d: d, id: id, R: r, Clipr: clipr, chn: chn, repl: repl}, nil +} + +// Color is the common case of Alloc: one replicated pixel of a solid colour. +func (d *Display) Color(col uint32) (*Image, error) { + return d.Alloc(Rect(0, 0, 1, 1), chRGBA32, true, col) +} + +// Free releases the image. All of a client's images go away by themselves +// when its data fd closes, so this is only for long-running programs. +func (i *Image) Free() error { + b := make([]byte, 5) + b[0] = 'f' + put32(b, 1, i.id) + return i.d.write(b) +} + +// Draw copies src (through mask, or opaquely if mask is nil) into r on dst. +func Draw(dst *Image, r Rectangle, src *Image, mask *Image, p Point) error { + b := make([]byte, 45) + b[0] = 'd' + put32(b, 1, dst.id) + put32(b, 5, src.id) + if mask == nil { + mask = dst.d.opaque + } + put32(b, 9, mask.id) + put32(b, 13, uint32(r.Min.X)) + put32(b, 17, uint32(r.Min.Y)) + put32(b, 21, uint32(r.Max.X)) + put32(b, 25, uint32(r.Max.Y)) + put32(b, 29, uint32(p.X)) + put32(b, 33, uint32(p.Y)) + put32(b, 37, uint32(p.X)) + put32(b, 41, uint32(p.Y)) + return dst.d.write(b) +} + +// Flush makes queued drawing visible. devdraw executes each write as it +// arrives, so this only matters for the screen refresh. +func (d *Display) Flush() error { + return d.write([]byte{'v'}) +} |
