29 Commits

Author SHA1 Message Date
d9cf931903 Upgrade to new version of typeset 2024-04-24 11:40:31 -04:00
7fc2b1cd87 Text boxes now have properly constrained scrolling/autoscrolling 2023-09-09 20:22:10 -04:00
2ecfafa469 Upgrade tomo version 2023-09-09 15:05:08 -04:00
c91d4577e7 Minor drawing fixess 2023-09-08 16:39:58 -04:00
db2ed06daf Fixed transparent boxes not redrawing sometimes 2023-09-07 18:22:04 -04:00
0c0b8ae475 Boxes no longer panic on nil texture 2023-09-05 21:23:24 -04:00
af6aa2c463 Upgrade tomo version 2023-09-05 17:50:53 -04:00
11a14431be lol whoops 2023-09-04 02:54:56 -04:00
f9eaab89ff Rewrite readme 2023-09-04 02:53:04 -04:00
Sasha Koshka
f48bf0609f Upgraded tomo version 2023-09-04 02:33:47 -04:00
ab80658cd9 Textured rectangles are working (if a bit slow) 2023-08-29 17:23:24 -04:00
e2a370259b WIP texture drawing 2023-08-29 15:52:24 -04:00
b5f67c65b0 Cleaned up code relating to transparency 2023-08-25 02:51:07 -04:00
8ed1352fd4 Fixed non-cannonized rectangles being drawn 2023-08-24 17:15:34 -04:00
77ccde9e9b Upgrade tomo version 2023-08-24 15:54:22 -04:00
94db4e8ead Transparency support!! 2023-08-23 19:21:28 -04:00
2f0259d913 WIP moving ggfx stuff into xcanvas 2023-08-22 21:24:38 -04:00
8401b5d0f9 Add support for transparency
Need to break away from ggfx to do this, probably put everything
in xcanvas.
2023-08-22 13:17:48 -04:00
45fa282b61 Texture now checks for transparent pixels 2023-08-21 22:39:13 -04:00
fae918a196 Added textures to backend 2023-08-21 00:35:55 -04:00
3cffcfd4e5 Upgraded tomo version 2023-08-21 00:34:57 -04:00
ff8875535d Changed the way minimum sizes are calculated
Boxes that need their minimum size to be updated now use a map like
for layout and drawing. Size set with MinimumSize is now treated as
separate from the content size and the larger size is used.
2023-08-17 23:20:08 -04:00
00629a863d Behavior relating to hovering is more solid 2023-08-12 12:15:34 -04:00
e126c01055 Add support for mouse hover events 2023-08-12 12:10:47 -04:00
19dbc73968 Fixed keyboard navigation 2023-08-12 11:55:25 -04:00
2de079d063 I forgor 2023-08-12 01:03:34 -04:00
009ae811d3 Box.Box now returns the correct value 2023-08-08 12:56:49 -04:00
5ba72cd1e5 Upgrade tomo version 2023-08-08 12:12:23 -04:00
Sasha Koshka
c6ee141d88 Upgrade typeset 2023-08-07 21:14:12 -04:00
19 changed files with 1204 additions and 301 deletions

View File

