From 5e7c666d9293214e9542d83afb7fb46e4dac05bd Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 2 Jul 2023 02:52:14 -0400 Subject: [PATCH] Initial commit --- README.md | 3 + backend.go | 80 ++++++++++++ box.go | 228 ++++++++++++++++++++++++++++++++ canvas/canvas.go | 96 ++++++++++++++ canvasbox.go | 27 ++++ containerbox.go | 8 ++ go.mod | 17 +++ go.sum | 66 ++++++++++ system.go | 20 +++ textbox.go | 8 ++ window.go | 329 +++++++++++++++++++++++++++++++++++++++++++++++ x/plugin.go | 17 +++ 12 files changed, 899 insertions(+) create mode 100644 README.md create mode 100644 backend.go create mode 100644 box.go create mode 100644 canvas/canvas.go create mode 100644 canvasbox.go create mode 100644 containerbox.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 system.go create mode 100644 textbox.go create mode 100644 window.go create mode 100644 x/plugin.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca745f8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# x + +WIP X backend for Tomo. diff --git a/backend.go b/backend.go new file mode 100644 index 0000000..5a1f17c --- /dev/null +++ b/backend.go @@ -0,0 +1,80 @@ +package x + +import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/xgbkb" + +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/keybind" +import "github.com/jezek/xgbutil/mousebind" + +type Backend struct { + x *xgbutil.XUtil + doChannel chan func() + windows map[xproto.Window] *window + open bool +} + +func NewBackend () (tomo.Backend, error) { + backend := &Backend { + doChannel: make(chan func (), 32), + windows: make(map[xproto.Window] *window), + open: true, + } + + var err error + backend.x, err = xgbutil.NewConn() + if err != nil { return nil, err } + + keybind .Initialize(backend.x) + xgbkb .Initialize(backend.x) + mousebind.Initialize(backend.x) + + return backend, nil +} + +func (backend *Backend) Run () error { + backend.assert() + pingBefore, + pingAfter, + pingQuit := xevent.MainPing(backend.x) + for { + select { + case <- pingBefore: + <- pingAfter + case callback := <- backend.doChannel: + callback() + case <- pingQuit: + return nil // FIXME: if we exited due to an error say so + } + for _, window := range backend.windows { + window.afterEvent() + } + } +} + +func (backend *Backend) Stop () { + backend.assert() + if !backend.open { return } + backend.open = false + + toClose := []*window { } + for _, panel := range backend.windows { + toClose = append(toClose, panel) + } + for _, panel := range toClose { + panel.Close() + } + xevent.Quit(backend.x) + backend.x.Conn().Close() +} + +func (backend *Backend) Do (callback func ()) { + backend.assert() + backend.doChannel <- callback +} + +func (backend *Backend) assert () { + if backend == nil { panic("nil backend") } +} diff --git a/box.go b/box.go new file mode 100644 index 0000000..ffe5c96 --- /dev/null +++ b/box.go @@ -0,0 +1,228 @@ +package x + +import "image" +import "image/color" +import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/tomo/data" +import "git.tebibyte.media/tomo/tomo/input" +import "git.tebibyte.media/tomo/tomo/event" +import "git.tebibyte.media/tomo/tomo/canvas" + +type box struct { + backend *Backend + window *window + + bounds image.Rectangle + minSize image.Point + + padding tomo.Inset + border []tomo.Border + color color.Color + + dndData data.Data + dndAccept []data.Mime + focused bool + focusable bool + + drawClean bool + layoutClean bool + + drawer canvas.Drawer + + on struct { + focusEnter event.FuncBroadcaster + focusLeave event.FuncBroadcaster + dndEnter event.FuncBroadcaster + dndLeave event.FuncBroadcaster + dndDrop event.Broadcaster[func (data.Data)] + mouseEnter event.FuncBroadcaster + mouseLeave event.FuncBroadcaster + mouseMove event.FuncBroadcaster + mouseDown event.Broadcaster[func (input.Button)] + mouseUp event.Broadcaster[func (input.Button)] + scroll event.Broadcaster[func (float64, float64)] + keyDown event.Broadcaster[func (input.Key, bool)] + keyUp event.Broadcaster[func (input.Key, bool)] + } +} + +func (backend *Backend) NewBox() tomo.Box { + box := &box { + backend: backend, + } + box.drawer = box + return box +} + +func (this *box) Box () tomo.Box { + return this +} + +func (this *box) Window () tomo.Window { + return this.window +} + +func (this *box) Bounds () image.Rectangle { + return this.bounds +} + +func (this *box) InnerBounds () image.Rectangle { + innerBounds := this.padding.Apply(this.bounds) + for _, border := range this.border { + innerBounds = border.Width.Apply(innerBounds) + } + return innerBounds +} + +func (this *box) SetBounds (bounds image.Rectangle) { + if this.bounds == bounds { return } + this.bounds = bounds + this.invalidateLayout() +} + +func (this *box) SetColor (c color.Color) { + if this.color == c { return } + this.color = c + this.invalidateDraw() +} + +func (this *box) SetBorder (border ...tomo.Border) { + this.border = border + this.invalidateLayout() +} + +func (this *box) SetMinimumSize (width, height int) { + this.minSize = image.Pt(width, height) + if this.bounds.Dx() < width || this.bounds.Dy() < height { + this.invalidateLayout() + } +} + +func (this *box) SetPadding (padding tomo.Inset) { + if this.padding == padding { return } + this.padding = padding + this.invalidateLayout() +} + +func (this *box) SetDNDData (dat data.Data) { + this.dndData = dat +} + +func (this *box) SetDNDAccept (types ...data.Mime) { + this.dndAccept = types +} + +func (this *box) SetFocused (focused bool) { + if this.Focused () && !focused { + this.window.focus(nil) + } else if !this.Focused() && focused { + this.window.focus(this) + } +} + +func (this *box) SetFocusable (focusable bool) { + if this.focusable == focusable { return } + this.focusable = focusable + if !focusable { + this.SetFocused(false) + } +} + +func (this *box) Focused () bool { + return this == this.window.focused +} + +func (this *box) Modifiers () input.Modifiers { + return this.window.modifiers +} + +func (this *box) MousePosition () image.Point { + return this.window.mousePosition +} + +// ----- event handlers ----------------------------------------------------- // +func (this *box) OnFocusEnter (callback func()) event.Cookie { + return this.on.focusEnter.Connect(callback) +} +func (this *box) OnFocusLeave (callback func()) event.Cookie { + return this.on.focusLeave.Connect(callback) +} +func (this *box) OnDNDEnter (callback func()) event.Cookie { + return this.on.dndEnter.Connect(callback) +} +func (this *box) OnDNDLeave (callback func()) event.Cookie { + return this.on.dndLeave.Connect(callback) +} +func (this *box) OnDNDDrop (callback func(data.Data)) event.Cookie { + return this.on.dndDrop.Connect(callback) +} +func (this *box) OnMouseEnter (callback func()) event.Cookie { + return this.on.mouseEnter.Connect(callback) +} +func (this *box) OnMouseLeave (callback func()) event.Cookie { + return this.on.mouseLeave.Connect(callback) +} +func (this *box) OnMouseMove (callback func()) event.Cookie { + return this.on.mouseMove.Connect(callback) +} +func (this *box) OnMouseDown (callback func(input.Button)) event.Cookie { + return this.on.mouseDown.Connect(callback) +} +func (this *box) OnMouseUp (callback func(input.Button)) event.Cookie { + return this.on.mouseUp.Connect(callback) +} +func (this *box) OnScroll (callback func(deltaX, deltaY float64)) event.Cookie { + return this.on.scroll.Connect(callback) +} +func (this *box) OnKeyDown (callback func(key input.Key, numberPad bool)) event.Cookie { + return this.on.keyDown.Connect(callback) +} +func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Cookie { + return this.on.keyUp.Connect(callback) +} +// -------------------------------------------------------------------------- // + +func (this *box) Draw (can canvas.Canvas) { + pen := can.Pen() + + bounds := this.bounds + for _, border := range this.border { + pen.Fill(border.Color[tomo.SideTop]) + pen.Rectangle(image.Rect ( + bounds.Min.X, + bounds.Min.Y, + bounds.Max.X, + bounds.Min.Y + border.Width[tomo.SideTop])) + pen.Fill(border.Color[tomo.SideBottom]) + pen.Rectangle(image.Rect ( + bounds.Min.X, + bounds.Max.Y - border.Width[tomo.SideBottom], + bounds.Max.X, + bounds.Max.Y)) + pen.Fill(border.Color[tomo.SideLeft]) + pen.Rectangle(image.Rect ( + bounds.Min.X, + bounds.Min.Y + border.Width[tomo.SideTop], + bounds.Min.X + border.Width[tomo.SideLeft], + bounds.Max.Y - border.Width[tomo.SideBottom])) + pen.Fill(border.Color[tomo.SideRight]) + pen.Rectangle(image.Rect ( + bounds.Max.X - border.Width[tomo.SideRight], + bounds.Min.Y + border.Width[tomo.SideTop], + bounds.Max.X, + bounds.Max.Y - border.Width[tomo.SideBottom])) + + bounds = border.Width.Apply(bounds) + } + pen.Fill(this.color) + pen.Rectangle(bounds) +} + +func (this *box) invalidateDraw () { + this.drawClean = false +} + +func (this *box) invalidateLayout () { + this.invalidateDraw() + this.layoutClean = false +} diff --git a/canvas/canvas.go b/canvas/canvas.go new file mode 100644 index 0000000..048af8a --- /dev/null +++ b/canvas/canvas.go @@ -0,0 +1,96 @@ +package xcanvas + +import "image" +import "image/color" +import "github.com/jezek/xgbutil" +import "git.tebibyte.media/tomo/ggfx" +import "github.com/jezek/xgbutil/xgraphics" +import "git.tebibyte.media/tomo/tomo/canvas" + +// Canvas satisfies the canvas.Canvas interface. It draws to an xgraphics.Image. +type Canvas struct { + *xgraphics.Image +} + +// New creates a new canvas from a bounding rectangle. +func New (x *xgbutil.XUtil, bounds image.Rectangle) Canvas { + return Canvas { xgraphics.New(x, bounds) } +} + +// NewFrom creates a new canvas from an existing xgraphics.Image. +func NewFrom (image *xgraphics.Image) Canvas { + return Canvas { image } +} + +// Pen returns a new drawing context. +func (this Canvas) Pen () canvas.Pen { + return pen { + image: this.Image, + gfx: ggfx.Image[uint8] { + Pix: this.Image.Pix, + Stride: this.Image.Stride, + Bounds: this.Image.Rect, + Width: 4, + }, + } +} + +// Clip returns a sub-canvas of this canvas. +func (this Canvas) Clip (bounds image.Rectangle) Canvas { + return Canvas { this.Image.SubImage(bounds).(*xgraphics.Image) } +} + +// TODO: we need to implement: +// - cap +// - joint +// - align + +type pen struct { + image *xgraphics.Image + gfx ggfx.Image[uint8] + + closed bool + endCap canvas.Cap + joint canvas.Joint + weight int + align canvas.StrokeAlign + stroke [4]uint8 + fill [4]uint8 +} + +func (this pen) Rectangle (bounds image.Rectangle) { + if this.weight == 0 { + this.gfx.FillRectangle(this.fill[:], bounds) + } else { + this.gfx.StrokeRectangle(this.stroke[:], this.weight, bounds) + } +} + +func (this pen) Path (points ...image.Point) { + if this.weight == 0 { + this.gfx.FillPolygon(this.fill[:], points...) + } else if this.closed { + this.gfx.StrokePolygon(this.stroke[:], this.weight, points...) + } else { + this.gfx.PolyLine(this.stroke[:], this.weight, points...) + } +} + +func (this pen) Closed (closed bool) { this.closed = closed } +func (this pen) Cap (endCap canvas.Cap) { this.endCap = endCap } +func (this pen) Joint (joint canvas.Joint) { this.joint = joint } +func (this pen) StrokeWeight (weight int) { this.weight = weight } +func (this pen) StrokeAlign (align canvas.StrokeAlign) { this.align = align } + +func (this pen) Stroke (stroke color.Color) { this.stroke = convertColor(stroke) } +func (this pen) Fill (fill color.Color) { this.fill = convertColor(fill) } + +func convertColor (c color.Color) [4]uint8 { + r, g, b, a := c.RGBA() + return [4]uint8 { + uint8(b >> 8), + uint8(g >> 8), + uint8(a >> 8), + uint8(r >> 8), + } +} diff --git a/canvasbox.go b/canvasbox.go new file mode 100644 index 0000000..1ccb038 --- /dev/null +++ b/canvasbox.go @@ -0,0 +1,27 @@ +package x + +import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/tomo/canvas" + +type canvasBox struct { + *box +} + +func (backend *Backend) NewCanvasBox () tomo.CanvasBox { + return &canvasBox { + box: backend.NewBox().(*box), + } +} + +func (this *canvasBox) Box () tomo.Box { + return this +} + +func (this *canvasBox) SetDrawer (drawer canvas.Drawer) { + this.drawer = drawer + this.invalidateDraw() +} + +func (this *canvasBox) Invalidate () { + this.invalidateDraw() +} diff --git a/containerbox.go b/containerbox.go new file mode 100644 index 0000000..525c187 --- /dev/null +++ b/containerbox.go @@ -0,0 +1,8 @@ +package x + +import "git.tebibyte.media/tomo/tomo" + +func (backend *Backend) NewContainerBox() tomo.ContainerBox { + // TODO + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1f1ee10 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module git.tebibyte.media/tomo/x + +go 1.20 + +require ( + git.tebibyte.media/tomo/ggfx v0.3.0 + git.tebibyte.media/tomo/tomo v0.7.3 + git.tebibyte.media/tomo/xgbkb v1.0.1 + github.com/jezek/xgb v1.1.0 + github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 +) + +require ( + github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect + github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect + golang.org/x/image v0.8.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..38f7759 --- /dev/null +++ b/go.sum @@ -0,0 +1,66 @@ +git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q= +git.tebibyte.media/tomo/ggfx v0.2.0 h1:TSWfNQgnnHewwHiGC3VPFssdOIYCfgqCcOiPX4Sgv00= +git.tebibyte.media/tomo/ggfx v0.2.0/go.mod h1:zPoz8BdVQyG2KhEmeGFQBK66V71i6Kj8oVFbrZaCwRA= +git.tebibyte.media/tomo/ggfx v0.3.0 h1:h+RfairZTt4jT76KwmJN8OcdU7Ew0vFRqMZFqz3iHaE= +git.tebibyte.media/tomo/ggfx v0.3.0/go.mod h1:zPoz8BdVQyG2KhEmeGFQBK66V71i6Kj8oVFbrZaCwRA= +git.tebibyte.media/tomo/tomo v0.4.0 h1:nraUtsmYLSe8BZOolmeBuD+aaMk4duSxI84RqnzflCs= +git.tebibyte.media/tomo/tomo v0.4.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.5.0 h1:bfHNExPewlt+n7nq8LvNiAbemqSllrCY/tAI08r8sAo= +git.tebibyte.media/tomo/tomo v0.5.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.5.1 h1:APOTY+YSV8JJwNmJsKFYzBYLPUy3DqNr49rrSspOKZ8= +git.tebibyte.media/tomo/tomo v0.5.1/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.6.0 h1:/gjY6neXEqyKQ2Ye05mZi3yIOvsRVyIKSddvCySGN2Y= +git.tebibyte.media/tomo/tomo v0.6.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.6.1 h1:XdtHfF2xhz9pZXqyrwSsPaore/8PHVqFrnT4NwlBOhY= +git.tebibyte.media/tomo/tomo v0.6.1/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.7.0 h1:dUYBB/gZzmkiKR8Cq/nmEQGwMqVE01CnQFtvjmInif0= +git.tebibyte.media/tomo/tomo v0.7.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.7.1 h1:CHOBGel7Acp88cVW+5SEIx41cRdwuuP/niSSp9/CRRg= +git.tebibyte.media/tomo/tomo v0.7.1/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.7.2 h1:15dMJm4Sm339b23o9RZSq87u99SaF2q+b5CRB5P58fA= +git.tebibyte.media/tomo/tomo v0.7.2/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.7.3 h1:eHwuYKe+0nLWoEfPZid8njirxmWY3dFmdY+PsPp1RN0= +git.tebibyte.media/tomo/tomo v0.7.3/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE= +git.tebibyte.media/tomo/xgbkb v1.0.1/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw= +github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= +github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= +github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= +github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= +github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= +github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 h1:Pf/0BAbppEOq4azPH6fnvUX2dycAwZdGkdxFn25j44c= +github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0/go.mod h1:AHecLyFNy6AN9f/+0AH/h1MI7X1+JL5bmCz4XlVZk7Y= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg= +golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/system.go b/system.go new file mode 100644 index 0000000..82b02c6 --- /dev/null +++ b/system.go @@ -0,0 +1,20 @@ +package x + +import "git.tebibyte.media/tomo/tomo" + +type anyBox interface { + tomo.Box + invalidateDraw () + invalidateLayout () +} + +func (window *window) SetRoot (root tomo.Object) { + +} + +func (window *window) focus (bx anyBox) { + if window.focused == bx { return } + window.focused.invalidateDraw() + window.focused = bx + bx.invalidateDraw() +} diff --git a/textbox.go b/textbox.go new file mode 100644 index 0000000..92572c6 --- /dev/null +++ b/textbox.go @@ -0,0 +1,8 @@ +package x + +import "git.tebibyte.media/tomo/tomo" + +func (backend *Backend) NewTextBox() tomo.TextBox { + // TODO + return nil +} diff --git a/window.go b/window.go new file mode 100644 index 0000000..8471498 --- /dev/null +++ b/window.go @@ -0,0 +1,329 @@ +package x + +import "image" +// import "errors" + +import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/tomo/data" +import "git.tebibyte.media/tomo/tomo/input" +import "git.tebibyte.media/tomo/tomo/event" + +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/ewmh" +import "github.com/jezek/xgbutil/icccm" +// import "github.com/jezek/xgbutil/xprop" +import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/xwindow" +import "github.com/jezek/xgbutil/keybind" +import "github.com/jezek/xgbutil/mousebind" +import "github.com/jezek/xgbutil/xgraphics" + +type mainWindow struct { *window } +type window struct { + backend *Backend + xWindow *xwindow.Window + xCanvas *xgraphics.Image + + title string + + modalParent *window + hasModal bool + shy bool + + metrics struct { + bounds image.Rectangle + } + + modifiers input.Modifiers + mousePosition image.Point + + onClose event.FuncBroadcaster + + root anyBox + focused anyBox +} + +func (backend *Backend) NewWindow ( + bounds image.Rectangle, +) ( + output tomo.MainWindow, + err error, +) { + if backend == nil { panic("nil backend") } + window, err := backend.newWindow(bounds, false) + + output = mainWindow { window: window } + return output, err +} + +func (backend *Backend) newWindow ( + bounds image.Rectangle, + override bool, +) ( + output *window, + err error, +) { + if bounds.Dx() == 0 { bounds.Max.X = bounds.Min.X + 8 } + if bounds.Dy() == 0 { bounds.Max.Y = bounds.Min.Y + 8 } + + window := &window { backend: backend } + + window.xWindow, err = xwindow.Generate(backend.x) + if err != nil { return } + + if override { + err = window.xWindow.CreateChecked ( + backend.x.RootWin(), + bounds.Min.X, bounds.Min.Y, bounds.Dx(), bounds.Dy(), + xproto.CwOverrideRedirect, 1) + } else { + err = window.xWindow.CreateChecked ( + backend.x.RootWin(), + bounds.Min.X, bounds.Min.Y, bounds.Dx(), bounds.Dy(), 0) + } + if err != nil { return } + + err = window.xWindow.Listen ( + xproto.EventMaskExposure, + xproto.EventMaskStructureNotify, + xproto.EventMaskPropertyChange, + xproto.EventMaskPointerMotion, + xproto.EventMaskKeyPress, + xproto.EventMaskKeyRelease, + xproto.EventMaskButtonPress, + xproto.EventMaskButtonRelease) + if err != nil { return } + + window.xWindow.WMGracefulClose (func (xWindow *xwindow.Window) { + window.Close() + }) + + // xevent.ExposeFun(window.handleExpose). + // Connect(backend.x, window.xWindow.Id) + // xevent.ConfigureNotifyFun(window.handleConfigureNotify). + // Connect(backend.x, window.xWindow.Id) + // xevent.KeyPressFun(window.handleKeyPress). + // Connect(backend.x, window.xWindow.Id) + // xevent.KeyReleaseFun(window.handleKeyRelease). + // Connect(backend.x, window.xWindow.Id) + // xevent.ButtonPressFun(window.handleButtonPress). + // Connect(backend.x, window.xWindow.Id) + // xevent.ButtonReleaseFun(window.handleButtonRelease). + // Connect(backend.x, window.xWindow.Id) + // xevent.MotionNotifyFun(window.handleMotionNotify). + // Connect(backend.x, window.xWindow.Id) + // xevent.SelectionNotifyFun(window.handleSelectionNotify). + // Connect(backend.x, window.xWindow.Id) + // xevent.PropertyNotifyFun(window.handlePropertyNotify). + // Connect(backend.x, window.xWindow.Id) + // xevent.SelectionClearFun(window.handleSelectionClear). + // Connect(backend.x, window.xWindow.Id) + // xevent.SelectionRequestFun(window.handleSelectionRequest). + // Connect(backend.x, window.xWindow.Id) + + window.metrics.bounds = bounds + window.setMinimumSize(8, 8) + + // window.reallocateCanvas() + + backend.windows[window.xWindow.Id] = window + + output = window + return +} + +func (window *window) SetTitle (title string) { + window.title = title + ewmh .WmNameSet (window.backend.x, window.xWindow.Id, title) + icccm.WmNameSet (window.backend.x, window.xWindow.Id, title) + icccm.WmIconNameSet (window.backend.x, window.xWindow.Id, title) +} + +func (window *window) SetIcon (sizes []image.Image) { + wmIcons := []ewmh.WmIcon { } + + for _, icon := range sizes { + width := icon.Bounds().Max.X + height := icon.Bounds().Max.Y + wmIcon := ewmh.WmIcon { + Width: uint(width), + Height: uint(height), + Data: make([]uint, width * height), + } + + // manually convert image data beacuse of course we have to do + // this + index := 0 + for y := 0; y < height; y ++ { + for x := 0; x < width; x ++ { + r, g, b, a := icon.At(x, y).RGBA() + r >>= 8 + g >>= 8 + b >>= 8 + a >>= 8 + wmIcon.Data[index] = + (uint(a) << 24) | + (uint(r) << 16) | + (uint(g) << 8) | + (uint(b) << 0) + index ++ + }} + + wmIcons = append(wmIcons, wmIcon) + } + + ewmh.WmIconSet ( + window.backend.x, + window.xWindow.Id, + wmIcons) +} + +func (window *window) NewMenu (bounds image.Rectangle) (tomo.Window, error) { + menu, err := window.backend.newWindow ( + bounds.Add(window.metrics.bounds.Min), true) + menu.shy = true + icccm.WmTransientForSet ( + window.backend.x, + menu.xWindow.Id, + window.xWindow.Id) + menu.setType("POPUP_MENU") + // menu.inheritProperties(window) + return menu, err +} + +func (window *window) NewModal (bounds image.Rectangle) (tomo.Window, error) { + modal, err := window.backend.newWindow ( + bounds.Add(window.metrics.bounds.Min), false) + icccm.WmTransientForSet ( + window.backend.x, + modal.xWindow.Id, + window.xWindow.Id) + ewmh.WmStateSet ( + window.backend.x, + modal.xWindow.Id, + []string { "_NET_WM_STATE_MODAL" }) + modal.modalParent = window + window.hasModal = true + // modal.inheritProperties(window) + return modal, err +} + +func (window mainWindow) NewChild (bounds image.Rectangle) (tomo.Window, error) { + child, err := window.backend.newWindow ( + bounds.Add(window.metrics.bounds.Min), false) + if err != nil { return nil, err } + child.setClientLeader(window.window) + window.setClientLeader(window.window) + icccm.WmTransientForSet ( + window.backend.x, + window.xWindow.Id, + window.xWindow.Id) + window.setType("UTILITY") + // window.inheritProperties(window.window) + return window, err +} + +func (window *window) Widget () (tomo.Window, error) { + // TODO + return nil, nil +} + +func (window *window) Copy (data.Data) { + // TODO +} + +func (window *window) Paste (callback func (data.Data, error), accept ...data.Mime) { + // TODO +} + +func (window *window) Show () { + // if window.child == nil { + // window.xCanvas.For (func (x, y int) xgraphics.BGRA { + // return xgraphics.BGRA { } + // }) +// + // window.pushRegion(window.xCanvas.Bounds()) + // } + + window.xWindow.Map() + if window.shy { window.grabInput() } +} + +func (window *window) Hide () { + window.xWindow.Unmap() + if window.shy { window.ungrabInput() } +} + +func (window *window) Close () { + xevent .Detach(window.backend.x, window.xWindow.Id) + keybind .Detach(window.backend.x, window.xWindow.Id) + mousebind.Detach(window.backend.x, window.xWindow.Id) + + window.onClose.Broadcast() + if window.modalParent != nil { + // we are a modal dialog, so unlock the parent + window.modalParent.hasModal = false + } + window.Hide() + // window.Adopt(nil) + delete(window.backend.windows, window.xWindow.Id) + window.xWindow.Destroy() +} + +func (window *window) OnClose (callback func ()) event.Cookie { + return window.onClose.Connect(callback) +} + +func (window *window) grabInput () { + keybind.GrabKeyboard(window.backend.x, window.xWindow.Id) + mousebind.GrabPointer ( + window.backend.x, + window.xWindow.Id, + window.backend.x.RootWin(), 0) +} + +func (window *window) ungrabInput () { + keybind.UngrabKeyboard(window.backend.x) + mousebind.UngrabPointer(window.backend.x) +} + +func (window *window) setType (ty string) error { + return ewmh.WmWindowTypeSet ( + window.backend.x, + window.xWindow.Id, + []string { "_NET_WM_WINDOW_TYPE_" + ty }) +} + +func (window *window) setClientLeader (leader *window) error { + hints, _ := icccm.WmHintsGet(window.backend.x, window.xWindow.Id) + if hints == nil { + hints = &icccm.Hints { } + } + hints.Flags |= icccm.HintWindowGroup + hints.WindowGroup = leader.xWindow.Id + return icccm.WmHintsSet ( + window.backend.x, + window.xWindow.Id, + hints) +} + +func (window *window) setMinimumSize (width, height int) { + if width < 8 { width = 8 } + if height < 8 { height = 8 } + icccm.WmNormalHintsSet ( + window.backend.x, + window.xWindow.Id, + &icccm.NormalHints { + Flags: icccm.SizeHintPMinSize, + MinWidth: uint(width), + MinHeight: uint(height), + }) + newWidth := window.metrics.bounds.Dx() + newHeight := window.metrics.bounds.Dy() + if newWidth < width { newWidth = width } + if newHeight < height { newHeight = height } + if newWidth != window.metrics.bounds.Dx() || + newHeight != window.metrics.bounds.Dy() { + window.xWindow.Resize(newWidth, newHeight) + } +} diff --git a/x/plugin.go b/x/plugin.go new file mode 100644 index 0000000..bad33dd --- /dev/null +++ b/x/plugin.go @@ -0,0 +1,17 @@ +// Plugin x provides the X11 backend as a plugin. +package main + +import "git.tebibyte.media/tomo/x" +import "git.tebibyte.media/tomo/tomo" + +func init () { + tomo.Register(x.NewBackend) +} + +func Name () string { + return "X" +} + +func Description () string { + return "Provides an X11 backend." +}