├── .gitignore ├── cursor.go ├── snarf.go ├── font_test.go ├── go.mod ├── pix.go ├── allocimagemix_test.go ├── snarf_test.go ├── mouse.go ├── allocimagemix.go ├── cursor_windows.go ├── color.go ├── init_test.go ├── README.md ├── example └── basic │ └── main.go ├── keyboard.go ├── go.sum ├── events.go ├── display.go ├── cursor_x11.go ├── image.go ├── ellipse.go ├── init.go ├── font.go └── snarf_unix.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /cursor.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | // +build !dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris 3 | 4 | package duitdraw 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "image" 10 | ) 11 | 12 | func moveTo(p image.Point) error { 13 | return fmt.Errorf("moveTo: TODO for this os") 14 | } 15 | 16 | func setCursor(c *Cursor) error { 17 | if c != nil { 18 | return errors.New("duitdraw: SetCursor is not implemented") 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /snarf.go: -------------------------------------------------------------------------------- 1 | // +build windows darwin 2 | 3 | package duitdraw 4 | 5 | import ( 6 | "github.com/atotto/clipboard" 7 | ) 8 | 9 | func (d *Display) readSnarf(buf []byte) (int, int, error) { 10 | s, err := clipboard.ReadAll() 11 | if err != nil { 12 | return 0, 0, err 13 | } 14 | n := copy(buf, s) 15 | if n < len(s) { 16 | return n, len(s), errShortSnarfBuffer 17 | } 18 | return n, n, nil 19 | } 20 | 21 | func (d *Display) writeSnarf(data []byte) error { 22 | return clipboard.WriteAll(string(data)) 23 | } 24 | -------------------------------------------------------------------------------- /font_test.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import "testing" 4 | 5 | func TestStringWidth(t *testing.T) { 6 | tt := []string{ 7 | "Hello world!", 8 | "I can eat glass and it doesn't hurt me.", 9 | "私はガラスを食べられます。それは私を傷つけません。", 10 | "আমি কাঁচ খেতে পারি, তাতে আমার কোনো ক্ষতি হয় না।", 11 | } 12 | for _, tc := range tt { 13 | sum := 0 14 | for _, c := range tc { 15 | sum += defaultFont.StringWidth(string(c)) 16 | } 17 | dx := defaultFont.StringWidth(tc) 18 | if dx != sum { 19 | t.Errorf("StringWidth(%q) is %v; expected %v", tc, dx, sum) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module duitdraw 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 7 | github.com/as/cursor v0.6.7 8 | github.com/as/ms v0.1.0 9 | github.com/atotto/clipboard v0.1.4 10 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 11 | github.com/ktye/duitdraw v0.0.0-20190328070634-a54e9bd5a862 12 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 13 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 14 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6 15 | ) 16 | -------------------------------------------------------------------------------- /pix.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | // Pix represents a pixel format described simple notation: r8g8b8 for RGB24, m8 4 | // for color-mapped 8 bits, etc. The representation is 8 bits per channel, 5 | // starting at the low end, with each byte represnted as a channel specifier 6 | // (CRed etc.) in the high 4 bits and the number of pixels in the low 4 bits. 7 | type Pix uint32 8 | 9 | const ( 10 | CRed = iota 11 | CGreen 12 | CBlue 13 | CGrey 14 | CAlpha 15 | CMap 16 | CIgnore 17 | NChan 18 | ) 19 | 20 | var GREY8 = MakePix(CGrey, 8) 21 | var ARGB32 = MakePix(CAlpha, 8, CRed, 8, CGreen, 8, CBlue, 8) // stupid VGAs 22 | var ABGR32 = MakePix(CAlpha, 8, CBlue, 8, CGreen, 8, CRed, 8) 23 | 24 | // MakePix returns a Pix by placing the successive integers into 4-bit nibbles, low bits first. 25 | func MakePix(list ...int) Pix { 26 | var p Pix 27 | for _, x := range list { 28 | p <<= 4 29 | p |= Pix(x) 30 | } 31 | return p 32 | } 33 | -------------------------------------------------------------------------------- /allocimagemix_test.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "image" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestAllocImageMix(t *testing.T) { 10 | tt := []struct { 11 | color1, color3, mix Color 12 | }{ 13 | {Palebluegreen, White, 0xEAFFFFFF}, 14 | {Paleyellow, White, 0xFFFFEAFF}, 15 | {0xAAAAAAAA, 0x55555555, 0x6A6A6AAA}, 16 | {0x55555555, 0xAAAAAAAA, 0x95959555}, 17 | {0x0A0A0A0A, 0x05050505, 0x0606060A}, 18 | {0x05050505, 0x0A0A0A0A, 0x08080805}, 19 | } 20 | d, err := Init(nil, "", "", "") 21 | if err != nil { 22 | t.Fatalf("Init failed: %v", err) 23 | } 24 | r := image.Rect(0, 0, 1, 1) 25 | for _, tc := range tt { 26 | b := d.AllocImageMix(tc.color1, tc.color3) 27 | if b.Display != d { 28 | t.Errorf("image has display %p; exptected %p", b.Display, d) 29 | } 30 | if !reflect.DeepEqual(b.R, r) { 31 | t.Errorf("image rect is %v; expected %v", b.R, r) 32 | } 33 | bm := image.NewUniform(tc.mix.rgba()) 34 | if !reflect.DeepEqual(bm, b.m) { 35 | t.Errorf("image is %X; exptected %X", b.m, bm) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /snarf_test.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestSnarf(t *testing.T) { 9 | tt := []struct { 10 | input []byte 11 | output []byte 12 | nbuf int 13 | err error 14 | }{ 15 | {nil, nil, 10, nil}, 16 | {[]byte("Hello"), []byte("Hello"), 5, nil}, 17 | {[]byte("Hello, 世界"), []byte("Hello, 世界"), 100, nil}, 18 | {[]byte("one\ntwo\three\n"), []byte("one\ntwo\three\n"), 100, nil}, 19 | {[]byte("0123456789"), []byte("0123456"), 7, errShortSnarfBuffer}, 20 | } 21 | for _, tc := range tt { 22 | var d Display 23 | err := d.WriteSnarf(tc.input) 24 | if err != nil { 25 | t.Errorf("writing snarf buffer %q failed: %v\n", tc.input, err) 26 | } 27 | b := make([]byte, tc.nbuf) 28 | n, size, err := d.ReadSnarf(b) 29 | if err != tc.err { 30 | t.Errorf("reading snarf buffer %q failed: %v\n", tc.input, err) 31 | } 32 | if size != len(tc.input) { 33 | t.Errorf("snarf buffer size is %v after writing %v bytes\n", size, len(tc.input)) 34 | } 35 | if !bytes.Equal(b[:n], tc.output) { 36 | t.Errorf("wrote %q to snarf buffer but read %q\n", tc.output, b[:n]) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /mouse.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | // Mouse is the structure describing the current state of the mouse. 8 | type Mouse struct { 9 | image.Point // Location. 10 | Buttons int // Buttons; bit 0 is button 1, bit 1 is button 2, etc. 11 | Msec uint32 // Time stamp in milliseconds. 12 | 13 | } 14 | 15 | // TODO: Mouse field is racy but okay. 16 | 17 | // Mousectl holds the interface to receive mouse events. 18 | // The Mousectl's Mouse is updated after send so it doesn't 19 | // have the wrong value if the sending goroutine blocks during send. 20 | // This means that programs should receive into Mousectl.Mouse 21 | // if they want full synchrony. 22 | type Mousectl struct { 23 | Mouse // Store Mouse events here. 24 | C chan Mouse // Channel of Mouse events. 25 | Resize chan bool // Each received value signals a window resize (see the display.Attach method). 26 | Display *Display 27 | } 28 | 29 | // Read returns the next mouse event. 30 | func (mc *Mousectl) Read() Mouse { 31 | mc.Display.Flush() 32 | m := <-mc.C 33 | // Mouse field is racy. See Mousectl documentation. 34 | mc.Mouse = m 35 | return m 36 | } 37 | -------------------------------------------------------------------------------- /allocimagemix.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import "image" 4 | 5 | // AllocImageMix blends the two colors to create a tiled image representing 6 | // their combination. For pixel formats of 8 bits or less, it creates a 2x2 7 | // pixel texture whose average value is the mix. Otherwise it creates a 1-pixel 8 | // solid color blended using 50% alpha for each. 9 | func (d *Display) AllocImageMix(color1, color3 Color) *Image { 10 | // if d.ScreenImage.Depth <= 8 { // create a 2x2 texture 11 | // t, _ := d.allocImage(image.Rect(0, 0, 1, 1), d.ScreenImage.Pix, false, color1) 12 | // b, _ := d.allocImage(image.Rect(0, 0, 2, 2), d.ScreenImage.Pix, true, color3) 13 | // b.draw(image.Rect(0, 0, 1, 1), t, nil, image.ZP) 14 | // t.free() 15 | // return b 16 | // } 17 | 18 | // use a solid color, blended using alpha 19 | const ( 20 | q1 = 0x3f 21 | q3 = 0xff - q1 22 | ) 23 | c1 := color1.rgba() 24 | c3 := color3.rgba() 25 | r := (uint32(c1.R)*q1 + uint32(c3.R)*q3) / 0xff 26 | g := (uint32(c1.G)*q1 + uint32(c3.G)*q3) / 0xff 27 | b := (uint32(c1.B)*q1 + uint32(c3.B)*q3) / 0xff 28 | a := uint32(c1.A) 29 | c := Color(r<<24 | g<<16 | b<<8 | a) 30 | img, _ := d.AllocImage(image.Rect(0, 0, 1, 1), d.ScreenImage.Pix, true, c) 31 | return img 32 | } 33 | -------------------------------------------------------------------------------- /cursor_windows.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "image" 7 | "os" 8 | 9 | "github.com/as/cursor" 10 | "github.com/as/ms/win" 11 | ) 12 | 13 | // This sets the mouse cursor relative to the current window. 14 | // 15 | // TODO: There seems to be an offset, maybe it's related to the window borders. 16 | // You can calculate your offset by pressing the tab key and clicking with the mouse. 17 | // Log coordinates of MouseMove and mouse.Event (in display.go) and substract the results. 18 | // 19 | // Source: github.com/as/a/mouse_windows.go 20 | 21 | var ( 22 | winfd win.Window 23 | winfderr error 24 | cursorOffset = image.Point{4, 4} 25 | ) 26 | 27 | func tryWindow() { 28 | if winfd != 0 && winfderr == nil { 29 | return 30 | } 31 | winfd, winfderr = win.Open(os.Getpid()) 32 | } 33 | 34 | func moveTo(pt image.Point) error { 35 | tryWindow() 36 | abs, err := winfd.Client() 37 | if err != nil { 38 | winfderr = err 39 | return err 40 | } 41 | pt = pt.Add(abs.Min).Sub(cursorOffset) 42 | if cursor.MoveTo(pt) == false { 43 | return fmt.Errorf("move cursor failed") 44 | } 45 | return nil 46 | } 47 | 48 | func setCursor(c *Cursor) error { 49 | if c != nil { 50 | return errors.New("duitdraw: SetCursor is not implemented") 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import "image/color" 4 | 5 | // A Color represents an RGBA value, 8 bits per element. Red is the high 8 6 | // bits, green the next 8 and so on. 7 | type Color uint32 8 | 9 | const ( 10 | Opaque Color = 0xFFFFFFFF 11 | Transparent Color = 0x00000000 /* only useful for allocimage memfillcolor */ 12 | Black Color = 0x000000FF 13 | White Color = 0xFFFFFFFF 14 | Red Color = 0xFF0000FF 15 | Green Color = 0x00FF00FF 16 | Blue Color = 0x0000FFFF 17 | Cyan Color = 0x00FFFFFF 18 | Magenta Color = 0xFF00FFFF 19 | Yellow Color = 0xFFFF00FF 20 | Paleyellow Color = 0xFFFFAAFF 21 | Darkyellow Color = 0xEEEE9EFF 22 | Darkgreen Color = 0x448844FF 23 | Palegreen Color = 0xAAFFAAFF 24 | Medgreen Color = 0x88CC88FF 25 | Darkblue Color = 0x000055FF 26 | Palebluegreen Color = 0xAAFFFFFF 27 | Paleblue Color = 0x0000BBFF 28 | Bluegreen Color = 0x008888FF 29 | Greygreen Color = 0x55AAAAFF 30 | Palegreygreen Color = 0x9EEEEEFF 31 | Yellowgreen Color = 0x99994CFF 32 | Medblue Color = 0x000099FF 33 | Greyblue Color = 0x005DBBFF 34 | Palegreyblue Color = 0x4993DDFF 35 | Purpleblue Color = 0x8888CCFF 36 | 37 | Notacolor Color = 0xFFFFFF00 38 | Nofill Color = Notacolor 39 | ) 40 | 41 | func (c Color) rgba() color.RGBA { 42 | return color.RGBA{ 43 | R: uint8(c >> 24), 44 | G: uint8(c >> 16), 45 | B: uint8(c >> 8), 46 | A: uint8(c), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /init_test.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | ) 8 | 9 | func testDrawMask(t *testing.T, d *Display, c *Image, expect func(color.RGBA) color.RGBA) { 10 | r := image.Rect(0, 0, 2, 2) 11 | 12 | m := image.NewRGBA(r) 13 | m.SetRGBA(0, 0, color.RGBA{0x11, 0x55, 0x99, 0xdd}) 14 | m.SetRGBA(0, 1, color.RGBA{0x22, 0x66, 0xaa, 0xee}) 15 | m.SetRGBA(1, 0, color.RGBA{0x33, 0x77, 0xbb, 0xff}) 16 | m.SetRGBA(1, 1, color.RGBA{0x44, 0x88, 0xcc, 0x00}) 17 | img := d.MakeImage(m) 18 | 19 | dst := d.MakeImage(image.NewRGBA(r)) 20 | dst.Draw(r, img, nil, image.ZP) 21 | 22 | mask := d.MakeImage(image.NewRGBA(r)) 23 | mask.Draw(r, c, nil, image.ZP) 24 | 25 | dst.Draw(dst.R, d.Black, mask, image.ZP) 26 | 27 | q := dst.m.(*image.RGBA) 28 | for x := r.Min.X; x < r.Max.X; x++ { 29 | for y := r.Min.Y; y < r.Max.Y; y++ { 30 | got := q.RGBAAt(x, y) 31 | want := expect(m.RGBAAt(x, y)) 32 | if got != want { 33 | t.Errorf("dst value at (%v, %v) is %v; expected %v", x, y, got, want) 34 | } 35 | } 36 | } 37 | } 38 | 39 | func TestTransparent(t *testing.T) { 40 | d, err := Init(nil, "", "Transparen test", "") 41 | if err != nil { 42 | t.Fatalf("can't open display: %v", err) 43 | } 44 | testDrawMask(t, d, d.Transparent, func(c color.RGBA) color.RGBA { 45 | return c 46 | }) 47 | } 48 | 49 | func TestOpaque(t *testing.T) { 50 | d, err := Init(nil, "", "Opaque test", "") 51 | if err != nil { 52 | t.Fatalf("can't open display: %v", err) 53 | } 54 | testDrawMask(t, d, d.Opaque, func(c color.RGBA) color.RGBA { 55 | return Black.rgba() 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # duitdraw 2 | shiny backend for duit 3 | 4 | # About 5 | [Duit](https://github.com/mjl-/duit) is a ui toolkit for [go](https://golang.org). 6 | As a backend it uses plan9 port's /dev/draw emulation via a [go interface](https://github.com/9fans/go/tree/master/draw). 7 | 8 | This has a some drawbacks: 9 | - heavy run time dependency 10 | - no easy windows support 11 | - no simple deployment with a single static binary 12 | 13 | As a first step `duitdraw` is a drop-in replacement for `github.com/9fans/go/draw`, using a backend based on [shiny](https://github.com/golang/exp/tree/master/shiny). This has the advantage, that no changes are needed for duit. 14 | 15 | Once this becomes a valid alternative to the original drawing backend, duit could be changed to interface better with shiny. 16 | 17 | The scope of the package is not a full implementation of `9fans.net/go/draw`. Everything that is not needed by duit in the initial release state is removed. 18 | 19 | 20 | # Usage 21 | To try the backend, copy the content of this repository to `$GOPATH/src/9fans.net/go/draw` and recompile duit. 22 | 23 | # Current state 24 | This is just a very basic first first release and tested only on windows. 25 | Please test and comment. 26 | 27 | - fonts (right now Go regular is embedded) 28 | - plan9 style or ttf path? 29 | - ttf: freetype or golang.org/x/image/font/sfnt? 30 | - drawing 31 | - only a simple line algorithm is implemented 32 | - which rasterization should be used, freetype or golang.org/x/image/vector? 33 | - general line rasterizer missing 34 | - Arc, FillArc missing (ellipse.go) 35 | - clipboard 36 | - uses atotto's, is that ok? 37 | - mouse movement 38 | - uses as/cursor, is that ok? 39 | - inner window offset is hard coded 40 | - shiny 41 | - window flickers on resize, are we using shiny the wrong way? 42 | -------------------------------------------------------------------------------- /example/basic/main.go: -------------------------------------------------------------------------------- 1 | // +build example 2 | // 3 | // This build tag means that "go install github.com/ktye/duitdraw/..." doesn't 4 | // install this example program. Use "go run main.go" to run it or "go install 5 | // -tags=example" to install it. 6 | 7 | // Basic is an example that demonstrates how to use the Main function to create 8 | // one or more windows. 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "image" 14 | "log" 15 | "strconv" 16 | 17 | draw "github.com/ktye/duitdraw" 18 | ) 19 | 20 | func main() { 21 | draw.Main(func(dd *draw.Device) { 22 | for i := 0; i < 3; i++ { 23 | label := fmt.Sprintf("duitdraw (%v)", i+1) 24 | display, err := dd.NewDisplay(nil, "@16pt", label, "500x500") 25 | if err != nil { 26 | log.Fatalf("can't open display: %v\n", err) 27 | } 28 | if err := display.Attach(draw.Refnone); err != nil { 29 | log.Fatalf("failed to attach to window: %v", err) 30 | } 31 | mousectl := display.InitMouse() 32 | keyboardctl := display.InitKeyboard() 33 | 34 | text := strconv.Itoa(i + 1) 35 | redraw(display, text) 36 | 37 | go func() { 38 | for { 39 | select { 40 | case mousectl.Mouse = <-mousectl.C: 41 | case <-mousectl.Resize: 42 | if err := display.Attach(draw.Refnone); err != nil { 43 | log.Fatalf("failed to attach to window: %v", err) 44 | } 45 | redraw(display, text) 46 | case <-keyboardctl.C: 47 | display.Close() 48 | return 49 | } 50 | } 51 | }() 52 | } 53 | }) 54 | } 55 | 56 | func redraw(display *draw.Display, text string) { 57 | r := display.ScreenImage.R 58 | display.ScreenImage.Draw(r, display.White, nil, image.ZP) 59 | 60 | p0 := image.Pt(r.Dx()/2, r.Dy()/2) 61 | display.ScreenImage.String(p0, display.Black, p0, display.DefaultFont, text) 62 | 63 | display.Flush() 64 | } 65 | -------------------------------------------------------------------------------- /keyboard.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import "golang.org/x/mobile/event/key" 4 | 5 | // Uncommented Key constants are defined for plan9 but not used by duit. 6 | 7 | const ( 8 | KeyFn = '\uF000' 9 | 10 | KeyHome = KeyFn | 0x0D 11 | KeyUp = KeyFn | 0x0E 12 | KeyPageUp = KeyFn | 0xF 13 | //KeyPrint = KeyFn | 0x10 14 | KeyLeft = KeyFn | 0x11 15 | KeyRight = KeyFn | 0x12 16 | KeyDown = 0x80 17 | //KeyView = 0x80 18 | KeyPageDown = KeyFn | 0x13 19 | KeyInsert = KeyFn | 0x14 20 | KeyEnd = KeyFn | 0x18 21 | //KeyAlt = KeyFn | 0x15 22 | //KeyShift = KeyFn | 0x16 23 | //KeyCtl = KeyFn | 0x17 24 | KeyBackspace = 0x08 25 | KeyDelete = 0x7F 26 | KeyEscape = 0x1b 27 | //KeyEOF = 0x04 28 | KeyCmd = 0xF100 29 | ) 30 | 31 | // Keymap maps from key event codes to runes, that duit expects. 32 | var keymap = map[key.Code]rune{ 33 | key.CodeHome: KeyHome, 34 | key.CodeUpArrow: KeyUp, 35 | key.CodePageUp: KeyPageUp, 36 | key.CodeLeftArrow: KeyLeft, 37 | key.CodeRightArrow: KeyRight, 38 | key.CodeDownArrow: KeyDown, 39 | key.CodePageDown: KeyPageDown, 40 | key.CodeInsert: KeyInsert, 41 | key.CodeEnd: KeyEnd, 42 | key.CodeDeleteBackspace: KeyBackspace, 43 | //key.CodeDelete: KeyDelete, 44 | key.CodeDeleteForward: KeyDelete, 45 | key.CodeEscape: KeyEscape, 46 | //key.CodeCmd: KeyCmd, 47 | key.CodeReturnEnter: '\n', 48 | key.CodeTab: '\t', 49 | } 50 | 51 | var ctrlMods = map[rune]rune{ 52 | 'a': 0x01, // ^a: beginning of line 53 | 'e': 0x05, // ^e: end of line 54 | 'f': 0x06, // ^f: complete 55 | 'h': 0x08, // ^h: erase character 56 | 'u': 0x15, // ^u: erase line 57 | 'w': 0x17, // ^w: erase word 58 | } 59 | 60 | // Keyboardctl is the source of keyboard events. 61 | type Keyboardctl struct { 62 | C chan rune // Channel on which keyboard characters are delivered. 63 | } 64 | 65 | // InitKeyboard connects to the keyboard and returns a Keyboardctl to listen to it. 66 | func (d *Display) InitKeyboard() *Keyboardctl { 67 | return &d.keyboard 68 | } 69 | 70 | // KeyTranslator translates a key.Event to a rune. 71 | // If present, it overwrites the default mechanism. 72 | // If TranslateKey returns -1, the key event is ignored. 73 | type KeyTranslator interface { 74 | TranslateKey(key.Event) rune 75 | } 76 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= 2 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 3 | github.com/as/cursor v0.6.7 h1:qFdoGUkpUmWi5DRZ47yfwalbjomzwqxwfCaO1gkggyU= 4 | github.com/as/cursor v0.6.7/go.mod h1:5KhKz3a12Z5ZtlwC5AFyO7A6WK5YhxuPxj7V0E/ZN1k= 5 | github.com/as/ms v0.1.0 h1:3AX4AK7wnxLBfgfcyq8ECuzjrOcXORvvdei+Gc7MrDQ= 6 | github.com/as/ms v0.1.0/go.mod h1:99/Re6FyxzzcO5rIVjzaaXdhoPuGB5LghcUDAYIX5qk= 7 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 8 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 9 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 10 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 11 | github.com/ktye/duitdraw v0.0.0-20190328070634-a54e9bd5a862 h1:ARGTQFjPom/kz8op6a+KTfKLAcoIDM6vdpjl5u4Cy+Y= 12 | github.com/ktye/duitdraw v0.0.0-20190328070634-a54e9bd5a862/go.mod h1:TsTcPGNjOvXTtzBTFZdpRDbSnhDizngqzp0Q6ZipRYk= 13 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 14 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= 16 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 17 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= 18 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 19 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6 h1:Tus/Y4w3V77xDsGwKUC8a/QrV7jScpU557J77lFffNs= 20 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 21 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 22 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 23 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 24 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 25 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 26 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 28 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 29 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "image" 5 | "io" 6 | "time" 7 | 8 | "golang.org/x/mobile/event/key" 9 | "golang.org/x/mobile/event/lifecycle" 10 | "golang.org/x/mobile/event/mouse" 11 | "golang.org/x/mobile/event/paint" 12 | "golang.org/x/mobile/event/size" 13 | ) 14 | 15 | // EventLoop is the event loop for a single window. 16 | func (d *Display) eventLoop(errch chan<- error) { 17 | w := d.window 18 | b := d.buffer 19 | var err error 20 | var mm Mouse 21 | 22 | resizeFunc := func(e size.Event) { 23 | d.ScreenImage.Lock() 24 | if b != nil { 25 | b.Release() 26 | } 27 | b, err = mainScreen.NewBuffer(e.Size()) 28 | if err != nil { 29 | errch <- err 30 | d.ScreenImage.Unlock() 31 | return 32 | } 33 | d.buffer = b 34 | d.ScreenImage.m = b.RGBA() 35 | d.ScreenImage.R = b.Bounds() 36 | d.ScreenImage.Unlock() 37 | d.mouse.Resize <- true 38 | } 39 | // Initial resize call, to allocate the buffer. 40 | resizeFunc(size.Event{WidthPx: 800, HeightPx: 600}) 41 | 42 | // Send an initial mouse event to trigger a Redraw. 43 | d.mouse.C <- d.mouse.Mouse 44 | 45 | // Delay and filter resize events. 46 | resize := make(chan size.Event) 47 | go func(se chan size.Event) { 48 | var cur size.Event 49 | delay := 100 * time.Millisecond 50 | t := time.NewTimer(delay) 51 | for { 52 | select { 53 | case e := <-se: 54 | cur = e 55 | t.Reset(delay) // Is that safe? 56 | case <-t.C: 57 | resizeFunc(cur) 58 | } 59 | } 60 | }(resize) 61 | 62 | for { 63 | switch e := w.NextEvent().(type) { 64 | case lifecycle.Event: 65 | if e.To == lifecycle.StageDead { 66 | errch <- io.EOF 67 | return 68 | } 69 | 70 | case paint.Event: 71 | if b != nil { 72 | 73 | d.ScreenImage.Lock() 74 | w.Upload(image.Point{}, b, b.Bounds()) 75 | w.Publish() 76 | d.ScreenImage.Unlock() 77 | } 78 | 79 | case size.Event: 80 | // When minimizing a window, it receives a size.Event, 81 | // but the new size is 0. duit complains about and exits. 82 | if e.WidthPx == 0 { 83 | continue 84 | } 85 | resize <- e 86 | 87 | case mouse.Event: 88 | // Mouse.Buttons stores a bitmask for each button state. 89 | // On the other side a mouse.Event arrives, if anything changes. 90 | if e.Button > 0 { // TODO: wheel is < 0 91 | if e.Direction == mouse.DirPress { 92 | // Uncomment for cursorOffset calibration: 93 | // fmt.Printf("shiny: mouse click: %f %f\n", e.X, e.Y) 94 | mm.Buttons ^= 1 << uint(e.Button-1) 95 | } else if e.Direction == mouse.DirRelease { 96 | mm.Buttons &= ^(1 << uint(e.Button-1)) 97 | } 98 | } else if e.Button < 0 { 99 | // For mouse wheel events, we receive a single event 100 | // but duit expects two: set the bit and release it. 101 | shift := uint(3) // ButtonWheelUp 102 | if e.Button == mouse.ButtonWheelDown { 103 | shift = 4 104 | } 105 | mm.Buttons ^= 1 << shift 106 | d.sendMouseEvent(e, &mm) 107 | mm.Buttons &= ^(1 << shift) 108 | } 109 | d.sendMouseEvent(e, &mm) 110 | 111 | case key.Event: 112 | if t := d.KeyTranslator; t == nil { 113 | // We forward the event for key presses and subsequent events 114 | // if the key remains down, but not for releases. 115 | var sendKey rune = -1 116 | if r := e.Rune; e.Direction != key.DirRelease { 117 | if r != -1 { 118 | sendKey = r 119 | } else { 120 | if r, ok := keymap[e.Code]; ok { 121 | sendKey = r 122 | } 123 | } 124 | 125 | } 126 | if sendKey != -1 { 127 | // Shiny sends \r on Enter, duit expects \n. 128 | if sendKey == '\r' { 129 | sendKey = '\n' 130 | } 131 | if e.Modifiers == 0x2 { // Ctrl 132 | if r, ok := ctrlMods[sendKey]; ok { 133 | sendKey = r 134 | } 135 | } 136 | // fmt.Printf("shiny: key: %x %v\n", sendKey, e) 137 | d.keyboard.C <- sendKey 138 | } 139 | 140 | // TODO: what about Shift-KeyLeft/Right 141 | // to mark text? This seems to be unsupported in duit right now. 142 | 143 | } else if r := t.TranslateKey(e); r != -1 { 144 | d.keyboard.C <- r 145 | } 146 | case error: 147 | errch <- e 148 | 149 | } 150 | } 151 | } 152 | 153 | func (d *Display) sendMouseEvent(e mouse.Event, m *Mouse) { 154 | m.Point.X = int(e.X) 155 | m.Point.Y = int(e.Y) 156 | t := time.Now().UnixNano() / (1000 * 1000) // Milliseconds 157 | m.Msec = uint32(t) 158 | 159 | d.mouse.C <- *m 160 | // Mouse field is racy. See Mousectl documentation. 161 | d.mouse.Mouse = *m 162 | } 163 | -------------------------------------------------------------------------------- /display.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/draw" 7 | 8 | "golang.org/x/exp/shiny/screen" 9 | "golang.org/x/mobile/event/lifecycle" 10 | ) 11 | 12 | // Refresh algorithms to execute when a window is resized or uncovered. 13 | // Ignored in this implementation. 14 | const ( 15 | Refbackup = 0 16 | Refnone = 1 17 | Refmesg = 2 18 | ) 19 | 20 | // DefaultDPI is the initial DPI setting for a new display. 21 | // TODO: should we get DPI settings from the screen? 22 | // Currently there is no interface in shiny. 23 | const DefaultDPI = 100 24 | 25 | const DefaultFontSize = 10 26 | 27 | // Display stores the information for a single window, that is returned to duit. 28 | // Duit requestes a Display by calling Init for each window. 29 | type Display struct { 30 | DPI int 31 | ScreenImage *Image 32 | DefaultFont *Font 33 | Black *Image // Pre-allocated color. 34 | White *Image // Pre-allocated color. 35 | Opaque *Image // Pre-allocated color. 36 | Transparent *Image // Pre-allocated color. 37 | KeyTranslator KeyTranslator 38 | mouse Mousectl 39 | keyboard Keyboardctl 40 | window screen.Window 41 | buffer screen.Buffer 42 | } 43 | 44 | // AllocImage allocates a new Image on display d. The arguments are: 45 | // - the rectangle representing the size 46 | // - the pixel descriptor: RGBA32 etc. 47 | // - whether the image is to be replicated (tiled) 48 | // - the starting background color for the image 49 | // 50 | // Duit calls AllocImage to allocate colors for a single pixel rectange with repl = true. 51 | // We return a uniform image instead. 52 | func (d *Display) AllocImage(r image.Rectangle, pix Pix, repl bool, val Color) (*Image, error) { 53 | c := val.rgba() 54 | 55 | // Ignore repl if the image size is > 1. 56 | if repl && r.Max.X == 1 && r.Max.Y == 1 { 57 | return &Image{ 58 | Display: d, 59 | R: r, 60 | m: image.NewUniform(c), 61 | }, nil 62 | } else { 63 | m := image.NewRGBA(r) 64 | draw.Draw(m, m.Bounds(), &image.Uniform{c}, image.ZP, draw.Src) 65 | return &Image{ 66 | Display: d, 67 | R: r, 68 | m: m, 69 | }, nil 70 | 71 | } 72 | } 73 | 74 | // Attach (re-)attaches to a display, typically after a resize, updating the 75 | // display's associated image, screen, and screen image data structures. 76 | func (d *Display) Attach(ref int) error { 77 | return nil // TODO: do we need this? 78 | } 79 | 80 | // Close closes the window. 81 | func (d *Display) Close() error { 82 | e := lifecycle.Event{ 83 | To: lifecycle.StageDead, 84 | } 85 | d.window.Send(e) 86 | return nil 87 | } 88 | 89 | // Flush flushes pending I/O to the server, making any drawing changes visible. 90 | func (d *Display) Flush() error { 91 | d.ScreenImage.Lock() 92 | defer d.ScreenImage.Unlock() 93 | 94 | d.window.Upload(image.Point{}, d.buffer, d.buffer.Bounds()) 95 | d.window.Publish() 96 | return nil 97 | } 98 | 99 | // InitMouse connects to the mouse and returns a Mousectl to interact with it. 100 | func (d *Display) InitMouse() *Mousectl { 101 | return &d.mouse 102 | } 103 | 104 | // Moveto moves the mouse cursor to the specified location. 105 | func (d *Display) MoveTo(pt image.Point) error { 106 | // Uncomment for cursor calibration: 107 | // fmt.Printf("shiny: MoveTo %v\n", pt) 108 | d.ScreenImage.Lock() 109 | defer d.ScreenImage.Unlock() 110 | return moveTo(pt) 111 | } 112 | 113 | // SetDebug enables debugging for the remote devdraw server. 114 | func (d *Display) SetDebug(debug bool) { 115 | } 116 | 117 | var errShortSnarfBuffer = fmt.Errorf("ReadSnarf: buffer is too short") 118 | 119 | // ReadSnarf reads the snarf buffer into buf, returning the number of bytes read, 120 | // the total size of the snarf buffer (useful if buf is too short), and any 121 | // error. No error is returned if there is no problem except for buf being too 122 | // short. 123 | func (d *Display) ReadSnarf(buf []byte) (int, int, error) { 124 | return d.readSnarf(buf) 125 | } 126 | 127 | // WriteSnarf writes the data to the snarf buffer. 128 | func (d *Display) WriteSnarf(data []byte) error { 129 | return d.writeSnarf(data) 130 | } 131 | 132 | func (d *Display) ScaleSize(n int) int { 133 | if d == nil || d.DPI <= DefaultDPI { 134 | return n 135 | } 136 | return (n*d.DPI + DefaultDPI/2) / DefaultDPI 137 | } 138 | 139 | // Cursor describes a single cursor. 140 | type Cursor struct { 141 | image.Point 142 | Clr [2 * 16]uint8 143 | Set [2 * 16]uint8 144 | } 145 | 146 | // SetCursor sets the mouse cursor to the specified cursor image. 147 | // SetCursor(nil) changes the cursor to the standard system cursor. 148 | func (d *Display) SetCursor(c *Cursor) error { 149 | d.ScreenImage.Lock() 150 | defer d.ScreenImage.Unlock() 151 | return setCursor(c) 152 | } 153 | -------------------------------------------------------------------------------- /cursor_x11.go: -------------------------------------------------------------------------------- 1 | // +build dragonfly freebsd linux netbsd openbsd solaris 2 | 3 | package duitdraw 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | 9 | "github.com/BurntSushi/xgb" 10 | "github.com/BurntSushi/xgb/xproto" 11 | ) 12 | 13 | var xdpy *x11Display 14 | 15 | func moveTo(pt image.Point) error { 16 | d, err := getX11Display() 17 | if err != nil { 18 | return err 19 | } 20 | return xproto.WarpPointerChecked(d.conn, 21 | xproto.WindowNone, // src 22 | d.focus, // dst 23 | 0, 0, 0, 0, // src X, Y, width, height 24 | int16(pt.X), int16(pt.Y), // dst X, Y 25 | ).Check() 26 | } 27 | 28 | func setCursor(c *Cursor) error { 29 | d, err := getX11Display() 30 | if err != nil { 31 | return err 32 | } 33 | if c != nil { 34 | return d.setCursor(c) 35 | } 36 | return d.unsetCursor() 37 | } 38 | 39 | type x11Display struct { 40 | conn *xgb.Conn 41 | focus xproto.Window 42 | cursor xproto.Cursor // previously set cursor 43 | } 44 | 45 | func getX11Display() (*x11Display, error) { 46 | if xdpy == nil { 47 | conn, err := xgb.NewConn() 48 | if err != nil { 49 | return nil, err 50 | } 51 | xdpy = &x11Display{ 52 | conn: conn, 53 | cursor: xproto.CursorNone, 54 | } 55 | } 56 | // TODO(fhs): This is wrong if the window is not in focus. 57 | gif, err := xproto.GetInputFocus(xdpy.conn).Reply() 58 | if err != nil { 59 | return nil, err 60 | } 61 | xdpy.focus = gif.Focus 62 | return xdpy, nil 63 | } 64 | 65 | func (d *x11Display) freeCursor() { 66 | if d.cursor != xproto.CursorNone { 67 | xproto.FreeCursor(d.conn, d.cursor) 68 | d.cursor = xproto.CursorNone 69 | } 70 | } 71 | 72 | func (d *x11Display) unsetCursor() error { 73 | err := xproto.ChangeWindowAttributesChecked(d.conn, d.focus, 74 | xproto.CwCursor, []uint32{xproto.CursorNone}).Check() 75 | if err != nil { 76 | return fmt.Errorf("ChangeWindowAttributesChecked: %v", err) 77 | } 78 | d.freeCursor() 79 | return nil 80 | } 81 | 82 | func (d *x11Display) setCursor(c *Cursor) error { 83 | var src, mask [2 * 16]byte 84 | 85 | for i := 0; i < 2*16; i++ { 86 | src[i] = reverseByte(c.Set[i]) 87 | mask[i] = reverseByte(c.Set[i] | c.Clr[i]) 88 | } 89 | 90 | xsrc, err := createPixmapFromData(d.conn, xproto.Drawable(d.focus), src[:], 16, 16) 91 | if err != nil { 92 | return fmt.Errorf("createPixmapFromData xsrc: %v", err) 93 | } 94 | defer xproto.FreePixmap(d.conn, xsrc) 95 | xmask, err := createPixmapFromData(d.conn, xproto.Drawable(d.focus), mask[:], 16, 16) 96 | if err != nil { 97 | return fmt.Errorf("createPixmapFromData xmask: %v", err) 98 | } 99 | defer xproto.FreePixmap(d.conn, xmask) 100 | xc, err := xproto.NewCursorId(d.conn) 101 | if err != nil { 102 | return fmt.Errorf("NewCursorId: %v", err) 103 | } 104 | err = xproto.CreateCursorChecked(d.conn, xc, xsrc, xmask, 105 | 0, 0, 0, // foreRed, foreGreen, foreblue 106 | 0xffff, 0xffff, 0xffff, // backRed, backGreen, backBlue 107 | uint16(-c.Point.X), uint16(-c.Point.Y), 108 | ).Check() 109 | if err != nil { 110 | return fmt.Errorf("CreateCursorChecked: %v", err) 111 | } 112 | err = xproto.ChangeWindowAttributesChecked(d.conn, d.focus, 113 | xproto.CwCursor, []uint32{uint32(xc)}).Check() 114 | if err != nil { 115 | return fmt.Errorf("ChangeWindowAttributesChecked: %v", err) 116 | } 117 | d.freeCursor() 118 | d.cursor = xc 119 | return nil 120 | } 121 | 122 | func createPixmapFromData(conn *xgb.Conn, drawable xproto.Drawable, data []byte, width, height uint16) (xproto.Pixmap, error) { 123 | pm, err := xproto.NewPixmapId(conn) 124 | if err != nil { 125 | return 0, fmt.Errorf("NewPixmapId: %v", err) 126 | } 127 | err = xproto.CreatePixmapChecked(conn, 1, pm, drawable, width, height).Check() 128 | if err != nil { 129 | return 0, fmt.Errorf("CreatePixmapChecked: %v", err) 130 | } 131 | gc, err := xproto.NewGcontextId(conn) 132 | if err != nil { 133 | return 0, fmt.Errorf("NewGcontextId: %v", err) 134 | } 135 | err = xproto.CreateGCChecked(conn, gc, xproto.Drawable(pm), 136 | xproto.GcForeground|xproto.GcBackground, 137 | []uint32{1, 0}).Check() 138 | if err != nil { 139 | return 0, fmt.Errorf("CreateGCChecked: %v", err) 140 | } 141 | // TODO(fhs): Just guessing the pixmap binary layout, 142 | // but this seems to make the Edwood boxcursor work. 143 | data2 := make([]byte, 0, len(data)*2) 144 | for i := 0; i < len(data); i += 2 { 145 | data2 = append(data2, data[i:i+2]...) 146 | data2 = append(data2, data[i:i+2]...) 147 | } 148 | err = xproto.PutImageChecked(conn, xproto.ImageFormatXYPixmap, xproto.Drawable(pm), gc, 149 | width, height, 150 | 0, 0, // DstX, DstY 151 | 0, 1, // LeftPad, Depth 152 | data2, 153 | ).Check() 154 | if err != nil { 155 | return 0, fmt.Errorf("PutImageChecked: %v", err) 156 | } 157 | return pm, nil 158 | } 159 | 160 | func reverseByte(b byte) byte { 161 | var r byte 162 | 163 | r = 0 164 | r |= (b & 0x01) << 7 165 | r |= (b & 0x02) << 5 166 | r |= (b & 0x04) << 3 167 | r |= (b & 0x08) << 1 168 | r |= (b & 0x10) >> 1 169 | r |= (b & 0x20) >> 3 170 | r |= (b & 0x40) >> 5 171 | r |= (b & 0x80) >> 7 172 | return r 173 | } 174 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | "sync" 9 | 10 | "golang.org/x/exp/shiny/imageutil" 11 | "golang.org/x/image/font" 12 | "golang.org/x/image/math/fixed" 13 | ) 14 | 15 | type Image struct { 16 | sync.Mutex // Locking is used only internally. 17 | R image.Rectangle // The extent of the image. 18 | m image.Image 19 | Display *Display 20 | Pix Pix // The pixel format for the image. 21 | } 22 | 23 | // MakeImage returns an Image from an *image.RGBA. 24 | func (d *Display) MakeImage(m *image.RGBA) *Image { 25 | return &Image{ 26 | Display: d, 27 | R: m.Bounds(), 28 | m: m, 29 | } 30 | } 31 | 32 | // DrawImage draws a normal image.Image over dst. 33 | // Image should implement draw.Image, but it currently doesn't. 34 | // This cannot be changed now, as Draw is already defined. 35 | func (dst *Image) DrawImage(r image.Rectangle, src image.Image, pt image.Point, op draw.Op) { 36 | dst.Lock() 37 | defer dst.Unlock() 38 | if m, ok := dst.m.(draw.Image); ok { 39 | draw.Draw(m, r, src, pt, op) 40 | } 41 | } 42 | 43 | // Draw copies the source image with upper left corner p1 to the destination 44 | // rectangle r, through the specified mask using operation SoverD. The 45 | // coordinates are aligned so p1 in src and mask both correspond to r.min in 46 | // the destination. 47 | func (dst *Image) Draw(r image.Rectangle, src, mask *Image, p1 image.Point) { 48 | dst.Lock() 49 | defer dst.Unlock() 50 | 51 | var im draw.Image 52 | switch m := dst.m.(type) { 53 | case *image.RGBA: 54 | im = m 55 | default: 56 | fmt.Printf("shiny: image dst not implemented %T\n", m) 57 | return 58 | } 59 | 60 | if src == nil { 61 | fmt.Println("shiny: Draw: src is nil") 62 | return 63 | } 64 | if mask == nil { 65 | draw.Draw(im, r, src.m, p1, draw.Src) 66 | return 67 | } 68 | draw.DrawMask(im, r, src.m, p1, mask.m, p1, draw.Over) 69 | } 70 | 71 | // Border draws a retangular border of size r and width n, with n positive 72 | // meaning the border is inside r. It uses SoverD. 73 | func (dst *Image) Border(r image.Rectangle, n int, src *Image, sp image.Point) { 74 | dst.Lock() 75 | defer dst.Unlock() 76 | 77 | for _, r := range imageutil.Border(r, n) { 78 | draw.Draw(dst.m.(*image.RGBA), r, src.m, sp, draw.Src) 79 | } 80 | } 81 | 82 | // Free is currently ignored. 83 | // TODO: do we need anything about this? 84 | func (i *Image) Free() error { 85 | return nil 86 | } 87 | 88 | // Load copies the pixel data from the buffer to the specified rectangle of the image. 89 | // The buffer must be big enough to fill the rectangle. 90 | // 91 | // Duit calls Load with Load(rgba.Bounds(), rgba.Pix), so we assume image.RGBA Pix data. 92 | func (dst *Image) Load(r image.Rectangle, data []byte) (int, error) { 93 | w, h := r.Dx(), r.Dy() 94 | if len(data) != 4*w*h { 95 | return 0, fmt.Errorf("image Load: wrong data size") 96 | } 97 | m := &image.RGBA{data, 4 * w, r} 98 | 99 | dst.R = r 100 | dst.m = m 101 | 102 | // Is len(data) ok? Duit does not read the first argument anyway. 103 | return len(data), nil 104 | } 105 | 106 | func (dst *Image) Bytes(pt image.Point, src *Image, sp image.Point, f *Font, b []byte) image.Point { 107 | return dst.String(pt, src, sp, f, string(b)) 108 | } 109 | 110 | // String draws the string in the specified font using SoverD on the image, 111 | // placing the upper left corner at p. 112 | func (dst *Image) String(pt image.Point, src *Image, sp image.Point, f *Font, s string) image.Point { 113 | dst.Lock() 114 | defer dst.Unlock() 115 | 116 | m := dst.m.(*image.RGBA) 117 | ascent := f.face.Metrics().Ascent 118 | dot := fixed.P(pt.X, pt.Y).Add(fixed.Point26_6{Y: ascent}) 119 | 120 | drawer := font.Drawer{ 121 | Dst: m, 122 | Src: src.m, 123 | Face: f.face, 124 | Dot: dot, 125 | } 126 | drawer.DrawString(s) 127 | dx := int(drawer.Dot.Sub(dot).X / 64) 128 | ret := pt.Add(image.Point{dx, 0}) 129 | 130 | // fmt.Printf("shiny: String(%s) to %p at %d,%d => %d %d\n", s, m, pt.X, pt.Y, ret.X, ret.Y) // TODO remove 131 | return ret 132 | } 133 | 134 | // Line draws a line in the source color from p0 to p1, of thickness 135 | // 1+2*radius, with the specified ends, using SoverD. The source is aligned so 136 | // sp corresponds to p0. See the Plan 9 documentation for more information. 137 | func (dst *Image) Line(p0, p1 image.Point, end0, end1, radius int, src *Image, sp image.Point) { 138 | dst.Lock() 139 | defer dst.Unlock() 140 | 141 | line(dst.m.(*image.RGBA), p0.X, p0.Y, p1.X, p1.Y, src.m.At(0, 0)) 142 | } 143 | 144 | // Line draws a line with Besenham's algorithm. 145 | // It only uses integer pixels. 146 | func line(m *image.RGBA, x0, y0, x1, y1 int, c color.Color) { 147 | abs := func(x int) int { 148 | if x < 0 { 149 | return -x 150 | } 151 | return x 152 | } 153 | 154 | var dx, dy, sx, sy, e, e2 int 155 | 156 | dx = abs(x1 - x0) 157 | dy = -abs(y1 - y0) 158 | if sx = -1; x0 < x1 { 159 | sx = 1 160 | } 161 | if sy = -1; y0 < y1 { 162 | sy = 1 163 | } 164 | e = dx + dy 165 | for { 166 | m.Set(x0, y0, c) 167 | if x0 == x1 && y0 == y1 { 168 | break 169 | } 170 | if e2 = 2 * e; e2 >= dy { 171 | e += dy 172 | x0 += sx 173 | } else if e2 <= dx { 174 | e += dx 175 | y0 += sy 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /ellipse.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | ) 8 | 9 | // How to draw ellipses? 10 | // As I understand, low level rasterizers don't have a function to draw arcs directly, you need to create the paths before. 11 | // 12 | // This is how srwiley does it in oksvg: 13 | // https://github.com/srwiley/oksvg/blob/bb18c2355556cc1db9c11f026bfe350deab1a482/svgp.go#L321 14 | // 15 | // This is what I did before to draw full circles: 16 | /* 17 | type circle struct { 18 | x, y, r fixed.Int26_6 19 | } 20 | 21 | // getPath approximates a circle by 8 quadrativ curve segments. 22 | func (c circle) getPath() raster.Path { 23 | d := fixed.Point26_6{c.x, c.y} 24 | r := c.r 25 | s := fixed.Int26_6(float64(c.r) * math.Sqrt(2.0) / 2.0) 26 | t := fixed.Int26_6(float64(c.r) * math.Tan(math.Pi/8)) 27 | P := func(x, y fixed.Int26_6) fixed.Point26_6 { 28 | return fixed.Point26_6{x, y} 29 | } 30 | var path raster.Path 31 | path.Start(d.Add(P(r, 0))) 32 | path.Add2(d.Add(P(r, t)), d.Add(P(s, s))) 33 | path.Add2(d.Add(P(t, r)), d.Add(P(0, r))) 34 | path.Add2(d.Add(P(-t, r)), d.Add(P(-s, s))) 35 | path.Add2(d.Add(P(-r, t)), d.Add(P(-r, 0))) 36 | path.Add2(d.Add(P(-r, -t)), d.Add(P(-s, -s))) 37 | path.Add2(d.Add(P(-t, -r)), d.Add(P(0, -r))) 38 | path.Add2(d.Add(P(t, -r)), d.Add(P(s, -s))) 39 | path.Add2(d.Add(P(r, -t)), d.Add(P(r, 0))) 40 | return path 41 | } 42 | */ 43 | // How accurate does it have to be anyway? 44 | // Is it used only for small segments (like 3 pixel edges to input boxes)? 45 | 46 | // Arc draws, using SoverD, the arc centered at c, with thickness 1+2*thick, 47 | // using the specified source color. The arc starts at angle alpha and extends 48 | // counterclockwise by phi; angles are measured in degrees from the x axis. 49 | func (dst *Image) Arc(c image.Point, a, b, thick int, src *Image, sp image.Point, alpha, phi int) { 50 | // doellipse('e', dst, c, a, b, thick, src, sp, uint32(alpha)|1<<31, phi, SoverD) 51 | // 52 | // Plan9 draw(3): 53 | // ellipse(dst, c, a, b, thick, src, sp) 54 | // Ellipse draws in dst an ellipse centered on c with horizontal 55 | // and vertical semiaxes a and b. 56 | // The source is aligned so sp in src corresponds to c in dst. 57 | // The ellipse is drawn with thickness 1+2*thick. 58 | // 59 | // arc(dst, c, a, b, thick, src, sp, alpha, phi) 60 | // Arc is like ellipse, but draws only that portion of the ellipse 61 | // starting at angle alpha and extending through an angle of phi. 62 | // The angles are measured in degrees counterclockwise from the positive x axis. 63 | 64 | // For full circles we assume that a==b and ignore thick. 65 | if alpha == 0 && phi == 360 { 66 | dst.Lock() 67 | defer dst.Unlock() 68 | drawCircle(dst.m.(*image.RGBA), c.X, c.Y, a, src.m.At(0, 0)) 69 | return 70 | } 71 | 72 | // We assume duit only calls with phi=90 and alpha: 0, 90, 180, 270. 73 | var p0, p1 image.Point 74 | switch alpha { 75 | case 0: 76 | p0 = image.Point{a, 0} 77 | p1 = image.Point{0, -b} 78 | case 90: 79 | p0 = image.Point{0, -b} 80 | p1 = image.Point{-a, 0} 81 | case 180: 82 | p0 = image.Point{-a, 0} 83 | p1 = image.Point{0, b} 84 | case 270: 85 | p0 = image.Point{0, b} 86 | p1 = image.Point{a, 0} 87 | } 88 | 89 | // We just draw a line. 90 | dst.Line(p0.Add(c), p1.Add(c), 0, 0, 0, src, sp) 91 | 92 | // fmt.Printf("Arc: a=%d b=%d thick=%d alpha=%d phi=%d\n", a, b, thick, alpha, phi) 93 | } 94 | 95 | // FillArc draws and fills, using SoverD, the arc centered at c, with thickness 96 | // 1+2*thick, using the specified source color. The arc starts at angle alpha 97 | // and extends counterclockwise by phi; angles are measured in degrees from the 98 | // x axis. 99 | func (dst *Image) FillArc(c image.Point, a, b, thick int, src *Image, sp image.Point, alpha, phi int) { 100 | //doellipse('E', dst, c, a, b, thick, src, sp, uint32(alpha)|1<<31, phi, SoverD) 101 | 102 | // For full arcs, we assume a==b and ignore thick. 103 | if alpha == 0 && phi == 360 { 104 | dst.Lock() 105 | defer dst.Unlock() 106 | fillCircle(dst.m.(*image.RGBA), c.X, c.Y, a, src.m) 107 | } 108 | } 109 | 110 | // drawCircle is a simple rasterizer for a circle with integer pixel coordinates and a thin border. 111 | func drawCircle(im draw.Image, xm, ym, r int, c color.Color) { 112 | var x, y, e int 113 | x = -r 114 | e = 2 - 2*r 115 | for x < 0 { 116 | im.Set(xm-x, ym+y, c) 117 | im.Set(xm-y, ym-x, c) 118 | im.Set(xm+x, ym-y, c) 119 | im.Set(xm+y, ym+x, c) 120 | r = e 121 | if r <= y { 122 | y++ 123 | e += 2*y + 1 124 | } 125 | if r > x || e > y { 126 | x++ 127 | e += 2*x + 1 128 | } 129 | } 130 | } 131 | 132 | // fillCircle fills a circle using a mask. 133 | func fillCircle(im draw.Image, xm, ym, r int, src image.Image) { 134 | draw.DrawMask(im, im.Bounds(), src, image.ZP, &circle{image.Point{xm, ym}, r}, image.ZP, draw.Over) 135 | } 136 | 137 | type circle struct { 138 | p image.Point 139 | r int 140 | } 141 | 142 | func (c *circle) ColorModel() color.Model { 143 | return color.AlphaModel 144 | } 145 | func (c *circle) Bounds() image.Rectangle { 146 | return image.Rect(c.p.X-c.r-1, c.p.Y-c.r-1, c.p.X+c.r+1, c.p.Y+c.r+1) 147 | } 148 | func (c *circle) At(x, y int) color.Color { 149 | xx, yy, rr := float64(x-c.p.X), float64(y-c.p.Y), float64(c.r)+0.5 150 | if xx*xx+yy*yy < rr*rr { 151 | return color.Alpha{255} 152 | } 153 | return color.Alpha{0} 154 | } 155 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "io" 8 | "log" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | 14 | "golang.org/x/exp/shiny/driver" 15 | "golang.org/x/exp/shiny/screen" 16 | ) 17 | 18 | // mainScreen stores the screen which is initialized for the first window. 19 | var mainScreen screen.Screen 20 | 21 | // Main is called by the program's main function to run the graphical 22 | // application. 23 | // 24 | // It calls f on the Device, possibly in a separate goroutine, as some OS- 25 | // specific libraries require being on 'the main thread'. It returns when f 26 | // returns. 27 | func Main(f func(*Device)) { 28 | driver.Main(func(ss screen.Screen) { 29 | dev := newDevice(ss) 30 | f(dev) 31 | dev.wait() 32 | }) 33 | } 34 | 35 | // Device represents the draw device on which multiple windows 36 | // can be created. 37 | type Device struct { 38 | wg sync.WaitGroup 39 | } 40 | 41 | func newDevice(ss screen.Screen) *Device { 42 | mainScreen = ss 43 | return &Device{} 44 | } 45 | 46 | // NewDisplay is called to create a new window. 47 | // There is no special mechanism to create the first window. 48 | func (dev *Device) NewDisplay(errch chan<- error, fontname, label, winsize string) (*Display, error) { 49 | if errch == nil { 50 | setDefaultErrorChan() 51 | errch = defaultErrorChan 52 | } 53 | dpy, opt := newDisplay(label, winsize, fontname) 54 | dev.wg.Add(1) 55 | go func() { 56 | createWindow(dpy, opt, errch) 57 | dev.wg.Done() 58 | }() 59 | 60 | // make sure ScreenImage buffer is allocated 61 | <-dpy.mouse.Resize 62 | 63 | return dpy, nil 64 | } 65 | 66 | func (dev *Device) wait() { 67 | dev.wg.Wait() 68 | } 69 | 70 | // Init is called to create a new window. 71 | // There is no special mechanism to create the first window. 72 | // This function does not work on systems (e.g. macOS) where the 73 | // OS-specific graphics libraries require being on 'the main thread'. 74 | // Use Main for best compatiblity. 75 | func Init(errch chan<- error, fontname, label, winsize string) (*Display, error) { 76 | if errch == nil { 77 | setDefaultErrorChan() 78 | errch = defaultErrorChan 79 | } 80 | if mainScreen == nil { 81 | dpy, opt := newDisplay(label, winsize, fontname) 82 | go driver.Main(func(s screen.Screen) { 83 | mainScreen = s 84 | createWindow(dpy, opt, errch) 85 | }) 86 | // make sure ScreenImage buffer is allocated 87 | <-dpy.mouse.Resize 88 | return dpy, nil 89 | } else { 90 | dpy, opt := newDisplay(label, winsize, fontname) 91 | go createWindow(dpy, opt, errch) 92 | // make sure ScreenImage buffer is allocated 93 | <-dpy.mouse.Resize 94 | return dpy, nil 95 | } 96 | } 97 | 98 | // NewDisplay creates a Display with it's mouse and keyboard channels. 99 | // It registers the window in mainScreen but does not call any shiny functions. 100 | func newDisplay(label, winsize, fontname string) (*Display, screen.NewWindowOptions) { 101 | opt := screen.NewWindowOptions{ 102 | Width: 800, 103 | Height: 800, 104 | Title: label, 105 | } 106 | if wh := strings.Split(winsize, "x"); len(wh) == 2 { 107 | if w, err := strconv.Atoi(wh[0]); err == nil { 108 | if h, err := strconv.Atoi(wh[1]); err == nil { 109 | opt.Width = w 110 | opt.Height = h 111 | } 112 | } 113 | } 114 | 115 | dpy := Display{ 116 | DPI: DefaultDPI, 117 | } 118 | dpy.Black = &Image{ 119 | Display: &dpy, 120 | R: image.Rect(0, 0, 1, 1), 121 | m: image.NewUniform(color.Black), 122 | } 123 | dpy.White = &Image{ 124 | Display: &dpy, 125 | R: image.Rect(0, 0, 1, 1), 126 | m: image.NewUniform(color.White), 127 | } 128 | dpy.Opaque = &Image{ 129 | Display: &dpy, 130 | R: image.Rect(0, 0, 1, 1), 131 | m: image.NewUniform(color.Opaque), 132 | } 133 | dpy.Transparent = &Image{ 134 | Display: &dpy, 135 | R: image.Rect(0, 0, 1, 1), 136 | m: image.NewUniform(color.Transparent), 137 | } 138 | dpy.ScreenImage = &Image{ 139 | Display: &dpy, 140 | R: image.Rect(0, 0, opt.Width, opt.Height), 141 | // m will be backed by screen.Buffer on size event. 142 | } 143 | if f, err := dpy.OpenFont(fontname); err != nil { 144 | dpy.DefaultFont = defaultFont 145 | log.Print(err) 146 | } else { 147 | dpy.DefaultFont = f 148 | } 149 | dpy.mouse.C = make(chan Mouse, 0) 150 | dpy.mouse.Resize = make(chan bool, 2) // Why 2? (copied from InitMouse). 151 | dpy.mouse.Display = &dpy 152 | dpy.keyboard.C = make(chan rune, 20) 153 | 154 | return &dpy, opt 155 | } 156 | 157 | // CreateWindow creates a new client window and runs it. 158 | // The function is called inside a go routine and is alive as long as the window is present. 159 | func createWindow(d *Display, opt screen.NewWindowOptions, errch chan<- error) { 160 | w, err := mainScreen.NewWindow(&opt) 161 | if err != nil { 162 | fmt.Printf("shiny: NewWindow error: %s\n", err) 163 | errch <- err 164 | return 165 | } 166 | defer w.Release() 167 | 168 | var b screen.Buffer 169 | defer func() { 170 | if b != nil { 171 | b.Release() 172 | } 173 | }() 174 | 175 | d.window = w 176 | d.buffer = b 177 | d.eventLoop(errch) 178 | } 179 | 180 | var ( 181 | defaultErrorChan chan<- error 182 | defaultErrorChanOnce sync.Once 183 | ) 184 | 185 | func setDefaultErrorChan() { 186 | defaultErrorChanOnce.Do(func() { 187 | ch := make(chan error) 188 | go func() { 189 | for err := range ch { 190 | if err != io.EOF { 191 | fmt.Fprintf(os.Stderr, "duitdraw: %v\n", err) 192 | } 193 | } 194 | }() 195 | defaultErrorChan = ch 196 | }) 197 | } 198 | -------------------------------------------------------------------------------- /font.go: -------------------------------------------------------------------------------- 1 | package duitdraw 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "io/ioutil" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/golang/freetype/truetype" 13 | "golang.org/x/image/font" 14 | "golang.org/x/image/font/gofont/goregular" 15 | "golang.org/x/image/font/plan9font" 16 | "golang.org/x/image/math/fixed" 17 | ) 18 | 19 | type Font struct { 20 | FaceID // This field is not present in 9fans draw package. 21 | Height int 22 | face font.Face 23 | } 24 | 25 | type FaceID struct { 26 | Name string 27 | Size int 28 | DPI int 29 | } 30 | 31 | // FaceCache stores font.Faces. 32 | type FaceCache struct { 33 | sync.Mutex 34 | m map[FaceID]font.Face 35 | } 36 | 37 | var faceCache FaceCache 38 | 39 | // OpenFont opens a font with a given name and an optional size. 40 | // Currently Plan 9 bitmap fonts and truetype fonts are supported. 41 | // Plan 9 font must have filename or extension "font" and truetype font 42 | // have the syntax "/path/to/font.ttf@12pt". 43 | func (d *Display) OpenFont(name string) (*Font, error) { 44 | if s := strings.ToLower(name); filepath.Base(s) == "font" || filepath.Ext(s) == ".font" { 45 | return openPlan9Font(FaceID{Name: name}) 46 | } 47 | size := DefaultFontSize 48 | if idx := strings.LastIndex(name, "@"); idx != -1 { 49 | ext := name[idx+1:] 50 | ext = strings.TrimSuffix(ext, "pt") 51 | if n, err := strconv.Atoi(ext); err != nil { 52 | return nil, fmt.Errorf("OpenFont: cannot parse font size: %s", name) 53 | } else { 54 | size = n 55 | } 56 | name = name[:idx] 57 | } 58 | return openFont(FaceID{Name: name, Size: size, DPI: d.DPI}) 59 | } 60 | 61 | // RegisterFont adds a font face to the font cache. 62 | func RegisterFont(id FaceID, face font.Face) { 63 | faceCache.Lock() 64 | defer faceCache.Unlock() 65 | faceCache.m[id] = face 66 | } 67 | 68 | // OpenFont loads a font from fontCache, from Disk or returns GoRegular 69 | // if the font name is empty. 70 | func openFont(id FaceID) (*Font, error) { 71 | faceCache.Lock() 72 | defer faceCache.Unlock() 73 | if f, ok := faceCache.m[id]; ok { 74 | m := f.Metrics() 75 | // TODO(fhs): Remove workaround for wrong m.Height. 76 | return &Font{ 77 | FaceID: id, 78 | Height: (m.Ascent + m.Descent).Round(), 79 | face: f, 80 | }, nil 81 | } 82 | 83 | var ttf []byte 84 | if id.Name == "" { 85 | ttf = goregular.TTF 86 | } else { 87 | if b, err := ioutil.ReadFile(id.Name); err != nil { 88 | return nil, err 89 | } else { 90 | ttf = b 91 | } 92 | } 93 | 94 | if f, err := truetype.Parse(ttf); err != nil { 95 | return nil, fmt.Errorf("%s: %s", id.Name, err) 96 | } else { 97 | opt := truetype.Options{ 98 | Size: float64(id.Size), 99 | DPI: float64(id.DPI), 100 | } 101 | face := pixFace{Face: truetype.NewFace(f, &opt)} 102 | faceCache.m[id] = face 103 | 104 | m := face.Metrics() 105 | // TODO(fhs): Remove workaround for wrong m.Height. 106 | return &Font{ 107 | FaceID: id, 108 | Height: (m.Ascent + m.Descent).Round(), 109 | face: face, 110 | }, nil 111 | } 112 | 113 | /* TODO: use sfnt/opentype, when it's finished. 114 | f, err := sfnt.Parse(ttf) 115 | opt := opentype.FaceOptions{ 116 | Size: 12, 117 | DPI: 72, 118 | Hinting: font.HintingNone, 119 | } 120 | face, err := opentype.NewFace(f, &opt) 121 | */ 122 | } 123 | 124 | func openPlan9Font(id FaceID) (*Font, error) { 125 | faceCache.Lock() 126 | defer faceCache.Unlock() 127 | if f, ok := faceCache.m[id]; ok { 128 | return &Font{ 129 | FaceID: id, 130 | Height: f.Metrics().Height.Round(), 131 | face: f, 132 | }, nil 133 | } 134 | 135 | fontData, err := ioutil.ReadFile(id.Name) 136 | if err != nil { 137 | return nil, err 138 | } 139 | dir := filepath.Dir(id.Name) 140 | face, err := plan9font.ParseFont(fontData, func(name string) ([]byte, error) { 141 | return ioutil.ReadFile(filepath.Join(dir, name)) 142 | }) 143 | if err != nil { 144 | return nil, err 145 | } 146 | faceCache.m[id] = face 147 | 148 | return &Font{ 149 | FaceID: id, 150 | Height: face.Metrics().Height.Round(), 151 | face: face, 152 | }, nil 153 | } 154 | 155 | func (f *Font) SetDPI(dpi int) *Font { 156 | id := f.FaceID 157 | id.DPI = dpi 158 | if font, err := openFont(id); err != nil { 159 | return f 160 | } else { 161 | return font 162 | } 163 | } 164 | 165 | func (f Font) StringSize(s string) image.Point { 166 | dx := f.StringWidth(s) 167 | dy := f.Height 168 | return image.Point{dx, dy} 169 | } 170 | 171 | // StringWidth returns the number of horizontal pixels that would be occupied 172 | // by the string if it were drawn using the font. 173 | func (f *Font) StringWidth(s string) int { 174 | dx := 0 175 | for _, c := range s { 176 | a, ok := f.face.GlyphAdvance(c) 177 | if ok { 178 | dx += a.Round() 179 | } 180 | } 181 | return dx 182 | } 183 | 184 | // ByteWidth returns the number of horizontal pixels that would be occupied by 185 | // the byte slice if it were drawn using the font. 186 | func (f *Font) BytesWidth(b []byte) int { 187 | return f.StringWidth(string(b)) 188 | } 189 | 190 | // RuneWidth returns the number of horizontal pixels that would be occupied by 191 | // the rune slice if it were drawn using the font. 192 | func (f *Font) RunesWidth(r []rune) int { 193 | return f.StringWidth(string(r)) 194 | } 195 | 196 | // pixFace wraps a font.Face which ignores Kern and advances only by full pixels. 197 | // Duit calls StringWidth on each rune to calculate coordinates and uses only ints. 198 | type pixFace struct { 199 | font.Face 200 | } 201 | 202 | func (f pixFace) Kern(r0, r1 rune) fixed.Int26_6 { 203 | return 0 204 | } 205 | 206 | func (f pixFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) { 207 | dr, mask, maskp, advance, ok = f.Face.Glyph(dot, r) 208 | advance = 64 * fixed.Int26_6(int(advance+32)/64) 209 | return 210 | } 211 | 212 | func (f pixFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) { 213 | bounds, advance, ok = f.Face.GlyphBounds(r) 214 | advance = 64 * fixed.Int26_6(int(advance+32)/64) 215 | return 216 | } 217 | 218 | // defaultFont is used for new Displays. 219 | // It is GoRegular at DefaultSize for DefaultDPI. 220 | var defaultFont *Font 221 | 222 | func init() { 223 | // DefaultFont is GoRegular which is built-in. 224 | faceCache.m = make(map[FaceID]font.Face) 225 | id := FaceID{ 226 | Name: "", 227 | Size: DefaultFontSize, 228 | DPI: DefaultDPI, 229 | } 230 | var err error 231 | defaultFont, err = openFont(id) 232 | if err != nil { 233 | panic(err) 234 | } 235 | faceCache.m[id] = defaultFont.face 236 | } 237 | -------------------------------------------------------------------------------- /snarf_unix.go: -------------------------------------------------------------------------------- 1 | // This is a modified version of the X11 clipboard implementation in nucular 2 | // (https://github.com/aarzilli/nucular/blob/7a48478aebff2ca5dadd95d52d34be4cb24af4ec/clipboard/clipboard_linux.go) 3 | // distributed under the following license: 4 | // 5 | // The MIT License (MIT) 6 | // 7 | // Copyright (c) 2016 Alessandro Arzilli 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 10 | // this software and associated documentation files (the "Software"), to deal in 11 | // the Software without restriction, including without limitation the rights to 12 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 13 | // the Software, and to permit persons to whom the Software is furnished to do so, 14 | // subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in all 17 | // copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 21 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 22 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 23 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 24 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | // +build freebsd linux,!android netbsd openbsd solaris dragonfly 27 | 28 | package duitdraw 29 | 30 | import ( 31 | "fmt" 32 | "os" 33 | "sync" 34 | "time" 35 | 36 | "github.com/BurntSushi/xgb" 37 | "github.com/BurntSushi/xgb/xproto" 38 | ) 39 | 40 | func (d *Display) readSnarf(buf []byte) (int, int, error) { 41 | xc, err := getXClip() 42 | if err != nil { 43 | return 0, 0, err 44 | } 45 | xc.mu.Lock() 46 | defer xc.mu.Unlock() 47 | 48 | n, size, err := xc.getSelection(primaryAtom, buf) 49 | if size > 0 { 50 | return n, size, err 51 | } 52 | return xc.getSelection(clipboardAtom, buf) 53 | } 54 | 55 | func (d *Display) writeSnarf(text []byte) error { 56 | xc, err := getXClip() 57 | if err != nil { 58 | return err 59 | } 60 | xc.mu.Lock() 61 | defer xc.mu.Unlock() 62 | 63 | xc.text = text 64 | ssoc := xproto.SetSelectionOwnerChecked(xc.conn, xc.win, clipboardAtom, xproto.TimeCurrentTime) 65 | if err := ssoc.Check(); err != nil { 66 | return fmt.Errorf("error setting clipboard: %v", err) 67 | } 68 | ssoc = xproto.SetSelectionOwnerChecked(xc.conn, xc.win, primaryAtom, xproto.TimeCurrentTime) 69 | if err := ssoc.Check(); err != nil { 70 | return fmt.Errorf("error setting primary selection: %v", err) 71 | } 72 | return nil 73 | } 74 | 75 | const debugClipboardRequests = false 76 | 77 | type xClip struct { 78 | conn *xgb.Conn 79 | win xproto.Window 80 | text []byte 81 | selnotify chan bool 82 | err error 83 | mu sync.Mutex 84 | } 85 | 86 | var clipboardAtom, primaryAtom, textAtom, targetsAtom, atomAtom xproto.Atom 87 | 88 | var ( 89 | xclip *xClip 90 | xclipOnce sync.Once 91 | ) 92 | 93 | func getXClip() (*xClip, error) { 94 | xclipOnce.Do(func() { 95 | var xc xClip 96 | xclip = &xc 97 | 98 | xc.conn, xc.err = xgb.NewConnDisplay("") 99 | if xc.err != nil { 100 | return 101 | } 102 | 103 | xc.selnotify = make(chan bool, 1) 104 | 105 | xc.win, xc.err = xproto.NewWindowId(xc.conn) 106 | if xc.err != nil { 107 | return 108 | } 109 | 110 | setup := xproto.Setup(xc.conn) 111 | s := setup.DefaultScreen(xc.conn) 112 | xc.err = xproto.CreateWindowChecked(xc.conn, s.RootDepth, xc.win, s.Root, 100, 100, 1, 1, 0, xproto.WindowClassInputOutput, s.RootVisual, 0, []uint32{}).Check() 113 | if xc.err != nil { 114 | return 115 | } 116 | 117 | clipboardAtom = xc.internAtom("CLIPBOARD") 118 | primaryAtom = xc.internAtom("PRIMARY") 119 | textAtom = xc.internAtom("UTF8_STRING") 120 | targetsAtom = xc.internAtom("TARGETS") 121 | atomAtom = xc.internAtom("ATOM") 122 | 123 | go xc.eventLoop() 124 | }) 125 | return xclip, xclip.err 126 | } 127 | 128 | func (xc *xClip) setError(err error) { 129 | if xc.err == nil && err != nil { 130 | xc.err = err 131 | } 132 | } 133 | 134 | func (xc *xClip) getSelection(selAtom xproto.Atom, buf []byte) (int, int, error) { 135 | err := xproto.ConvertSelectionChecked(xc.conn, xc.win, selAtom, textAtom, selAtom, xproto.TimeCurrentTime).Check() 136 | if err != nil { 137 | return 0, 0, err 138 | } 139 | 140 | select { 141 | case r := <-xc.selnotify: 142 | if !r { 143 | return 0, 0, fmt.Errorf("bad response from selection owner") 144 | } 145 | gpr, err := xproto.GetProperty(xc.conn, true, xc.win, selAtom, textAtom, 0, uint32(len(buf))).Reply() 146 | if err != nil { 147 | return 0, 0, err 148 | } 149 | n := copy(buf, gpr.Value[:gpr.ValueLen]) 150 | if n < int(gpr.ValueLen) || gpr.BytesAfter != 0 { 151 | return n, int(gpr.ValueLen + gpr.BytesAfter), errShortSnarfBuffer 152 | } 153 | return n, n, nil 154 | case <-time.After(1 * time.Second): 155 | return 0, 0, fmt.Errorf("clipboard retrieval failed, timeout") 156 | } 157 | } 158 | 159 | func (xc *xClip) eventLoop() { 160 | targetAtoms := []xproto.Atom{targetsAtom, textAtom} 161 | 162 | for { 163 | e, err := xc.conn.WaitForEvent() 164 | if err != nil { 165 | continue 166 | } 167 | 168 | switch e := e.(type) { 169 | case xproto.SelectionRequestEvent: // write snarf 170 | if debugClipboardRequests { 171 | tgtname := xc.lookupAtom(e.Target) 172 | fmt.Fprintln(os.Stderr, "SelectionRequest", e, textAtom, tgtname, "isPrimary:", e.Selection == primaryAtom, "isClipboard:", e.Selection == clipboardAtom) 173 | } 174 | t := xc.text 175 | 176 | switch e.Target { 177 | case textAtom: 178 | if debugClipboardRequests { 179 | fmt.Fprintln(os.Stderr, "Sending as text") 180 | } 181 | err := xproto.ChangePropertyChecked(xc.conn, xproto.PropModeReplace, e.Requestor, e.Property, textAtom, 8, uint32(len(t)), []byte(t)).Check() 182 | if err == nil { 183 | xc.sendSelectionNotify(e) 184 | } else { 185 | fmt.Fprintf(os.Stderr, "duitdraw: %v\n", err) 186 | } 187 | 188 | case targetsAtom: 189 | if debugClipboardRequests { 190 | fmt.Fprintln(os.Stderr, "Sending targets") 191 | } 192 | buf := make([]byte, len(targetAtoms)*4) 193 | for i, atom := range targetAtoms { 194 | xgb.Put32(buf[i*4:], uint32(atom)) 195 | } 196 | 197 | xproto.ChangePropertyChecked(xc.conn, xproto.PropModeReplace, e.Requestor, e.Property, atomAtom, 32, uint32(len(targetAtoms)), buf).Check() 198 | if err == nil { 199 | xc.sendSelectionNotify(e) 200 | } else { 201 | fmt.Fprintf(os.Stderr, "duitdraw: %v\n", err) 202 | } 203 | 204 | default: 205 | if debugClipboardRequests { 206 | fmt.Fprintln(os.Stderr, "Skipping") 207 | } 208 | e.Property = 0 209 | xc.sendSelectionNotify(e) 210 | } 211 | 212 | case xproto.SelectionNotifyEvent: // read snarf 213 | xc.selnotify <- (e.Property == clipboardAtom) || (e.Property == primaryAtom) 214 | } 215 | } 216 | } 217 | 218 | func (xc *xClip) sendSelectionNotify(e xproto.SelectionRequestEvent) { 219 | sn := xproto.SelectionNotifyEvent{ 220 | Time: xproto.TimeCurrentTime, 221 | Requestor: e.Requestor, 222 | Selection: e.Selection, 223 | Target: e.Target, 224 | Property: e.Property, 225 | } 226 | err := xproto.SendEventChecked(xc.conn, false, e.Requestor, 0, string(sn.Bytes())).Check() 227 | if err != nil { 228 | fmt.Fprintf(os.Stderr, "duitdraw: %v\n", err) 229 | } 230 | } 231 | 232 | func (xc *xClip) internAtom(n string) xproto.Atom { 233 | iar, err := xproto.InternAtom(xc.conn, true, uint16(len(n)), n).Reply() 234 | xc.setError(err) 235 | return iar.Atom 236 | } 237 | 238 | func (xc *xClip) lookupAtom(at xproto.Atom) string { 239 | reply, err := xproto.GetAtomName(xc.conn, at).Reply() 240 | if err != nil { 241 | panic(err) 242 | } 243 | return string(reply.Name) 244 | } 245 | --------------------------------------------------------------------------------