19 Commits

Author SHA1 Message Date
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
18 changed files with 915 additions and 244 deletions

View File

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

257
box.go
View File

@@ -3,6 +3,7 @@ package x
import "image" import "image"
import "image/color" import "image/color"
import "git.tebibyte.media/tomo/tomo" 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/data"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
@@ -13,12 +14,18 @@ type box struct {
parent parent parent parent
outer anyBox outer anyBox
bounds image.Rectangle bounds image.Rectangle
minSize image.Point minSize image.Point
userMinSize image.Point
innerClippingBounds image.Rectangle
minSizeQueued bool
focusQueued *bool
padding tomo.Inset padding tomo.Inset
border []tomo.Border border []tomo.Border
color color.Color color color.Color
texture *xcanvas.Texture
dndData data.Data dndData data.Data
dndAccept []data.Mime 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 { box := &box {
backend: backend, backend: backend,
color: color.White, color: color.Transparent,
outer: outer,
drawer: outer,
} }
box.drawer = box if outer == nil {
box.outer = box box.drawer = box
box.outer = box
}
box.invalidateMinimum()
return box return box
} }
func (this *box) Box () tomo.Box { func (backend *Backend) NewBox() tomo.Box {
return this box := backend.newBox(nil)
return box
}
func (this *box) GetBox () tomo.Box {
return this.outer
} }
func (this *box) Window () tomo.Window { func (this *box) Window () tomo.Window {
@@ -68,7 +85,7 @@ func (this *box) Bounds () image.Rectangle {
} }
func (this *box) InnerBounds () 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 { func (this *box) MinimumSize () image.Point {
@@ -86,10 +103,6 @@ func (this *box) borderSum () tomo.Inset {
return sum return sum
} }
func (this *box) innerClippingBounds () image.Rectangle {
return this.borderSum().Apply(this.bounds)
}
func (this *box) SetBounds (bounds image.Rectangle) { func (this *box) SetBounds (bounds image.Rectangle) {
if this.bounds == bounds { return } if this.bounds == bounds { return }
this.bounds = bounds this.bounds = bounds
@@ -97,31 +110,34 @@ func (this *box) SetBounds (bounds image.Rectangle) {
} }
func (this *box) SetColor (c color.Color) { func (this *box) SetColor (c color.Color) {
if c == nil { c = color.Transparent }
if this.color == c { return } if this.color == c { return }
this.color = c this.color = c
this.invalidateDraw() this.invalidateDraw()
} }
func (this *box) SetTexture (texture canvas.Texture) {
this.texture = xcanvas.AssertTexture(texture)
this.invalidateDraw()
}
func (this *box) SetBorder (border ...tomo.Border) { func (this *box) SetBorder (border ...tomo.Border) {
this.border = border this.border = border
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *box) SetMinimumSize (size image.Point) { func (this *box) SetMinimumSize (size image.Point) {
if this.minSize == size { return } if this.userMinSize == size { return }
this.minSize = size this.userMinSize = size
this.invalidateMinimum()
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
} }
func (this *box) SetPadding (padding tomo.Inset) { func (this *box) SetPadding (padding tomo.Inset) {
if this.padding == padding { return } if this.padding == padding { return }
this.padding = padding this.padding = padding
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *box) SetDNDData (dat data.Data) { func (this *box) SetDNDData (dat data.Data) {
@@ -133,9 +149,11 @@ func (this *box) SetDNDAccept (types ...data.Mime) {
} }
func (this *box) SetFocused (focused bool) { func (this *box) SetFocused (focused bool) {
if this.parent == nil { return } if this.parent == nil || this.parent.window () == nil {
window := this.parent.window() focusedCopy := focused
if window == nil { return } this.focusQueued = &focusedCopy
return
}
if !this.focusable { return } if !this.focusable { return }
if this.Focused () && !focused { if this.Focused () && !focused {
@@ -208,58 +226,141 @@ 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 { func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Cookie {
return this.on.keyUp.Connect(callback) 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) 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) { func (this *box) Draw (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
pen := can.Pen() pen := can.Pen()
pen.Fill(this.color) 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) { func (this *box) drawBorders (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
pen := can.Pen() pen := can.Pen()
bounds := this.bounds 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 { for _, border := range this.border {
pen.Fill(border.Color[tomo.SideTop]) rectangle (
pen.Rectangle(image.Rect (
bounds.Min.X, bounds.Min.X,
bounds.Min.Y, bounds.Min.Y,
bounds.Max.X, bounds.Max.X,
bounds.Min.Y + border.Width[tomo.SideTop])) bounds.Min.Y + border.Width[tomo.SideTop],
pen.Fill(border.Color[tomo.SideBottom]) border.Color[tomo.SideTop])
pen.Rectangle(image.Rect ( rectangle (
bounds.Min.X, bounds.Min.X,
bounds.Max.Y - border.Width[tomo.SideBottom], bounds.Max.Y - border.Width[tomo.SideBottom],
bounds.Max.X, bounds.Max.X,
bounds.Max.Y)) bounds.Max.Y,
pen.Fill(border.Color[tomo.SideLeft]) border.Color[tomo.SideBottom])
pen.Rectangle(image.Rect ( rectangle (
bounds.Min.X, bounds.Min.X,
bounds.Min.Y + border.Width[tomo.SideTop], bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Min.X + border.Width[tomo.SideLeft], bounds.Min.X + border.Width[tomo.SideLeft],
bounds.Max.Y - border.Width[tomo.SideBottom])) bounds.Max.Y - border.Width[tomo.SideBottom],
pen.Fill(border.Color[tomo.SideRight]) border.Color[tomo.SideLeft])
pen.Rectangle(image.Rect ( rectangle (
bounds.Max.X - border.Width[tomo.SideRight], bounds.Max.X - border.Width[tomo.SideRight],
bounds.Min.Y + border.Width[tomo.SideTop], bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Max.X, 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) 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 () { func (this *box) doDraw () {
if this.canvas == nil { return } if this.canvas == nil { return }
if this.drawer != nil { if this.drawer != nil {
this.drawBorders(this.canvas) this.drawBorders(this.canvas)
this.drawer.Draw(this.canvas.Clip(this.innerClippingBounds())) this.drawer.Draw(this.canvas.Clip(this.innerClippingBounds))
} }
} }
func (this *box) doLayout () { func (this *box) doLayout () {
this.innerClippingBounds = this.borderSum().Apply(this.bounds)
if this.parent == nil { this.canvas = nil; return } if this.parent == nil { this.canvas = nil; return }
parentCanvas := this.parent.canvas() parentCanvas := this.parent.canvas()
if parentCanvas == nil { this.canvas = nil; return } if parentCanvas == nil { this.canvas = nil; return }
@@ -273,6 +374,17 @@ func (this *box) setParent (parent parent) {
this.parent = parent this.parent = 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 () { func (this *box) recursiveRedo () {
this.doLayout() this.doLayout()
this.doDraw() this.doDraw()
@@ -288,10 +400,12 @@ func (this *box) invalidateDraw () {
this.parent.window().invalidateDraw(this.outer) this.parent.window().invalidateDraw(this.outer)
} }
func (this *box) recalculateMinimumSize () { func (this *box) invalidateMinimum () {
if this.outer != anyBox(this) { if this.parent == nil || this.parent.window() == nil {
this.outer.recalculateMinimumSize() this.minSizeQueued = true
return
} }
this.parent.window().invalidateMinimum(this.outer)
} }
func (this *box) canBeFocused () bool { func (this *box) canBeFocused () bool {
@@ -306,62 +420,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 { 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 { 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 "image/color"
import "github.com/jezek/xgbutil" import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto" import "github.com/jezek/xgb/xproto"
import "git.tebibyte.media/tomo/ggfx"
import "github.com/jezek/xgbutil/xgraphics" import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
// Canvas satisfies the canvas.Canvas interface. It draws to an xgraphics.Image. // Canvas satisfies the canvas.Canvas interface. It draws to an xgraphics.Image.
// It must be closed after use.
type Canvas struct { type Canvas struct {
*xgraphics.Image *xgraphics.Image
} }
// New creates a new canvas from a bounding rectangle. // NewCanvas creates a new canvas from a bounding rectangle.
func New (x *xgbutil.XUtil, bounds image.Rectangle) *Canvas { func NewCanvas (x *xgbutil.XUtil, bounds image.Rectangle) *Canvas {
return NewFrom(xgraphics.New(x, bounds)) return NewCanvasFrom(xgraphics.New(x, bounds))
} }
// NewFrom creates a new canvas from an existing xgraphics.Image. // NewCanvasFrom creates a new canvas from an existing xgraphics.Image. Note
func NewFrom (image *xgraphics.Image) *Canvas { // that calling Close() on the resulting canvas will destroy this image.
func NewCanvasFrom (image *xgraphics.Image) *Canvas {
if image == nil { return nil } if image == nil { return nil }
return &Canvas { image } return &Canvas { image }
} }
@@ -28,12 +29,6 @@ func NewFrom (image *xgraphics.Image) *Canvas {
func (this *Canvas) Pen () canvas.Pen { func (this *Canvas) Pen () canvas.Pen {
return &pen { return &pen {
image: this.Image, 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) 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 () { func (this *Canvas) assert () {
if this == nil { panic("nil canvas") } if this == nil { panic("nil canvas") }
} }
@@ -64,50 +65,69 @@ func (this *Canvas) assert () {
type pen struct { type pen struct {
image *xgraphics.Image image *xgraphics.Image
gfx ggfx.Image[uint8]
closed bool closed bool
endCap canvas.Cap endCap canvas.Cap
joint canvas.Joint joint canvas.Joint
weight int weight int
align canvas.StrokeAlign align canvas.StrokeAlign
stroke [4]uint8 stroke xgraphics.BGRA
fill [4]uint8 fill xgraphics.BGRA
texture *Texture
} }
func (this *pen) Rectangle (bounds image.Rectangle) { func (this *pen) Rectangle (bounds image.Rectangle) {
bounds = bounds.Canon()
if this.weight == 0 { 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 { } 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) { func (this *pen) Path (points ...image.Point) {
if this.weight == 0 { if this.weight == 0 {
this.gfx.FillPolygon(this.fill[:], points...) if this.fill.A > 0 {
this.fillPolygon(this.fill, points...)
}
} else if this.closed { } else if this.closed {
this.gfx.StrokePolygon(this.stroke[:], this.weight, points...) if this.stroke.A > 0 {
this.strokePolygon(this.stroke, points...)
}
} else { } 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) Closed (closed bool) { this.closed = closed }
func (this *pen) Cap (endCap canvas.Cap) { this.endCap = endCap } func (this *pen) Cap (endCap canvas.Cap) { this.endCap = endCap }
func (this *pen) Joint (joint canvas.Joint) { this.joint = joint } func (this *pen) Joint (joint canvas.Joint) { this.joint = joint }
func (this *pen) StrokeWeight (weight int) { this.weight = weight } func (this *pen) StrokeWeight (weight int) { this.weight = weight }
func (this *pen) StrokeAlign (align canvas.StrokeAlign) { this.align = align } func (this *pen) StrokeAlign (align canvas.StrokeAlign) { this.align = align }
func (this *pen) Stroke (stroke color.Color) { this.stroke = convertColor(stroke) } 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) 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() r, g, b, a := c.RGBA()
return [4]uint8 { return xgraphics.BGRA {
uint8(b >> 8), B: uint8(b >> 8),
uint8(g >> 8), G: uint8(g >> 8),
uint8(r >> 8), R: uint8(r >> 8),
uint8(a >> 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
}

94
canvas/line.go Normal file
View File

@@ -0,0 +1,94 @@
package xcanvas
import "image"
import "github.com/jezek/xgbutil/xgraphics"
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
}}
}
}

75
canvas/texture.go Normal file
View File

@@ -0,0 +1,75 @@
package xcanvas
import "image"
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
}
// Opaque reports whether or not the texture is fully opaque.
func (this *Texture) Opaque () bool {
return !this.transparent
}
// 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
}
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
}
// AssertTexture checks if a given canvas.Texture is a texture from this package.
func AssertTexture (unknown canvas.Texture) *Texture {
if tx, ok := unknown.(*Texture); ok {
return tx
} else {
panic("foregin texture implementation, i did not make this!")
}
}

View File

@@ -8,11 +8,9 @@ type canvasBox struct {
} }
func (backend *Backend) NewCanvasBox () tomo.CanvasBox { func (backend *Backend) NewCanvasBox () tomo.CanvasBox {
box := &canvasBox { this := &canvasBox { }
box: backend.NewBox().(*box), this.box = backend.newBox(this)
} return this
box.outer = box
return box
} }
func (this *canvasBox) Box () tomo.Box { func (this *canvasBox) Box () tomo.Box {

View File

@@ -24,16 +24,8 @@ type containerBox struct {
} }
func (backend *Backend) NewContainerBox() tomo.ContainerBox { func (backend *Backend) NewContainerBox() tomo.ContainerBox {
box := &containerBox { this := &containerBox { propagateEvents: true }
box: backend.NewBox().(*box), this.box = backend.newBox(this)
propagateEvents: true,
}
box.drawer = box
box.outer = box
return box
}
func (this *containerBox) Box () tomo.Box {
return this return this
} }
@@ -73,42 +65,43 @@ func (this *containerBox) SetGap (gap image.Point) {
if this.gap == gap { return } if this.gap == gap { return }
this.gap = gap this.gap = gap
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Add (child tomo.Object) { func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.Box()) box := assertAnyBox(child.GetBox())
if indexOf(this.children, tomo.Box(box)) > -1 { return } if indexOf(this.children, tomo.Box(box)) > -1 { return }
box.setParent(this) box.setParent(this)
box.flushActionQueue()
this.children = append(this.children, box) this.children = append(this.children, box)
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Delete (child tomo.Object) { func (this *containerBox) Delete (child tomo.Object) {
box := assertAnyBox(child.Box()) box := assertAnyBox(child.GetBox())
index := indexOf(this.children, tomo.Box(box)) index := indexOf(this.children, tomo.Box(box))
if index < 0 { return } if index < 0 { return }
box.setParent(nil) box.setParent(nil)
this.children = remove(this.children, index) this.children = remove(this.children, index)
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Insert (child, before tomo.Object) { 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 } if indexOf(this.children, tomo.Box(box)) > -1 { return }
beforeBox := assertAnyBox(before.Box()) beforeBox := assertAnyBox(before.GetBox())
index := indexOf(this.children, tomo.Box(beforeBox)) index := indexOf(this.children, tomo.Box(beforeBox))
if index < 0 { return } if index < 0 { return }
box.setParent(this) box.setParent(this)
this.children = insert(this.children, index, tomo.Box(box)) this.children = insert(this.children, index, tomo.Box(box))
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Clear () { func (this *containerBox) Clear () {
@@ -117,7 +110,7 @@ func (this *containerBox) Clear () {
} }
this.children = nil this.children = nil
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Length () int { func (this *containerBox) Length () int {
@@ -134,24 +127,48 @@ func (this *containerBox) At (index int) tomo.Object {
func (this *containerBox) SetLayout (layout tomo.Layout) { func (this *containerBox) SetLayout (layout tomo.Layout) {
this.layout = layout this.layout = layout
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Draw (can canvas.Canvas) { func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
this.drawBorders(can)
pen := can.Pen()
pen.Fill(this.color)
rocks := make([]image.Rectangle, len(this.children)) rocks := make([]image.Rectangle, len(this.children))
for index, box := range this.children { for index, box := range this.children {
rocks[index] = box.Bounds() rocks[index] = box.Bounds()
} }
for _, tile := range canvas.Shatter(this.bounds, rocks...) { 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) flushActionQueue () {
for _, box := range this.children {
box.(anyBox).flushActionQueue()
}
this.box.flushActionQueue()
}
func (this *containerBox) window () *window { func (this *containerBox) window () *window {
if this.parent == nil { return nil } if this.parent == nil { return nil }
return this.parent.window() return this.parent.window()
@@ -162,8 +179,7 @@ func (this *containerBox) canvas () canvas.Canvas {
} }
func (this *containerBox) notifyMinimumSizeChange (child anyBox) { func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.recalculateMinimumSize() this.invalidateMinimum()
size := child.MinimumSize() size := child.MinimumSize()
bounds := child.Bounds() bounds := child.Bounds()
if bounds.Dx() < size.X || bounds.Dy() < size.Y { if bounds.Dx() < size.X || bounds.Dy() < size.Y {
@@ -183,18 +199,15 @@ func (this *containerBox) layoutHints () tomo.LayoutHints {
} }
} }
func (this *containerBox) recalculateMinimumSize () { func (this *containerBox) contentMinimum () image.Point {
if this.layout == nil { minimum := this.box.contentMinimum()
this.SetMinimumSize(image.Point { }) if this.layout != nil {
return minimum = minimum.Add (
this.layout.MinimumSize (
this.layoutHints(),
this.children))
} }
minimum := this.layout.MinimumSize(this.layoutHints(), this.children) return minimum
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
} }
func (this *containerBox) doLayout () { func (this *containerBox) doLayout () {

View File

@@ -293,6 +293,7 @@ func (window *window) handleButtonRelease (
window.updateModifiers(buttonEvent.State) window.updateModifiers(buttonEvent.State)
window.updateMousePosition(buttonEvent.EventX, buttonEvent.EventY) window.updateMousePosition(buttonEvent.EventX, buttonEvent.EventY)
dragging := window.drags[buttonEvent.Detail] dragging := window.drags[buttonEvent.Detail]
window.drags[buttonEvent.Detail] = nil
if dragging != nil { if dragging != nil {
dragging.handleMouseUp(input.Button(buttonEvent.Detail)) dragging.handleMouseUp(input.Button(buttonEvent.Detail))
@@ -317,8 +318,11 @@ func (window *window) handleMotionNotify (
handled = true handled = true
} }
underneath := window.boxUnder(image.Pt(x, y))
window.hover(underneath)
if !handled { if !handled {
window.boxUnder(image.Pt(x, y)).handleMouseMove() underneath.handleMouseMove()
} }
} }

7
go.mod
View File

@@ -3,13 +3,12 @@ module git.tebibyte.media/tomo/x
go 1.20 go 1.20
require ( require (
git.tebibyte.media/tomo/ggfx v0.4.0 git.tebibyte.media/tomo/tomo v0.26.1
git.tebibyte.media/tomo/tomo v0.18.0 git.tebibyte.media/tomo/typeset v0.5.2
git.tebibyte.media/tomo/typeset v0.5.0
git.tebibyte.media/tomo/xgbkb v1.0.1 git.tebibyte.media/tomo/xgbkb v1.0.1
github.com/jezek/xgb v1.1.0 github.com/jezek/xgb v1.1.0
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0
golang.org/x/image v0.9.0 golang.org/x/image v0.11.0
) )
require ( 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/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/tomo v0.26.1 h1:V5ciRuixMYb79aAawgquFEfJ1icyEmMKBKFPWwi94NE=
git.tebibyte.media/tomo/ggfx v0.4.0/go.mod h1:zPoz8BdVQyG2KhEmeGFQBK66V71i6Kj8oVFbrZaCwRA= git.tebibyte.media/tomo/tomo v0.26.1/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/tomo v0.18.0 h1:PdkX9hVV0TZWZzHn6fLx+Vq6W6DwDYuaog4E7djm7FE= git.tebibyte.media/tomo/typeset v0.5.2 h1:qHxN62/VDnrAouOuzxLmLleQNwAebshrfVYvtoOnAG4=
git.tebibyte.media/tomo/tomo v0.18.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= git.tebibyte.media/tomo/typeset v0.5.2/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
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/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE= git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE=
git.tebibyte.media/tomo/xgbkb v1.0.1/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw= 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 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= 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-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/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.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= 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.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/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-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.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.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.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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@@ -36,17 +36,22 @@ type parent interface {
window () *window window () *window
canvas () canvas.Canvas canvas () canvas.Canvas
notifyMinimumSizeChange (anyBox) notifyMinimumSizeChange (anyBox)
drawBackgroundPart (canvas.Canvas)
} }
type anyBox interface { type anyBox interface {
tomo.Box tomo.Box
doDraw () canvas.Drawer
doLayout ()
setParent (parent) doDraw ()
recursiveRedo () doLayout ()
canBeFocused () bool doMinimumSize ()
boxUnder (image.Point) anyBox contentMinimum () image.Point
recalculateMinimumSize () setParent (parent)
flushActionQueue ()
recursiveRedo ()
canBeFocused () bool
boxUnder (image.Point) anyBox
propagate (func (anyBox) bool) bool propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool propagateAlt (func (anyBox) bool) bool
@@ -56,8 +61,8 @@ type anyBox interface {
// handleDndEnter () // handleDndEnter ()
// handleDndLeave () // handleDndLeave ()
// handleDndDrop (data.Data) // handleDndDrop (data.Data)
// handleMouseEnter () handleMouseEnter ()
// handleMouseLeave () handleMouseLeave ()
handleMouseMove () handleMouseMove ()
handleMouseDown (input.Button) handleMouseDown (input.Button)
handleMouseUp (input.Button) handleMouseUp (input.Button)
@@ -81,12 +86,13 @@ func (window *window) SetRoot (root tomo.Object) {
if root == nil { if root == nil {
window.root = nil window.root = nil
} else { } else {
box := assertAnyBox(root.Box()) box := assertAnyBox(root.GetBox())
box.setParent(window) box.setParent(window)
box.flushActionQueue()
window.invalidateLayout(box) window.invalidateLayout(box)
window.root = box window.root = box
} }
window.recalculateMinimumSize() window.minimumClean = false
} }
func (window *window) window () *window { func (window *window) window () *window {
@@ -98,7 +104,11 @@ func (window *window) canvas () canvas.Canvas {
} }
func (window *window) notifyMinimumSizeChange (anyBox) { 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) { func (window *window) invalidateDraw (box anyBox) {
@@ -106,8 +116,6 @@ func (window *window) invalidateDraw (box anyBox) {
} }
func (window *window) invalidateLayout (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.needLayout.Add(box)
window.invalidateDraw(box) window.invalidateDraw(box)
} }
@@ -119,15 +127,27 @@ func (window *window) focus (box anyBox) {
window.focused = box window.focused = box
if previous != nil { if previous != nil {
window.invalidateDraw(previous)
previous.handleFocusLeave() previous.handleFocusLeave()
} }
if box != nil && box.canBeFocused() { if box != nil && box.canBeFocused() {
window.invalidateDraw(box)
box.handleFocusEnter() 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 { func (window *window) anyFocused () bool {
return window.focused != nil return window.focused != nil
} }
@@ -202,6 +222,12 @@ func (window *window) afterEvent () {
return return
} }
for len(window.needMinimum) > 0 {
window.needMinimum.Pop().doMinimumSize()
}
if !window.minimumClean {
window.doMinimumSize()
}
for len(window.needLayout) > 0 { for len(window.needLayout) > 0 {
window.needLayout.Pop().doLayout() window.needLayout.Pop().doLayout()
} }

View File

@@ -13,7 +13,7 @@ import "git.tebibyte.media/tomo/tomo/canvas"
type textBox struct { type textBox struct {
*box *box
hOverflow, vOverflow bool hOverflow, vOverflow bool
contentBounds image.Rectangle contentBounds image.Rectangle
scroll image.Point scroll image.Point
@@ -29,9 +29,9 @@ type textBox struct {
selectStart int selectStart int
dot text.Dot dot text.Dot
dotColor color.Color dotColor color.Color
drawer typeset.Drawer drawer typeset.Drawer
on struct { on struct {
contentBoundsChange event.FuncBroadcaster contentBoundsChange event.FuncBroadcaster
dotChange event.FuncBroadcaster dotChange event.FuncBroadcaster
@@ -39,14 +39,12 @@ type textBox struct {
} }
func (backend *Backend) NewTextBox() tomo.TextBox { func (backend *Backend) NewTextBox() tomo.TextBox {
box := &textBox { this := &textBox {
box: backend.NewBox().(*box),
textColor: color.Black, textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 }, dotColor: color.RGBA { B: 255, G: 255, A: 255 },
} }
box.box.drawer = box this.box = backend.newBox(this)
box.outer = box return this
return box
} }
func (this *textBox) SetOverflow (horizontal, vertical bool) { func (this *textBox) SetOverflow (horizontal, vertical bool) {
@@ -74,7 +72,7 @@ func (this *textBox) SetText (text string) {
if this.text == text { return } if this.text == text { return }
this.text = text this.text = text
this.drawer.SetText([]rune(text)) this.drawer.SetText([]rune(text))
this.recalculateMinimumSize() this.invalidateMinimum()
this.invalidateLayout() this.invalidateLayout()
} }
@@ -88,14 +86,14 @@ func (this *textBox) SetFace (face font.Face) {
if this.face == face { return } if this.face == face { return }
this.face = face this.face = face
this.drawer.SetFace(face) this.drawer.SetFace(face)
this.recalculateMinimumSize() this.invalidateMinimum()
this.invalidateLayout() this.invalidateLayout()
} }
func (this *textBox) SetWrap (wrap bool) { func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return } if this.wrap == wrap { return }
this.drawer.SetWrap(wrap) this.drawer.SetWrap(wrap)
this.recalculateMinimumSize() this.invalidateMinimum()
this.invalidateLayout() this.invalidateLayout()
} }
@@ -139,7 +137,7 @@ func (this *textBox) SetAlign (x, y tomo.Align) {
case tomo.AlignEnd: this.drawer.SetAlign(typeset.AlignRight) case tomo.AlignEnd: this.drawer.SetAlign(typeset.AlignRight)
case tomo.AlignEven: this.drawer.SetAlign(typeset.AlignJustify) case tomo.AlignEven: this.drawer.SetAlign(typeset.AlignJustify)
} }
this.invalidateDraw() this.invalidateDraw()
} }
@@ -148,6 +146,11 @@ func (this *textBox) Draw (can canvas.Canvas) {
this.drawBorders(can) this.drawBorders(can)
pen := can.Pen() pen := can.Pen()
pen.Fill(this.color) pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(can.Bounds()) pen.Rectangle(can.Bounds())
if this.selectable && this.Focused() { if this.selectable && this.Focused() {
@@ -167,10 +170,11 @@ func fixPt (point image.Point) fixed.Point26_6 {
} }
func (this *textBox) drawDot (can canvas.Canvas) { func (this *textBox) drawDot (can canvas.Canvas) {
if this.face == nil { return }
pen := can.Pen() pen := can.Pen()
pen.Fill(color.Transparent) pen.Fill(color.Transparent)
pen.Stroke(this.textColor) pen.Stroke(this.textColor)
pen.StrokeWeight(1)
bounds := this.InnerBounds() bounds := this.InnerBounds()
metrics := this.face.Metrics() metrics := this.face.Metrics()
@@ -180,11 +184,12 @@ func (this *textBox) drawDot (can canvas.Canvas) {
height := this.drawer.LineHeight().Round() height := this.drawer.LineHeight().Round()
ascent := fixed.Point26_6 { Y: metrics.Descent } ascent := fixed.Point26_6 { Y: metrics.Descent }
descent := fixed.Point26_6 { Y: metrics.Ascent } descent := fixed.Point26_6 { Y: metrics.Ascent }
switch { switch {
case dot.Empty(): case dot.Empty():
pen.StrokeWeight(1)
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent))) pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
case start.Y == end.Y: case start.Y == end.Y:
pen.Fill(this.dotColor) pen.Fill(this.dotColor)
pen.StrokeWeight(0) pen.StrokeWeight(0)
@@ -192,11 +197,11 @@ func (this *textBox) drawDot (can canvas.Canvas) {
Min: roundPt(start.Add(ascent)), Min: roundPt(start.Add(ascent)),
Max: roundPt(end.Sub(descent)), Max: roundPt(end.Sub(descent)),
}) })
default: default:
pen.Fill(this.dotColor) pen.Fill(this.dotColor)
pen.StrokeWeight(0) pen.StrokeWeight(0)
rect := image.Rectangle { rect := image.Rectangle {
Min: roundPt(start.Add(ascent)), Min: roundPt(start.Add(ascent)),
Max: roundPt(start.Sub(descent)), Max: roundPt(start.Sub(descent)),
@@ -269,7 +274,7 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
return bounds.Sub(bounds.Min) return bounds.Sub(bounds.Min)
} }
func (this *textBox) recalculateMinimumSize () { func (this *textBox) contentMinimum () image.Point {
minimum := image.Pt ( minimum := image.Pt (
this.drawer.Em().Round(), this.drawer.Em().Round(),
this.drawer.LineHeight().Round()) this.drawer.LineHeight().Round())
@@ -281,13 +286,8 @@ func (this *textBox) recalculateMinimumSize () {
if !this.vOverflow { if !this.vOverflow {
minimum.Y = textSize.Y minimum.Y = textSize.Y
} }
minimum.X += this.padding.Horizontal() return minimum.Add(this.box.contentMinimum())
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
} }
func (this *textBox) doLayout () { func (this *textBox) doLayout () {
@@ -297,9 +297,9 @@ func (this *textBox) doLayout () {
innerBounds := this.InnerBounds() innerBounds := this.InnerBounds()
this.drawer.SetMaxWidth(innerBounds.Dx()) this.drawer.SetMaxWidth(innerBounds.Dx())
this.drawer.SetMaxHeight(innerBounds.Dy()) this.drawer.SetMaxHeight(innerBounds.Dy())
this.contentBounds = this.normalizedLayoutBoundsSpace().Sub(this.scroll) this.contentBounds = this.normalizedLayoutBoundsSpace().Sub(this.scroll)
if previousContentBounds != this.contentBounds { if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast() this.on.contentBoundsChange.Broadcast()
} }

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.Texture {
return xcanvas.NewTextureFrom(source)
}

View File

@@ -1,5 +1,7 @@
package x package x
import "image/color"
func indexOf[T comparable] (haystack []T, needle T) int { func indexOf[T comparable] (haystack []T, needle T) int {
for index, test := range haystack { for index, test := range haystack {
if test == needle { if test == needle {
@@ -18,3 +20,8 @@ func insert[T any] (slice []T, index int, element T) []T {
slice[index] = element slice[index] = element
return slice 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/data"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "github.com/jezek/xgb/xproto" import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/ewmh" import "github.com/jezek/xgbutil/ewmh"
@@ -44,10 +45,19 @@ type window struct {
root anyBox root anyBox
focused anyBox focused anyBox
hovered anyBox
needDraw boxSet // TODO: needMinimum and needLayout should be priority queues. for the
needLayout boxSet // minimums, we need to start at the deeper parts of the layout tree and
needRedo bool // 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 ( func (backend *Backend) NewWindow (
@@ -129,7 +139,7 @@ func (backend *Backend) newWindow (
// Connect(backend.x, window.xWindow.Id) // Connect(backend.x, window.xWindow.Id)
window.metrics.bounds = bounds window.metrics.bounds = bounds
window.setMinimumSize(image.Pt(8, 8)) window.doMinimumSize()
backend.windows[window.xWindow.Id] = window backend.windows[window.xWindow.Id] = window
@@ -322,7 +332,7 @@ func (window *window) reallocateCanvas () {
if window.xCanvas != nil { if window.xCanvas != nil {
window.xCanvas.Destroy() window.xCanvas.Destroy()
} }
window.xCanvas = xcanvas.NewFrom(xgraphics.New ( window.xCanvas = xcanvas.NewCanvasFrom(xgraphics.New (
window.backend.x, window.backend.x,
image.Rect ( image.Rect (
0, 0, 0, 0,
@@ -352,15 +362,19 @@ func (window *window) pushRegion (region image.Rectangle) {
subCanvas.(*xcanvas.Canvas).Push(window.xWindow.Id) subCanvas.(*xcanvas.Canvas).Push(window.xWindow.Id)
} }
func (window *window) recalculateMinimumSize () { func (window *window) drawBackgroundPart (canvas.Canvas) {
rootMinimum := image.Point { } // no-op for now? maybe eventually windows will be able to have a
if window.root != nil { // background
rootMinimum = window.root.MinimumSize()
}
window.setMinimumSize(rootMinimum)
} }
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.X < 8 { size.X = 8 }
if size.Y < 8 { size.Y = 8 } if size.Y < 8 { size.Y = 8 }
icccm.WmNormalHintsSet ( icccm.WmNormalHintsSet (

View File

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