@@ -1,3 +1,14 @@
# x
WIP X backend for Tomo.
[![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/x.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/x)
An X11 backend for Tomo.
## Installation
```
cd x/x
go build -buildmode=plugin .
mkdir -p ~/.local/lib/tomo/plugins
mv x.so ~/.local/lib/tomo/plugins
```

View File

@@ -48,9 +48,7 @@ func (backend *Backend) Run () error {
case <- pingQuit:
return nil // FIXME: if we exited due to an error say so
}
for _, window := range backend.windows {
window.afterEvent()
}
backend.afterEvent()
}
}
@@ -78,3 +76,9 @@ func (backend *Backend) Do (callback func ()) {
func (backend *Backend) assert () {
if backend == nil { panic("nil backend") }
}
func (backend *Backend) afterEvent () {
for _, window := range backend.windows {
window.afterEvent()
}
}

268
box.go
View File

@@ -3,6 +3,7 @@ package x
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/x/canvas"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
@@ -13,12 +14,18 @@ type box struct {
parent parent
outer anyBox
bounds image.Rectangle
minSize image.Point
bounds image.Rectangle
minSize image.Point
userMinSize image.Point
innerClippingBounds image.Rectangle
minSizeQueued bool
focusQueued *bool
padding tomo.Inset
border []tomo.Border
color color.Color
texture *xcanvas.Texture
dndData data.Data
dndAccept []data.Mime
@@ -45,18 +52,28 @@ type box struct {
}
}
func (backend *Backend) NewBox() tomo.Box {
func (backend *Backend) newBox (outer anyBox) *box {
box := &box {
backend: backend,
color: color.White,
color: color.Transparent,
outer: outer,
drawer: outer,
}
box.drawer = box
box.outer = box
if outer == nil {
box.drawer = box
box.outer = box
}
box.invalidateMinimum()
return box
}
func (this *box) Box () tomo.Box {
return this
func (backend *Backend) NewBox() tomo.Box {
box := backend.newBox(nil)
return box
}
func (this *box) GetBox () tomo.Box {
return this.outer
}
func (this *box) Window () tomo.Window {
@@ -68,7 +85,7 @@ func (this *box) Bounds () image.Rectangle {
}
func (this *box) InnerBounds () image.Rectangle {
return this.padding.Apply(this.innerClippingBounds())
return this.padding.Apply(this.innerClippingBounds)
}
func (this *box) MinimumSize () image.Point {
@@ -86,10 +103,6 @@ func (this *box) borderSum () tomo.Inset {
return sum
}
func (this *box) innerClippingBounds () image.Rectangle {
return this.borderSum().Apply(this.bounds)
}
func (this *box) SetBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
@@ -97,31 +110,34 @@ func (this *box) SetBounds (bounds image.Rectangle) {
}
func (this *box) SetColor (c color.Color) {
if c == nil { c = color.Transparent }
if this.color == c { return }
this.color = c
this.invalidateDraw()
}
func (this *box) SetTexture (texture canvas.Texture) {
this.texture = xcanvas.AssertTexture(texture)
this.invalidateDraw()
}
func (this *box) SetBorder (border ...tomo.Border) {
this.border = border
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *box) SetMinimumSize (size image.Point) {
if this.minSize == size { return }
this.minSize = size
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
if this.userMinSize == size { return }
this.userMinSize = size
this.invalidateMinimum()
}
func (this *box) SetPadding (padding tomo.Inset) {
if this.padding == padding { return }
this.padding = padding
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *box) SetDNDData (dat data.Data) {
@@ -133,9 +149,11 @@ func (this *box) SetDNDAccept (types ...data.Mime) {
}
func (this *box) SetFocused (focused bool) {
if this.parent == nil { return }
window := this.parent.window()
if window == nil { return }
if this.parent == nil || this.parent.window () == nil {
focusedCopy := focused
this.focusQueued = &focusedCopy
return
}
if !this.focusable { return }
if this.Focused () && !focused {
@@ -208,58 +226,146 @@ func (this *box) OnKeyDown (callback func(key input.Key, numberPad bool)) event.
func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Cookie {
return this.on.keyUp.Connect(callback)
}
func (this *box) handleFocusEnter () {
this.on.focusEnter.Broadcast()
}
func (this *box) handleFocusLeave () {
this.on.focusLeave.Broadcast()
}
func (this *box) handleMouseEnter () {
this.on.mouseEnter.Broadcast()
}
func (this *box) handleMouseLeave () {
this.on.mouseLeave.Broadcast()
}
func (this *box) handleMouseMove () {
this.on.mouseMove.Broadcast()
}
func (this *box) handleMouseDown (button input.Button) {
if this.focusable {
this.SetFocused(true)
} else {
if this.parent == nil { return }
window := this.parent.window()
if window == nil { return }
window.focus(nil)
}
for _, listener := range this.on.mouseDown.Listeners() {
listener(button)
}
}
func (this *box) handleMouseUp (button input.Button) {
for _, listener := range this.on.mouseUp.Listeners() {
listener(button)
}
}
func (this *box) handleScroll (x, y float64) {
for _, listener := range this.on.scroll.Listeners() {
listener(x, y)
}
}
func (this *box) handleKeyDown (key input.Key, numberPad bool) {
for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad)
}
}
func (this *box) handleKeyUp (key input.Key, numberPad bool) {
for _, listener := range this.on.keyUp.Listeners() {
listener(key, numberPad)
}
}
// -------------------------------------------------------------------------- //
func (this *box) Draw (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
pen.Fill(this.color)
pen.Rectangle(this.bounds)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(can.Bounds())
}
func (this *box) drawBorders (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
bounds := this.bounds
rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1)
if transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.Clip(area))
}
pen.Fill(c)
pen.Rectangle(area)
}
for _, border := range this.border {
pen.Fill(border.Color[tomo.SideTop])
pen.Rectangle(image.Rect (
rectangle (
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.Y + border.Width[tomo.SideTop],
border.Color[tomo.SideTop])
rectangle (
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.Max.Y,
border.Color[tomo.SideBottom])
rectangle (
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.Y - border.Width[tomo.SideBottom],
border.Color[tomo.SideLeft])
rectangle (
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.Max.Y - border.Width[tomo.SideBottom],
border.Color[tomo.SideRight])
bounds = border.Width.Apply(bounds)
}
}
func (this *box) contentMinimum () image.Point {
var minimum image.Point
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
return minimum
}
func (this *box) doMinimumSize () {
this.minSize = this.outer.contentMinimum()
if this.minSize.X < this.userMinSize.X {
this.minSize.X = this.userMinSize.X
}
if this.minSize.Y < this.userMinSize.Y {
this.minSize.Y = this.userMinSize.Y
}
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
}
func (this *box) doDraw () {
if this.canvas == nil { return }
if this.drawer != nil {
this.drawBorders(this.canvas)
this.drawer.Draw(this.canvas.Clip(this.innerClippingBounds()))
this.drawer.Draw(this.canvas.Clip(this.innerClippingBounds))
}
}
func (this *box) doLayout () {
this.innerClippingBounds = this.borderSum().Apply(this.bounds)
if this.parent == nil { this.canvas = nil; return }
parentCanvas := this.parent.canvas()
if parentCanvas == nil { this.canvas = nil; return }
@@ -273,6 +379,21 @@ func (this *box) setParent (parent parent) {
this.parent = parent
}
func (this *box) getParent () parent {
return this.parent
}
func (this *box) flushActionQueue () {
if this.parent == nil || this.parent.window() == nil { return }
if this.minSizeQueued {
this.invalidateMinimum()
}
if this.focusQueued != nil {
this.SetFocused(*this.focusQueued)
}
}
func (this *box) recursiveRedo () {
this.doLayout()
this.doDraw()
@@ -288,17 +409,19 @@ func (this *box) invalidateDraw () {
this.parent.window().invalidateDraw(this.outer)
}
func (this *box) recalculateMinimumSize () {
if this.outer != anyBox(this) {
this.outer.recalculateMinimumSize()
func (this *box) invalidateMinimum () {
if this.parent == nil || this.parent.window() == nil {
this.minSizeQueued = true
return
}
this.parent.window().invalidateMinimum(this.outer)
}
func (this *box) canBeFocused () bool {
return this.focusable
}
func (this *box) boxUnder (point image.Point) anyBox {
func (this *box) boxUnder (point image.Point, category eventCategory) anyBox {
if point.In(this.bounds) {
return this.outer
} else {
@@ -306,62 +429,15 @@ func (this *box) boxUnder (point image.Point) anyBox {
}
}
func (this *box) handleFocusEnter () {
this.on.focusEnter.Broadcast()
}
func (this *box) handleFocusLeave () {
this.on.focusLeave.Broadcast()
}
func (this *box) handleMouseEnter () {
this.on.mouseEnter.Broadcast()
}
func (this *box) handleMouseLeave () {
this.on.mouseLeave.Broadcast()
}
func (this *box) handleMouseMove () {
this.on.mouseMove.Broadcast()
}
func (this *box) handleMouseDown (button input.Button) {
if this.focusable {
this.SetFocused(true)
} else {
if this.parent == nil { return }
window := this.parent.window()
if window == nil { return }
window.focus(nil)
}
for _, listener := range this.on.mouseDown.Listeners() {
listener(button)
}
}
func (this *box) handleMouseUp (button input.Button) {
for _, listener := range this.on.mouseUp.Listeners() {
listener(button)
}
}
func (this *box) handleKeyDown (key input.Key, numberPad bool) {
for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad)
}
}
func (this *box) handleKeyUp (key input.Key, numberPad bool) {
for _, listener := range this.on.keyUp.Listeners() {
listener(key, numberPad)
}
}
func (this *box) propagate (callback func (anyBox) bool) bool {
return callback(this)
return callback(this.outer)
}
func (this *box) propagateAlt (callback func (anyBox) bool) bool {
return callback(this)
return callback(this.outer)
}
func (this *box) transparent () bool {
return transparent(this.color) &&
(this.texture == nil || !this.texture.Opaque())
}

View File

@@ -4,22 +4,23 @@ import "image"
import "image/color"
import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto"
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.
// It must be closed after use.
type Canvas struct {
*xgraphics.Image
}
// New creates a new canvas from a bounding rectangle.
func New (x *xgbutil.XUtil, bounds image.Rectangle) *Canvas {
return NewFrom(xgraphics.New(x, bounds))
// NewCanvas creates a new canvas from a bounding rectangle.
func NewCanvas (x *xgbutil.XUtil, bounds image.Rectangle) *Canvas {
return NewCanvasFrom(xgraphics.New(x, bounds))
}
// NewFrom creates a new canvas from an existing xgraphics.Image.
func NewFrom (image *xgraphics.Image) *Canvas {
// NewCanvasFrom creates a new canvas from an existing xgraphics.Image. Note
// that calling Close() on the resulting canvas will destroy this image.
func NewCanvasFrom (image *xgraphics.Image) *Canvas {
if image == nil { return nil }
return &Canvas { image }
}
@@ -28,12 +29,6 @@ func NewFrom (image *xgraphics.Image) *Canvas {
func (this *Canvas) Pen () canvas.Pen {
return &pen {
image: this.Image,
gfx: ggfx.Image[uint8] {
Pix: this.Image.Pix,
Stride: this.Image.Stride,
Rect: this.Image.Rect,
Width: 4,
},
}
}
@@ -53,6 +48,12 @@ func (this *Canvas) Push (window xproto.Window) {
this.XExpPaint(window, this.Bounds().Min.X, this.Bounds().Min.Y)
}
// Close frees this canvas from the X server.
func (this *Canvas) Close () {
this.assert()
this.Image.Destroy()
}
func (this *Canvas) assert () {
if this == nil { panic("nil canvas") }
}
@@ -64,50 +65,69 @@ func (this *Canvas) assert () {
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
closed bool
endCap canvas.Cap
joint canvas.Joint
weight int
align canvas.StrokeAlign
stroke xgraphics.BGRA
fill xgraphics.BGRA
texture *Texture
}
func (this *pen) Rectangle (bounds image.Rectangle) {
bounds = bounds.Canon()
if this.weight == 0 {
this.gfx.FillRectangle(this.fill[:], bounds)
if this.fill.A > 0 && !this.textureObscures() {
this.fillRectangle(this.fill, bounds)
}
if this.texture != nil {
this.textureRectangle(bounds)
}
} else {
this.gfx.StrokeRectangle(this.stroke[:], this.weight, bounds)
if this.stroke.A > 0 {
this.strokeRectangle(this.stroke, 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...)
if this.fill.A > 0 {
this.fillPolygon(this.fill, points...)
}
} else if this.closed && len(points) > 2 {
if this.stroke.A > 0 {
this.strokePolygon(this.stroke, points...)
}
} else {
this.gfx.PolyLine(this.stroke[:], this.weight, points...)
if this.stroke.A > 0 {
this.polyLine(this.stroke, 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 (this *pen) Stroke (stroke color.Color) { this.stroke = convertColor(stroke) }
func (this *pen) Fill (fill color.Color) { this.fill = convertColor(fill) }
func (this *pen) Texture (texture canvas.Texture) { this.texture = AssertTexture(texture) }
func convertColor (c color.Color) [4]uint8 {
func (this *pen) textureObscures () bool {
return this.texture != nil && this.texture.Opaque()
}
func convertColor (c color.Color) xgraphics.BGRA {
r, g, b, a := c.RGBA()
return [4]uint8 {
uint8(b >> 8),
uint8(g >> 8),
uint8(r >> 8),
uint8(a >> 8),
return xgraphics.BGRA {
B: uint8(b >> 8),
G: uint8(g >> 8),
R: uint8(r >> 8),
A: uint8(a >> 8),
}
}

296
canvas/draw.go Normal file
View File

@@ -0,0 +1,296 @@
package xcanvas
import "sort"
import "image"
import "github.com/jezek/xgbutil/xgraphics"
func (this *pen) textureRectangle (bounds image.Rectangle) {
if this.texture.Opaque() {
this.textureRectangleOpaque(bounds)
} else {
this.textureRectangleTransparent(bounds)
}
}
func (this *pen) textureRectangleOpaque (bounds image.Rectangle) {
dstBounds := bounds.Intersect(this.image.Bounds())
var pos image.Point
dst := this.image.Pix
src := this.texture.pix
offset := this.texture.rect.Min.Sub(bounds.Min)
for pos.Y = dstBounds.Min.Y; pos.Y < dstBounds.Max.Y; pos.Y ++ {
for pos.X = dstBounds.Min.X; pos.X < dstBounds.Max.X; pos.X ++ {
srcPos := pos.Add(offset)
dstIndex := this.image.PixOffset(pos.X, pos.Y)
srcIndex := this.texture.PixOffset(srcPos.X, srcPos.Y)
dst[dstIndex + 0] = src[srcIndex + 0]
dst[dstIndex + 1] = src[srcIndex + 1]
dst[dstIndex + 2] = src[srcIndex + 2]
dst[dstIndex + 3] = src[srcIndex + 3]
}}
}
func (this *pen) textureRectangleTransparent (bounds image.Rectangle) {
dstBounds := bounds.Intersect(this.image.Bounds())
var pos image.Point
dst := this.image.Pix
src := this.texture.pix
offset := this.texture.rect.Min.Sub(bounds.Min)
for pos.Y = dstBounds.Min.Y; pos.Y < dstBounds.Max.Y; pos.Y ++ {
for pos.X = dstBounds.Min.X; pos.X < dstBounds.Max.X; pos.X ++ {
srcPos := pos.Add(offset)
dstIndex := this.image.PixOffset(pos.X, pos.Y)
srcIndex := this.texture.PixOffset(srcPos.X, srcPos.Y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
B: dst[dstIndex + 0],
G: dst[dstIndex + 1],
R: dst[dstIndex + 2],
A: dst[dstIndex + 3],
}, xgraphics.BGRA {
B: src[srcIndex + 0],
G: src[srcIndex + 1],
R: src[srcIndex + 2],
A: src[srcIndex + 3],
})
dst[dstIndex + 0] = pixel.B
dst[dstIndex + 1] = pixel.G
dst[dstIndex + 2] = pixel.R
dst[dstIndex + 3] = pixel.A
}}
}
func (this *pen) fillRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
if c.A == 255 {
this.fillRectangleOpaque(c, bounds)
} else {
this.fillRectangleTransparent(c, bounds)
}
}
func (this *pen) fillRectangleOpaque (c xgraphics.BGRA, bounds image.Rectangle) {
bounds = bounds.Intersect(this.image.Bounds())
var pos image.Point
for pos.Y = bounds.Min.Y; pos.Y < bounds.Max.Y; pos.Y ++ {
for pos.X = bounds.Min.X; pos.X < bounds.Max.X; pos.X ++ {
index := this.image.PixOffset(pos.X, pos.Y)
this.image.Pix[index + 0] = c.B
this.image.Pix[index + 1] = c.G
this.image.Pix[index + 2] = c.R
this.image.Pix[index + 3] = c.A
}}
}
func (this *pen) fillRectangleTransparent (c xgraphics.BGRA, bounds image.Rectangle) {
bounds = bounds.Intersect(this.image.Bounds())
var pos image.Point
for pos.Y = bounds.Min.Y; pos.Y < bounds.Max.Y; pos.Y ++ {
for pos.X = bounds.Min.X; pos.X < bounds.Max.X; pos.X ++ {
index := this.image.PixOffset(pos.X, pos.Y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
B: this.image.Pix[index + 0],
G: this.image.Pix[index + 1],
R: this.image.Pix[index + 2],
A: this.image.Pix[index + 3],
}, c)
this.image.Pix[index + 0] = pixel.B
this.image.Pix[index + 1] = pixel.G
this.image.Pix[index + 2] = pixel.R
this.image.Pix[index + 3] = pixel.A
}}
}
func (this *pen) strokeRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
if this.weight > bounds.Dx() / 2 || this.weight > bounds.Dy() / 2 {
this.fillRectangle(c, bounds)
return
}
top := image.Rect (
bounds.Min.X,
bounds.Min.Y,
bounds.Max.X,
bounds.Min.Y + this.weight)
bottom := image.Rect (
bounds.Min.X,
bounds.Max.Y - this.weight,
bounds.Max.X,
bounds.Max.Y)
left := image.Rect (
bounds.Min.X,
bounds.Min.Y + this.weight,
bounds.Min.X + this.weight,
bounds.Max.Y - this.weight)
right := image.Rect (
bounds.Max.X - this.weight,
bounds.Min.Y + this.weight,
bounds.Max.X,
bounds.Max.Y - this.weight)
this.fillRectangle(c, top,)
this.fillRectangle(c, bottom,)
this.fillRectangle(c, left,)
this.fillRectangle(c, right,)
}
// the polygon filling algorithm is adapted from:
// https://www.alienryderflex.com/polygon_fill/
// (if you write C like that i will disassemble you)
func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) {
if len(points) < 3 { return }
// figure out the bounds of the polygon so we don't test empty space
var area image.Rectangle
area.Min = points[0]
area.Max = points[0]
for _, point := range points[1:] {
if point.X < area.Min.X { area.Min.X = point.X }
if point.Y < area.Min.Y { area.Min.Y = point.Y }
if point.X > area.Max.X { area.Max.X = point.X }
if point.Y > area.Max.Y { area.Max.Y = point.Y }
}
area = this.image.Bounds().Intersect(area)
if area.Empty() { return }
context := fillingContext {
image: this.image,
color: this.fill,
min: area.Min.X,
max: area.Max.X,
boundaries: make([]int, len(points)),
points: points,
}
for context.y = area.Min.Y; context.y < area.Max.Y; context.y ++ {
// build boundary list
boundaryCount := 0
prevPoint := points[len(points) - 1]
for _, point := range points {
fy := float64(context.y)
fPointX := float64(point.X)
fPointY := float64(point.Y)
fPrevX := float64(prevPoint.X)
fPrevY := float64(prevPoint.Y)
addboundary :=
(fPointY < fy && fPrevY >= fy) ||
(fPrevY < fy && fPointY >= fy)
if addboundary {
context.boundaries[boundaryCount] = int (
fPointX +
(fy - fPointY) /
(fPrevY - fPointY) *
(fPrevX - fPointX))
boundaryCount ++
}
prevPoint = point
}
// sort boundary list
cutBoundaries := context.boundaries[:boundaryCount]
sort.Ints(cutBoundaries)
// fill pixels between boundary pairs
if c.A == 255 {
context.fillPolygonHotOpaque()
} else {
context.fillPolygonHotTransparent()
}
}
}
type fillingContext struct {
image *xgraphics.Image
color xgraphics.BGRA
min, max int
y int
boundaries []int
points []image.Point
}
func (context *fillingContext) fillPolygonHotOpaque () {
for index := 0; index < len(context.boundaries); index += 2 {
left := context.boundaries[index]
right := context.boundaries[index + 1]
// stop if we have exited the polygon
if left >= context.max { break }
// begin filling if we are within the polygon
if right > context.min {
// constrain boundaries to image size
if left < context.min { left = context.min }
if right > context.max { right = context.max }
// fill pixels in between
for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y)
context.image.Pix[index + 0] = context.color.B
context.image.Pix[index + 1] = context.color.G
context.image.Pix[index + 2] = context.color.R
context.image.Pix[index + 3] = context.color.A
}
}
}
}
func (context *fillingContext) fillPolygonHotTransparent () {
for index := 0; index < len(context.boundaries); index += 2 {
left := context.boundaries[index]
right := context.boundaries[index + 1]
// stop if we have exited the polygon
if left >= context.max { break }
// begin filling if we are within the polygon
if right > context.min {
// constrain boundaries to image size
if left < context.min { left = context.min }
if right > context.max { right = context.max }
// fill pixels in between
for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
B: context.image.Pix[index + 0],
G: context.image.Pix[index + 1],
R: context.image.Pix[index + 2],
A: context.image.Pix[index + 3],
}, context.color)
context.image.Pix[index + 0] = pixel.B
context.image.Pix[index + 1] = pixel.G
context.image.Pix[index + 2] = pixel.R
context.image.Pix[index + 3] = pixel.A
}
}
}
}
func (this *pen) strokePolygon (c xgraphics.BGRA, points ...image.Point) {
prevPoint := points[len(points) - 1]
for _, point := range points {
this.line(c, prevPoint, point)
prevPoint = point
}
}
func (this *pen) polyLine (c xgraphics.BGRA, points ...image.Point) {
if len(points) < 2 { return }
prevPoint := points[0]
for _, point := range points[1:] {
this.line(c, prevPoint, point)
prevPoint = point
}
}
func wrap (n, min, max int) int {
max -= min
n -= min
n %= max
if n < 0 { n += max }
return n + min
}

95
canvas/line.go Normal file
View File

@@ -0,0 +1,95 @@
package xcanvas
import "image"
import "github.com/jezek/xgbutil/xgraphics"
// TODO: clip the line to the bounds
func (this *pen) line (
c xgraphics.BGRA,
min image.Point,
max image.Point,
) {
context := linePlottingContext {
plottingContext: plottingContext {
image: this.image,
color: c,
weight: this.weight,
},
min: min,
max: max,
}
if abs(max.Y - min.Y) < abs(max.X - min.X) {
if max.X < min.X { context.swap() }
context.lineLow()
} else {
if max.Y < min.Y { context.swap() }
context.lineHigh()
}
}
type linePlottingContext struct {
plottingContext
min image.Point
max image.Point
}
func (context *linePlottingContext) swap () {
temp := context.max
context.max = context.min
context.min = temp
}
func (context linePlottingContext) lineLow () {
deltaX := context.max.X - context.min.X
deltaY := context.max.Y - context.min.Y
yi := 1
if deltaY < 0 {
yi = -1
deltaY *= -1
}
D := (2 * deltaY) - deltaX
point := context.min
for ; point.X < context.max.X; point.X ++ {
context.plot(point)
if D > 0 {
D += 2 * (deltaY - deltaX)
point.Y += yi
} else {
D += 2 * deltaY
}
}
}
func (context linePlottingContext) lineHigh () {
deltaX := context.max.X - context.min.X
deltaY := context.max.Y - context.min.Y
xi := 1
if deltaX < 0 {
xi = -1
deltaX *= -1
}
D := (2 * deltaX) - deltaY
point := context.min
for ; point.Y < context.max.Y; point.Y ++ {
context.plot(point)
if D > 0 {
point.X += xi
D += 2 * (deltaX - deltaY)
} else {
D += 2 * deltaX
}
}
}
func abs (n int) int {
if n < 0 { n *= -1}
return n
}

47
canvas/plot.go Normal file
View File

@@ -0,0 +1,47 @@
package xcanvas
import "image"
import "github.com/jezek/xgbutil/xgraphics"
type plottingContext struct {
image *xgraphics.Image
color xgraphics.BGRA
weight int
}
func (context plottingContext) square (center image.Point) (square image.Rectangle) {
return image.Rect(0, 0, context.weight, context.weight).
Sub(image.Pt(context.weight / 2, context.weight / 2)).
Add(center).
Intersect(context.image.Bounds())
}
func (context plottingContext) plot (center image.Point) {
square := context.square(center)
if context.color.A == 255 {
for y := square.Min.Y; y < square.Max.Y; y ++ {
for x := square.Min.X; x < square.Max.X; x ++ {
index := context.image.PixOffset(x, y)
context.image.Pix[index + 0] = context.color.B
context.image.Pix[index + 1] = context.color.G
context.image.Pix[index + 2] = context.color.R
context.image.Pix[index + 3] = context.color.A
}}
} else {
for y := square.Min.Y; y < square.Max.Y; y ++ {
for x := square.Min.X; x < square.Max.X; x ++ {
index := context.image.PixOffset(x, y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
B: context.image.Pix[index + 0],
G: context.image.Pix[index + 1],
R: context.image.Pix[index + 2],
A: context.image.Pix[index + 3],
}, context.color)
context.image.Pix[index + 0] = pixel.B
context.image.Pix[index + 1] = pixel.G
context.image.Pix[index + 2] = pixel.R
context.image.Pix[index + 3] = pixel.A
}}
}
}

106
canvas/texture.go Normal file
View File

@@ -0,0 +1,106 @@
package xcanvas
import "image"
import "image/color"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/tomo/tomo/canvas"
// Texture is a read-only image texture that can be quickly written to a canvas.
// It must be closed manually after use.
type Texture struct {
pix []uint8
stride int
rect image.Rectangle
transparent bool
}
// NewTextureFrom creates a new texture from a source image.
func NewTextureFrom (source image.Image) *Texture {
bounds := source.Bounds()
texture := &Texture {
pix: make([]uint8, bounds.Dx() * bounds.Dy() * 4),
stride: bounds.Dx() * 4,
rect: bounds.Sub(bounds.Min),
}
index := 0
var point image.Point
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
r, g, b, a := source.At(point.X, point.Y).RGBA()
texture.pix[index + 0] = uint8(b >> 8)
texture.pix[index + 1] = uint8(g >> 8)
texture.pix[index + 2] = uint8(r >> 8)
texture.pix[index + 3] = uint8(a >> 8)
index += 4
if a != 0xFFFF {
texture.transparent = true
}
}}
return texture
}
func (this *Texture) BGRAAt (x, y int) xgraphics.BGRA {
if !(image.Point{ x, y }.In(this.rect)) {
return xgraphics.BGRA { }
}
index := this.PixOffset(x, y)
return xgraphics.BGRA {
B: this.pix[index ],
G: this.pix[index + 1],
R: this.pix[index + 2],
A: this.pix[index + 3],
}
}
func (this *Texture) At (x, y int) color.Color {
return this.BGRAAt(x, y)
}
// Bounds returns the bounding rectangle of this texture.
func (this *Texture) Bounds () image.Rectangle {
return this.rect
}
func (this *Texture) ColorModel () color.Model {
return xgraphics.BGRAModel
}
// Opaque reports whether or not the texture is fully opaque.
func (this *Texture) Opaque () bool {
return !this.transparent
}
func (this *Texture) PixOffset (x, y int) int {
x = wrap(x, this.rect.Min.X, this.rect.Max.X)
y = wrap(y, this.rect.Min.Y, this.rect.Max.Y)
return x * 4 + y * this.stride
}
// Close frees the texture from memory.
func (this *Texture) Close () error {
// i lied we dont actually need to close this, but we will once this
// texture resides on the x server or in video memory.
return nil
}
// Clip returns a subset of this texture that points to the same data.
func (this *Texture) Clip (bounds image.Rectangle) canvas.Texture {
clipped := *this
clipped.rect = bounds
return &clipped
}
// AssertTexture checks if a given canvas.Texture is a texture from this package.
func AssertTexture (unknown canvas.Texture) *Texture {
if unknown == nil {
return nil
}
if tx, ok := unknown.(*Texture); ok {
return tx
} else {
panic("foregin texture implementation, i did not make this!")
}
}

View File

@@ -5,14 +5,14 @@ import "git.tebibyte.media/tomo/tomo/canvas"
type canvasBox struct {
*box
userDrawer canvas.Drawer
}
func (backend *Backend) NewCanvasBox () tomo.CanvasBox {
box := &canvasBox {
box: backend.NewBox().(*box),
}
box.outer = box
return box
this := &canvasBox { }
this.box = backend.newBox(this)
this.drawer = this
return this
}
func (this *canvasBox) Box () tomo.Box {
@@ -20,10 +20,16 @@ func (this *canvasBox) Box () tomo.Box {
}
func (this *canvasBox) SetDrawer (drawer canvas.Drawer) {
this.drawer = drawer
this.userDrawer = drawer
this.invalidateDraw()
}
func (this *canvasBox) Invalidate () {
this.invalidateDraw()
}
func (this *canvasBox) Draw (can canvas.Canvas) {
this.box.Draw(can)
this.userDrawer.Draw (
can.Clip(this.padding.Apply(this.innerClippingBounds)))
}

View File

@@ -1,6 +1,7 @@
package x
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
@@ -12,11 +13,11 @@ type containerBox struct {
hAlign, vAlign tomo.Align
contentBounds image.Rectangle
scroll image.Point
capture [4]bool
gap image.Point
children []tomo.Box
layout tomo.Layout
propagateEvents bool
on struct {
contentBoundsChange event.FuncBroadcaster
@@ -24,17 +25,19 @@ type containerBox struct {
}
func (backend *Backend) NewContainerBox() tomo.ContainerBox {
box := &containerBox {
box: backend.NewBox().(*box),
propagateEvents: true,
}
box.drawer = box
box.outer = box
return box
this := &containerBox { }
this.box = backend.newBox(this)
return this
}
func (this *containerBox) Box () tomo.Box {
return this
func (this *containerBox) SetColor (c color.Color) {
this.box.SetColor(c)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTexture (texture canvas.Texture) {
this.box.SetTexture(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetOverflow (horizontal, vertical bool) {
@@ -65,50 +68,63 @@ func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback)
}
func (this *containerBox) SetPropagateEvents (propagate bool) {
this.propagateEvents = propagate
func (this *containerBox) CaptureDND (capture bool) {
this.capture[eventCategoryDND] = capture
}
func (this *containerBox) CaptureMouse (capture bool) {
this.capture[eventCategoryMouse] = capture
}
func (this *containerBox) CaptureScroll (capture bool) {
this.capture[eventCategoryScroll] = capture
}
func (this *containerBox) CaptureKeyboard (capture bool) {
this.capture[eventCategoryKeyboard] = capture
}
func (this *containerBox) SetGap (gap image.Point) {
if this.gap == gap { return }
this.gap = gap
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.Box())
box := assertAnyBox(child.GetBox())
if indexOf(this.children, tomo.Box(box)) > -1 { return }
box.setParent(this)
box.flushActionQueue()
this.children = append(this.children, box)
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Delete (child tomo.Object) {
box := assertAnyBox(child.Box())
box := assertAnyBox(child.GetBox())
index := indexOf(this.children, tomo.Box(box))
if index < 0 { return }
box.setParent(nil)
this.children = remove(this.children, index)
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Insert (child, before tomo.Object) {
box := assertAnyBox(child.Box())
box := assertAnyBox(child.GetBox())
if indexOf(this.children, tomo.Box(box)) > -1 { return }
beforeBox := assertAnyBox(before.Box())
beforeBox := assertAnyBox(before.GetBox())
index := indexOf(this.children, tomo.Box(beforeBox))
if index < 0 { return }
box.setParent(this)
this.children = insert(this.children, index, tomo.Box(box))
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Clear () {
@@ -117,7 +133,7 @@ func (this *containerBox) Clear () {
}
this.children = nil
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Length () int {
@@ -134,24 +150,59 @@ func (this *containerBox) At (index int) tomo.Object {
func (this *containerBox) SetLayout (layout tomo.Layout) {
this.layout = layout
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return }
this.drawBorders(can)
pen := can.Pen()
pen.Fill(this.color)
rocks := make([]image.Rectangle, len(this.children))
for index, box := range this.children {
rocks[index] = box.Bounds()
}
for _, tile := range canvas.Shatter(this.bounds, rocks...) {
pen.Rectangle(tile)
clipped := can.Clip(tile)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(clipped)
}
if clipped == nil { continue }
pen := clipped.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
pen.Rectangle(this.innerClippingBounds)
}
}
func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(this.innerClippingBounds)
}
func (this *containerBox) invalidateTransparentChildren () {
window := this.window()
if window == nil { return }
for _, box := range this.children {
box := assertAnyBox(box)
if box.transparent() {
window.invalidateDraw(box)
}
}
}
func (this *containerBox) flushActionQueue () {
for _, box := range this.children {
box.(anyBox).flushActionQueue()
}
this.box.flushActionQueue()
}
func (this *containerBox) window () *window {
if this.parent == nil { return nil }
return this.parent.window()
@@ -162,8 +213,7 @@ func (this *containerBox) canvas () canvas.Canvas {
}
func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.recalculateMinimumSize()
this.invalidateMinimum()
size := child.MinimumSize()
bounds := child.Bounds()
if bounds.Dx() < size.X || bounds.Dy() < size.Y {
@@ -183,18 +233,15 @@ func (this *containerBox) layoutHints () tomo.LayoutHints {
}
}
func (this *containerBox) recalculateMinimumSize () {
if this.layout == nil {
this.SetMinimumSize(image.Point { })
return
func (this *containerBox) contentMinimum () image.Point {
minimum := this.box.contentMinimum()
if this.layout != nil {
minimum = minimum.Add (
this.layout.MinimumSize (
this.layoutHints(),
this.children))
}
minimum := this.layout.MinimumSize(this.layoutHints(), this.children)
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
return minimum
}
func (this *containerBox) doLayout () {
@@ -216,15 +263,15 @@ func (this *containerBox) recursiveRedo () {
}
}
func (this *containerBox) boxUnder (point image.Point) anyBox {
if this.propagateEvents {
func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
if !this.capture[category] {
for _, box := range this.children {
candidate := box.(anyBox).boxUnder(point)
candidate := box.(anyBox).boxUnder(point, category)
if candidate != nil { return candidate }
}
}
return this.box.boxUnder(point)
return this.box.boxUnder(point, category)
}
func (this *containerBox) propagate (callback func (anyBox) bool) bool {
@@ -238,11 +285,15 @@ func (this *containerBox) propagate (callback func (anyBox) bool) bool {
func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
if !callback(this) { return false}
for _, box := range this.children {
box := box.(anyBox)
if !box.propagateAlt(callback) { return false }
}
return true
}
func (this *containerBox) captures (category eventCategory) bool {
return this.capture[category]
}

View File

@@ -8,6 +8,31 @@ import "git.tebibyte.media/tomo/xgbkb"
import "github.com/jezek/xgbutil/xevent"
import "git.tebibyte.media/tomo/tomo/input"
type scrollSum struct {
x, y int
}
// TODO: this needs to be configurable, we need a config api
const scrollDistance = 16
func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
if xgbkb.StateToModifiers(state).Shift {
switch button {
case 4: sum.x -= scrollDistance
case 5: sum.x += scrollDistance
case 6: sum.y -= scrollDistance
case 7: sum.y += scrollDistance
}
} else {
switch button {
case 4: sum.y -= scrollDistance
case 5: sum.y += scrollDistance
case 6: sum.x -= scrollDistance
case 7: sum.x += scrollDistance
}
}
}
var buttonCodeTable = map[xproto.Keysym] input.Key {
0xFFFFFF: input.KeyNone,
@@ -208,7 +233,7 @@ func (window *window) handleKeyPress (
} else if key == input.KeyEscape && window.shy {
window.Close()
} else if window.focused != nil {
window.focused.handleKeyDown(key, numberPad)
window.keyboardTarget().handleKeyDown(key, numberPad)
}
}
@@ -241,7 +266,7 @@ func (window *window) handleKeyRelease (
window.updateModifiers(keyEvent.State)
if window.focused != nil {
window.focused.handleKeyUp(key, numberPad)
window.keyboardTarget().handleKeyUp(key, numberPad)
}
}
@@ -261,20 +286,15 @@ func (window *window) handleButtonPress (
if !insideWindow && window.shy && !scrolling {
window.Close()
} else if scrolling {
// TODO
// underneath := window.scrollTargetChildAt(point)
// if underneath != nil {
// if child, ok := underneath.element.(ability.ScrollTarget); ok {
// sum := scrollSum { }
// sum.add(buttonEvent.Detail, window, buttonEvent.State)
// window.compressScrollSum(buttonEvent, &sum)
// child.HandleScroll (
// point, float64(sum.x), float64(sum.y),
// modifiers)
// }
// }
underneath := window.boxUnder(point, eventCategoryScroll)
if underneath != nil {
sum := scrollSum { }
sum.add(buttonEvent.Detail, window, buttonEvent.State)
window.compressScrollSum(buttonEvent, &sum)
underneath.handleScroll(float64(sum.x), float64(sum.y))
}
} else {
underneath := window.boxUnder(point)
underneath := window.boxUnder(point, eventCategoryMouse)
window.drags[buttonEvent.Detail] = underneath
if underneath != nil {
underneath.handleMouseDown(input.Button(buttonEvent.Detail))
@@ -293,6 +313,7 @@ func (window *window) handleButtonRelease (
window.updateModifiers(buttonEvent.State)
window.updateMousePosition(buttonEvent.EventX, buttonEvent.EventY)
dragging := window.drags[buttonEvent.Detail]
window.drags[buttonEvent.Detail] = nil
if dragging != nil {
dragging.handleMouseUp(input.Button(buttonEvent.Detail))
@@ -317,8 +338,11 @@ func (window *window) handleMotionNotify (
handled = true
}
underneath := window.boxUnder(image.Pt(x, y), eventCategoryMouse)
window.hover(underneath)
if !handled {
window.boxUnder(image.Pt(x, y)).handleMouseMove()
underneath.handleMouseMove()
}
}
@@ -387,6 +411,33 @@ func (window *window) compressConfigureNotify (
return
}
func (window *window) compressScrollSum (
firstEvent xproto.ButtonPressEvent,
sum *scrollSum,
) {
window.backend.x.Sync()
xevent.Read(window.backend.x, false)
for index, untypedEvent := range xevent.Peek(window.backend.x) {
if untypedEvent.Err != nil { continue }
typedEvent, ok := untypedEvent.Event.(xproto.ButtonPressEvent)
if !ok { continue }
if firstEvent.Event == typedEvent.Event &&
typedEvent.Detail >= 4 &&
typedEvent.Detail <= 7 {
sum.add(typedEvent.Detail, window, typedEvent.State)
defer func (index int) {
xevent.DequeueAt(window.backend.x, index)
} (index)
}
}
return
}
func (window *window) compressMotionNotify (
firstEvent xproto.MotionNotifyEvent,
) (

7
go.mod
View File

@@ -3,13 +3,12 @@ module git.tebibyte.media/tomo/x
go 1.20
require (
git.tebibyte.media/tomo/ggfx v0.4.0
git.tebibyte.media/tomo/tomo v0.18.0
git.tebibyte.media/tomo/typeset v0.5.0
git.tebibyte.media/tomo/tomo v0.29.0
git.tebibyte.media/tomo/typeset v0.7.0
git.tebibyte.media/tomo/xgbkb v1.0.1
github.com/jezek/xgb v1.1.0
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0
golang.org/x/image v0.9.0
golang.org/x/image v0.11.0
)
require (

16
go.sum
View File

@@ -1,10 +1,8 @@
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/ggfx v0.4.0 h1:3aUHeGS/yYWRV/zCDubBsXnik5ygkMnj/VgrM5Z75A4=
git.tebibyte.media/tomo/ggfx v0.4.0/go.mod h1:zPoz8BdVQyG2KhEmeGFQBK66V71i6Kj8oVFbrZaCwRA=
git.tebibyte.media/tomo/tomo v0.18.0 h1:PdkX9hVV0TZWZzHn6fLx+Vq6W6DwDYuaog4E7djm7FE=
git.tebibyte.media/tomo/tomo v0.18.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY=
git.tebibyte.media/tomo/typeset v0.5.0 h1:EyLFDIIsMKYm+DLB404zbG6JMDYLvWBBbLiHJoAUlKQ=
git.tebibyte.media/tomo/typeset v0.5.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/tomo v0.29.0 h1:uvdPaEQYcWH965y85SjIKwhLklnTbs+x6MRwLfdRvfo=
git.tebibyte.media/tomo/tomo v0.29.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/typeset v0.7.0 h1:JFpEuGmN6R2XSCvkINYxpH0AyYUqqs+dZYr6OSd91y0=
git.tebibyte.media/tomo/typeset v0.7.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
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=
@@ -18,8 +16,8 @@ github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0/go.mod h1:AHecLyFNy6
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.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g=
golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
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=
@@ -42,7 +40,7 @@ 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.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.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=

100
system.go
View File

@@ -36,17 +36,25 @@ type parent interface {
window () *window
canvas () canvas.Canvas
notifyMinimumSizeChange (anyBox)
drawBackgroundPart (canvas.Canvas)
captures (eventCategory) bool
}
type anyBox interface {
tomo.Box
doDraw ()
doLayout ()
setParent (parent)
recursiveRedo ()
canBeFocused () bool
boxUnder (image.Point) anyBox
recalculateMinimumSize ()
canvas.Drawer
doDraw ()
doLayout ()
doMinimumSize ()
contentMinimum () image.Point
setParent (parent)
getParent () parent
flushActionQueue ()
recursiveRedo ()
canBeFocused () bool
boxUnder (image.Point, eventCategory) anyBox
transparent () bool
propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool
@@ -56,12 +64,12 @@ type anyBox interface {
// handleDndEnter ()
// handleDndLeave ()
// handleDndDrop (data.Data)
// handleMouseEnter ()
// handleMouseLeave ()
handleMouseEnter ()
handleMouseLeave ()
handleMouseMove ()
handleMouseDown (input.Button)
handleMouseUp (input.Button)
// handleScroll (float64, float64)
handleScroll (float64, float64)
handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool)
}
@@ -81,12 +89,13 @@ func (window *window) SetRoot (root tomo.Object) {
if root == nil {
window.root = nil
} else {
box := assertAnyBox(root.Box())
box := assertAnyBox(root.GetBox())
box.setParent(window)
box.flushActionQueue()
window.invalidateLayout(box)
window.root = box
}
window.recalculateMinimumSize()
window.minimumClean = false
}
func (window *window) window () *window {
@@ -98,7 +107,11 @@ func (window *window) canvas () canvas.Canvas {
}
func (window *window) notifyMinimumSizeChange (anyBox) {
window.recalculateMinimumSize()
window.minimumClean = false
}
func (window *window) invalidateMinimum (box anyBox) {
window.needMinimum.Add(box)
}
func (window *window) invalidateDraw (box anyBox) {
@@ -106,35 +119,72 @@ func (window *window) invalidateDraw (box anyBox) {
}
func (window *window) invalidateLayout (box anyBox) {
// TODO: use a priority queue for this and have the value be the amount
// of parents a box has
window.needLayout.Add(box)
window.invalidateDraw(box)
}
func (window *window) focus (box anyBox) {
if window.focused == box { return }
previous := window.focused
window.focused = box
if previous != nil {
window.invalidateDraw(previous)
previous.handleFocusLeave()
}
if box != nil && box.canBeFocused() {
window.invalidateDraw(box)
box.handleFocusEnter()
}
}
func (window *window) hover (box anyBox) {
if window.hovered == box { return }
previous := window.hovered
window.hovered = box
if previous != nil {
previous.handleMouseLeave()
}
if box != nil {
box.handleMouseEnter()
}
}
func (window *window) anyFocused () bool {
return window.focused != nil
}
func (this *window) boxUnder (point image.Point) anyBox {
type eventCategory int; const (
eventCategoryDND eventCategory = iota
eventCategoryMouse
eventCategoryScroll
eventCategoryKeyboard
)
func (this *window) boxUnder (point image.Point, category eventCategory) anyBox {
if this.root == nil { return nil }
return this.root.boxUnder(point)
return this.root.boxUnder(point, category)
}
func (this *window) captures (eventCategory) bool {
return false
}
func (this *window) keyboardTarget () anyBox {
focused := this.window().focused
if focused == nil { return nil }
parent := focused.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.captures(eventCategoryKeyboard) {
return parentBox
}
parent = parentBox.getParent()
}
return focused
}
func (this *window) focusNext () {
@@ -193,7 +243,7 @@ func (window *window) afterEvent () {
childBounds = childBounds.Sub(childBounds.Min)
window.root.SetBounds(childBounds)
// full relayout/redraw
// full relayout/redraw
if window.root != nil {
window.root.recursiveRedo()
}
@@ -202,6 +252,12 @@ func (window *window) afterEvent () {
return
}
for len(window.needMinimum) > 0 {
window.needMinimum.Pop().doMinimumSize()
}
if !window.minimumClean {
window.doMinimumSize()
}
for len(window.needLayout) > 0 {
window.needLayout.Pop().doLayout()
}

View File

@@ -13,7 +13,7 @@ import "git.tebibyte.media/tomo/tomo/canvas"
type textBox struct {
*box
hOverflow, vOverflow bool
contentBounds image.Rectangle
scroll image.Point
@@ -23,15 +23,16 @@ type textBox struct {
face font.Face
wrap bool
hAlign tomo.Align
vAlign tomo.Align
selectable bool
selecting bool
selectStart int
dot text.Dot
dotColor color.Color
drawer typeset.Drawer
on struct {
contentBoundsChange event.FuncBroadcaster
dotChange event.FuncBroadcaster
@@ -39,14 +40,12 @@ type textBox struct {
}
func (backend *Backend) NewTextBox() tomo.TextBox {
box := &textBox {
box: backend.NewBox().(*box),
this := &textBox {
textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
}
box.box.drawer = box
box.outer = box
return box
this.box = backend.newBox(this)
return this
}
func (this *textBox) SetOverflow (horizontal, vertical bool) {
@@ -61,7 +60,7 @@ func (this *textBox) ContentBounds () image.Rectangle {
}
func (this *textBox) ScrollTo (point image.Point) {
// TODO: constrain scroll
if this.scroll == point { return }
this.scroll = point
this.invalidateLayout()
}
@@ -74,7 +73,7 @@ func (this *textBox) SetText (text string) {
if this.text == text { return }
this.text = text
this.drawer.SetText([]rune(text))
this.recalculateMinimumSize()
this.invalidateMinimum()
this.invalidateLayout()
}
@@ -88,14 +87,14 @@ func (this *textBox) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.drawer.SetFace(face)
this.recalculateMinimumSize()
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.drawer.SetWrap(wrap)
this.recalculateMinimumSize()
this.invalidateMinimum()
this.invalidateLayout()
}
@@ -117,6 +116,7 @@ func (this *textBox) Select (dot text.Dot) {
if this.dot == dot { return }
this.SetFocused(true)
this.dot = dot
this.scrollToDot()
this.on.dotChange.Broadcast()
this.invalidateDraw()
}
@@ -130,16 +130,10 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie {
}
func (this *textBox) SetAlign (x, y tomo.Align) {
if this.hAlign == x { return }
if this.hAlign == x && this.vAlign == y { return }
this.hAlign = x
switch x {
case tomo.AlignStart: this.drawer.SetAlign(typeset.AlignLeft)
case tomo.AlignMiddle: this.drawer.SetAlign(typeset.AlignCenter)
case tomo.AlignEnd: this.drawer.SetAlign(typeset.AlignRight)
case tomo.AlignEven: this.drawer.SetAlign(typeset.AlignJustify)
}
this.vAlign = y
this.drawer.SetAlign(typeset.Align(x), typeset.Align(y))
this.invalidateDraw()
}
@@ -148,6 +142,11 @@ func (this *textBox) Draw (can canvas.Canvas) {
this.drawBorders(can)
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(can.Bounds())
if this.selectable && this.Focused() {
@@ -167,10 +166,11 @@ func fixPt (point image.Point) fixed.Point26_6 {
}
func (this *textBox) drawDot (can canvas.Canvas) {
if this.face == nil { return }
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(this.textColor)
pen.StrokeWeight(1)
bounds := this.InnerBounds()
metrics := this.face.Metrics()
@@ -180,11 +180,12 @@ func (this *textBox) drawDot (can canvas.Canvas) {
height := this.drawer.LineHeight().Round()
ascent := fixed.Point26_6 { Y: metrics.Descent }
descent := fixed.Point26_6 { Y: metrics.Ascent }
switch {
case dot.Empty():
pen.StrokeWeight(1)
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
case start.Y == end.Y:
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
@@ -192,11 +193,11 @@ func (this *textBox) drawDot (can canvas.Canvas) {
Min: roundPt(start.Add(ascent)),
Max: roundPt(end.Sub(descent)),
})
default:
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
rect := image.Rectangle {
Min: roundPt(start.Add(ascent)),
Max: roundPt(start.Sub(descent)),
@@ -269,25 +270,17 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
return bounds.Sub(bounds.Min)
}
func (this *textBox) recalculateMinimumSize () {
minimum := image.Pt (
this.drawer.Em().Round(),
this.drawer.LineHeight().Round())
func (this *textBox) contentMinimum () image.Point {
minimum := this.drawer.MinimumSize()
textSize := this.drawer.MinimumSize()
if !this.hOverflow && !this.wrap {
minimum.X = textSize.X
if this.hOverflow || this.wrap {
minimum.X = this.drawer.Em().Round()
}
if !this.vOverflow {
minimum.Y = textSize.Y
if this.vOverflow {
minimum.Y = this.drawer.LineHeight().Round()
}
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
return minimum.Add(this.box.contentMinimum())
}
func (this *textBox) doLayout () {
@@ -295,12 +288,62 @@ func (this *textBox) doLayout () {
previousContentBounds := this.contentBounds
innerBounds := this.InnerBounds()
this.drawer.SetMaxWidth(innerBounds.Dx())
this.drawer.SetMaxHeight(innerBounds.Dy())
this.contentBounds = this.normalizedLayoutBoundsSpace().Sub(this.scroll)
this.drawer.SetWidth(innerBounds.Dx())
this.drawer.SetHeight(innerBounds.Dy())
this.contentBounds = this.normalizedLayoutBoundsSpace()
this.constrainScroll()
this.contentBounds = this.contentBounds.Add(this.scroll)
if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast()
}
}
func (this *textBox) constrainScroll () {
innerBounds := this.InnerBounds()
width := this.contentBounds.Dx()
height := this.contentBounds.Dy()
// X
if width <= innerBounds.Dx() {
this.scroll.X = 0
} else if this.scroll.X < 0 {
this.scroll.X = 0
} else if this.scroll.X > width - innerBounds.Dx() {
this.scroll.X = width - innerBounds.Dx()
}
// Y
if height <= innerBounds.Dy() {
this.scroll.Y = 0
} else if this.scroll.Y < 0 {
this.scroll.Y = 0
} else if this.scroll.Y > height - innerBounds.Dy() {
this.scroll.Y = height - innerBounds.Dy()
}
}
func (this *textBox) scrollToDot () {
dot := roundPt(this.drawer.PositionAt(this.dot.End)).Add(this.textOffset())
innerBounds := this.InnerBounds()
scroll := this.scroll
em := this.drawer.Em().Round()
lineHeight := this.drawer.LineHeight().Round()
// X
if dot.X < innerBounds.Min.X + em {
scroll.X -= innerBounds.Min.X - dot.X + em
} else if dot.X > innerBounds.Max.X - em {
scroll.X += dot.X - innerBounds.Max.X + em
}
// Y
if dot.Y < innerBounds.Min.Y + lineHeight {
scroll.Y -= innerBounds.Min.Y - dot.Y + lineHeight
} else if dot.Y > innerBounds.Max.Y - lineHeight {
scroll.Y += dot.Y - innerBounds.Max.Y + lineHeight
}
this.ScrollTo(scroll)
}

9
texture.go Normal file
View File

@@ -0,0 +1,9 @@
package x
import "image"
import "git.tebibyte.media/tomo/x/canvas"
import "git.tebibyte.media/tomo/tomo/canvas"
func (backend Backend) NewTexture (source image.Image) canvas.TextureCloser {
return xcanvas.NewTextureFrom(source)
}

View File

@@ -1,5 +1,7 @@
package x
import "image/color"
func indexOf[T comparable] (haystack []T, needle T) int {
for index, test := range haystack {
if test == needle {
@@ -18,3 +20,8 @@ func insert[T any] (slice []T, index int, element T) []T {
slice[index] = element
return slice
}
func transparent (c color.Color) bool {
_, _, _, a := c.RGBA()
return a != 0xFFFF
}

View File

@@ -8,6 +8,7 @@ import "git.tebibyte.media/tomo/x/canvas"
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"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/ewmh"
@@ -44,10 +45,19 @@ type window struct {
root anyBox
focused anyBox
hovered anyBox
needDraw boxSet
needLayout boxSet
needRedo bool
// TODO: needMinimum and needLayout should be priority queues. for the
// minimums, we need to start at the deeper parts of the layout tree and
// go upward towards the top. for the layouts, we need to start at the
// top of the layout tree and progressively go deeper. this will
// eliminate redundant layout calculations.
needMinimum boxSet
needLayout boxSet
needDraw boxSet
needRedo bool
minimumClean bool
}
func (backend *Backend) NewWindow (
@@ -63,6 +73,20 @@ func (backend *Backend) NewWindow (
return output, err
}
func (backend *Backend) NewPlainWindow (
bounds image.Rectangle,
) (
output tomo.MainWindow,
err error,
) {
backend.assert()
window, err := backend.newWindow(bounds, false)
window.setType("dock")
output = mainWindow { window: window }
return output, err
}
func (backend *Backend) newWindow (
bounds image.Rectangle,
override bool,
@@ -129,7 +153,7 @@ func (backend *Backend) newWindow (
// Connect(backend.x, window.xWindow.Id)
window.metrics.bounds = bounds
window.setMinimumSize(image.Pt(8, 8))
window.doMinimumSize()
backend.windows[window.xWindow.Id] = window
@@ -322,7 +346,7 @@ func (window *window) reallocateCanvas () {
if window.xCanvas != nil {
window.xCanvas.Destroy()
}
window.xCanvas = xcanvas.NewFrom(xgraphics.New (
window.xCanvas = xcanvas.NewCanvasFrom(xgraphics.New (
window.backend.x,
image.Rect (
0, 0,
@@ -352,15 +376,19 @@ func (window *window) pushRegion (region image.Rectangle) {
subCanvas.(*xcanvas.Canvas).Push(window.xWindow.Id)
}
func (window *window) recalculateMinimumSize () {
rootMinimum := image.Point { }
if window.root != nil {
rootMinimum = window.root.MinimumSize()
}
window.setMinimumSize(rootMinimum)
func (window *window) drawBackgroundPart (canvas.Canvas) {
// no-op for now? maybe eventually windows will be able to have a
// background
}
func (window *window) setMinimumSize (size image.Point) {
func (window *window) doMinimumSize () {
window.minimumClean = true
size := image.Point { }
if window.root != nil {
size = window.root.MinimumSize()
}
if size.X < 8 { size.X = 8 }
if size.Y < 8 { size.Y = 8 }
icccm.WmNormalHintsSet (

View File

@@ -5,7 +5,7 @@ import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/tomo"
func init () {
tomo.Register(x.NewBackend)
tomo.Register(0, tomo.Factory(x.NewBackend))
}
func Name () string {