commit 5e7c666d9293214e9542d83afb7fb46e4dac05bd Author: Sasha Koshka Date: Sun Jul 2 02:52:14 2023 -0400 Initial commit 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." +}