26 Commits

Author SHA1 Message Date
a64c27953a Don't invalidate ContainerBox's children when something is set
redundantly
2024-05-15 01:18:58 -04:00
8f555f82ee Don't invalidate box if texture was set redundantly 2024-05-15 01:18:17 -04:00
79f81688bb SetBorder compares borders before doing re-layout/draw 2024-05-15 01:07:52 -04:00
b926881233 Box draws texture according to its bounds 2024-05-14 00:19:16 -04:00
b9092eae87 Fix TextBox scroll behavior 2024-05-13 19:39:36 -04:00
96fa7b5623 Fix scroll behavior for ContainerBox 2024-05-13 19:35:03 -04:00
664ce5f556 Add scroll code for ContainerBox 2024-05-13 17:43:26 -04:00
c409bc1a1e Don't crash when user hovers over nothing 2024-05-07 20:14:46 -04:00
6a2dc36140 Fix ContainerBox child insertion 2024-05-07 20:11:58 -04:00
0e2ad5bb4f Upgrade typeset version 2024-05-05 02:38:18 -04:00
78e13ed045 Removed plugin 2024-05-03 13:32:41 -04:00
0c540d0e41 Upgrade Tomo version 2024-05-03 13:32:01 -04:00
3951b2ffda Upgrade tomo version 2024-04-29 01:11:49 -04:00
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
17 changed files with 540 additions and 163 deletions

View File

@@ -1,3 +1,14 @@
# x # 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
```

89
box.go
View File

@@ -17,6 +17,7 @@ type box struct {
bounds image.Rectangle bounds image.Rectangle
minSize image.Point minSize image.Point
userMinSize image.Point userMinSize image.Point
innerClippingBounds image.Rectangle
minSizeQueued bool minSizeQueued bool
focusQueued *bool focusQueued *bool
@@ -25,8 +26,6 @@ type box struct {
border []tomo.Border border []tomo.Border
color color.Color color color.Color
texture *xcanvas.Texture texture *xcanvas.Texture
fillTransparent bool
dndData data.Data dndData data.Data
dndAccept []data.Mime dndAccept []data.Mime
@@ -56,7 +55,7 @@ type box struct {
func (backend *Backend) newBox (outer anyBox) *box { func (backend *Backend) newBox (outer anyBox) *box {
box := &box { box := &box {
backend: backend, backend: backend,
color: color.White, color: color.Transparent,
outer: outer, outer: outer,
drawer: outer, drawer: outer,
} }
@@ -86,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 {
@@ -104,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
@@ -118,21 +113,38 @@ func (this *box) SetColor (c color.Color) {
if c == nil { c = color.Transparent } if c == nil { c = color.Transparent }
if this.color == c { return } if this.color == c { return }
this.color = c this.color = c
this.determineFillTransparency()
this.invalidateDraw() this.invalidateDraw()
} }
func (this *box) SetTexture (texture canvas.Texture) { func (this *box) SetTexture (texture canvas.Texture) {
if this.texture == texture { return }
this.texture = xcanvas.AssertTexture(texture) this.texture = xcanvas.AssertTexture(texture)
this.determineFillTransparency()
this.invalidateDraw() this.invalidateDraw()
} }
func (this *box) SetBorder (border ...tomo.Border) { func (this *box) SetBorder (borders ...tomo.Border) {
this.border = border previousBorderSum := this.borderSum()
this.determineFillTransparency() previousBorders := this.border
this.invalidateLayout() this.border = borders
this.invalidateMinimum()
// only invalidate the layout if the border is sized differently
if this.borderSum() != previousBorderSum {
this.invalidateLayout()
this.invalidateMinimum()
return
}
// if the border takes up the same amount of space, only invalidate the
// drawing if it looks different
for index, newBorder := range this.border {
different :=
index >= len(previousBorders) ||
newBorder != previousBorders[index]
if different {
this.invalidateDraw()
return
}
}
} }
func (this *box) SetMinimumSize (size image.Point) { func (this *box) SetMinimumSize (size image.Point) {
@@ -267,6 +279,11 @@ func (this *box) handleMouseUp (button input.Button) {
listener(button) 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) { func (this *box) handleKeyDown (key input.Key, numberPad bool) {
for _, listener := range this.on.keyDown.Listeners() { for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad) listener(key, numberPad)
@@ -283,12 +300,12 @@ 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)
if this.texture == nil || !this.texture.Opaque() { pen.Texture(this.texture)
pen.Rectangle(this.bounds)
} if this.transparent() && this.parent != nil {
if this.texture != nil { this.parent.drawBackgroundPart(can)
// TODO drawR texture
} }
pen.Rectangle(this.Bounds())
} }
func (this *box) drawBorders (can canvas.Canvas) { func (this *box) drawBorders (can canvas.Canvas) {
@@ -298,8 +315,7 @@ func (this *box) drawBorders (can canvas.Canvas) {
rectangle := func (x0, y0, x1, y1 int, c color.Color) { rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1) area := image.Rect(x0, y0, x1, y1)
_, _, _, a := c.RGBA() if transparent(c) && this.parent != nil {
if a != 0xFFFF && this.parent != nil {
this.parent.drawBackgroundPart(can.Clip(area)) this.parent.drawBackgroundPart(can.Clip(area))
} }
pen.Fill(c) pen.Fill(c)
@@ -360,15 +376,24 @@ func (this *box) doMinimumSize () {
} }
} }
// var drawcnt int
func (this *box) doDraw () { func (this *box) doDraw () {
// println("DRAW", drawcnt)
// drawcnt ++
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))
} }
} }
// var laycnt int
func (this *box) doLayout () { func (this *box) doLayout () {
// println("LAYOUT", laycnt)
// laycnt ++
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 }
@@ -382,6 +407,10 @@ func (this *box) setParent (parent parent) {
this.parent = parent this.parent = parent
} }
func (this *box) getParent () parent {
return this.parent
}
func (this *box) flushActionQueue () { func (this *box) flushActionQueue () {
if this.parent == nil || this.parent.window() == nil { return } if this.parent == nil || this.parent.window() == nil { return }
@@ -420,7 +449,7 @@ func (this *box) canBeFocused () bool {
return this.focusable 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) { if point.In(this.bounds) {
return this.outer return this.outer
} else { } else {
@@ -428,13 +457,6 @@ func (this *box) boxUnder (point image.Point) anyBox {
} }
} }
func (this *box) determineFillTransparency () {
_, _, _, a := this.color.RGBA()
this.fillTransparent =
a != 0xFFFF &&
!(this.texture != nil && this.texture.Opaque())
}
func (this *box) propagate (callback func (anyBox) bool) bool { func (this *box) propagate (callback func (anyBox) bool) bool {
return callback(this.outer) return callback(this.outer)
} }
@@ -442,3 +464,8 @@ func (this *box) propagate (callback func (anyBox) bool) bool {
func (this *box) propagateAlt (callback func (anyBox) bool) bool { func (this *box) propagateAlt (callback func (anyBox) bool) bool {
return callback(this.outer) return callback(this.outer)
} }
func (this *box) transparent () bool {
return transparent(this.color) &&
(this.texture == nil || !this.texture.Opaque())
}

View File

@@ -79,9 +79,12 @@ type pen struct {
func (this *pen) Rectangle (bounds image.Rectangle) { func (this *pen) Rectangle (bounds image.Rectangle) {
bounds = bounds.Canon() bounds = bounds.Canon()
if this.weight == 0 { if this.weight == 0 {
if this.fill.A > 0 { if this.fill.A > 0 && !this.textureObscures() {
this.fillRectangle(this.fill, bounds) this.fillRectangle(this.fill, bounds)
} }
if this.texture != nil {
this.textureRectangle(bounds)
}
} else { } else {
if this.stroke.A > 0 { if this.stroke.A > 0 {
this.strokeRectangle(this.stroke, bounds) this.strokeRectangle(this.stroke, bounds)
@@ -94,7 +97,7 @@ func (this *pen) Path (points ...image.Point) {
if this.fill.A > 0 { if this.fill.A > 0 {
this.fillPolygon(this.fill, points...) this.fillPolygon(this.fill, points...)
} }
} else if this.closed { } else if this.closed && len(points) > 2 {
if this.stroke.A > 0 { if this.stroke.A > 0 {
this.strokePolygon(this.stroke, points...) this.strokePolygon(this.stroke, points...)
} }
@@ -115,6 +118,10 @@ func (this *pen) Stroke (stroke color.Color) { this.stroke = convertColor(
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 (this *pen) Texture (texture canvas.Texture) { this.texture = AssertTexture(texture) }
func (this *pen) textureObscures () bool {
return this.texture != nil && this.texture.Opaque()
}
func convertColor (c color.Color) xgraphics.BGRA { func convertColor (c color.Color) xgraphics.BGRA {
r, g, b, a := c.RGBA() r, g, b, a := c.RGBA()
return xgraphics.BGRA { return xgraphics.BGRA {

View File

@@ -4,6 +4,65 @@ import "sort"
import "image" import "image"
import "github.com/jezek/xgbutil/xgraphics" 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) { func (this *pen) fillRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
if c.A == 255 { if c.A == 255 {
this.fillRectangleOpaque(c, bounds) this.fillRectangleOpaque(c, bounds)
@@ -51,7 +110,7 @@ func (this *pen) strokeRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
this.fillRectangle(c, bounds) this.fillRectangle(c, bounds)
return return
} }
top := image.Rect ( top := image.Rect (
bounds.Min.X, bounds.Min.X,
bounds.Min.Y, bounds.Min.Y,
@@ -82,7 +141,7 @@ func (this *pen) strokeRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
// the polygon filling algorithm is adapted from: // the polygon filling algorithm is adapted from:
// https://www.alienryderflex.com/polygon_fill/ // https://www.alienryderflex.com/polygon_fill/
// (if you write C like that i will disassemble you) // (if you write C like that i will disassemble you)
func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) { func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) {
if len(points) < 3 { return } if len(points) < 3 { return }
@@ -143,7 +202,7 @@ func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) {
context.fillPolygonHotTransparent() context.fillPolygonHotTransparent()
} }
} }
} }
type fillingContext struct { type fillingContext struct {
@@ -167,7 +226,7 @@ func (context *fillingContext) fillPolygonHotOpaque () {
// constrain boundaries to image size // constrain boundaries to image size
if left < context.min { left = context.min } if left < context.min { left = context.min }
if right > context.max { right = context.max } if right > context.max { right = context.max }
// fill pixels in between // fill pixels in between
for x := left; x < right; x ++ { for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y) index := context.image.PixOffset(x, context.y)
@@ -192,7 +251,7 @@ func (context *fillingContext) fillPolygonHotTransparent () {
// constrain boundaries to image size // constrain boundaries to image size
if left < context.min { left = context.min } if left < context.min { left = context.min }
if right > context.max { right = context.max } if right > context.max { right = context.max }
// fill pixels in between // fill pixels in between
for x := left; x < right; x ++ { for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y) index := context.image.PixOffset(x, context.y)
@@ -227,3 +286,11 @@ func (this *pen) polyLine (c xgraphics.BGRA, points ...image.Point) {
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
}

View File

@@ -3,6 +3,7 @@ package xcanvas
import "image" import "image"
import "github.com/jezek/xgbutil/xgraphics" import "github.com/jezek/xgbutil/xgraphics"
// TODO: clip the line to the bounds
func (this *pen) line ( func (this *pen) line (
c xgraphics.BGRA, c xgraphics.BGRA,
min image.Point, min image.Point,
@@ -17,11 +18,11 @@ func (this *pen) line (
min: min, min: min,
max: max, max: max,
} }
if abs(max.Y - min.Y) < abs(max.X - min.X) { if abs(max.Y - min.Y) < abs(max.X - min.X) {
if max.X < min.X { context.swap() } if max.X < min.X { context.swap() }
context.lineLow() context.lineLow()
} else { } else {
if max.Y < min.Y { context.swap() } if max.Y < min.Y { context.swap() }
context.lineHigh() context.lineHigh()

View File

@@ -1,6 +1,8 @@
package xcanvas package xcanvas
import "image" import "image"
import "image/color"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
// Texture is a read-only image texture that can be quickly written to a canvas. // Texture is a read-only image texture that can be quickly written to a canvas.
@@ -17,7 +19,7 @@ func NewTextureFrom (source image.Image) *Texture {
bounds := source.Bounds() bounds := source.Bounds()
texture := &Texture { texture := &Texture {
pix: make([]uint8, bounds.Dx() * bounds.Dy() * 4), pix: make([]uint8, bounds.Dx() * bounds.Dy() * 4),
stride: bounds.Dx(), stride: bounds.Dx() * 4,
rect: bounds.Sub(bounds.Min), rect: bounds.Sub(bounds.Min),
} }
@@ -36,15 +38,47 @@ func NewTextureFrom (source image.Image) *Texture {
texture.transparent = true texture.transparent = true
} }
}} }}
return texture 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. // Opaque reports whether or not the texture is fully opaque.
func (this *Texture) Opaque () bool { func (this *Texture) Opaque () bool {
return !this.transparent 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. // Close frees the texture from memory.
func (this *Texture) Close () error { func (this *Texture) Close () error {
// i lied we dont actually need to close this, but we will once this // i lied we dont actually need to close this, but we will once this
@@ -61,6 +95,9 @@ func (this *Texture) Clip (bounds image.Rectangle) canvas.Texture {
// AssertTexture checks if a given canvas.Texture is a texture from this package. // AssertTexture checks if a given canvas.Texture is a texture from this package.
func AssertTexture (unknown canvas.Texture) *Texture { func AssertTexture (unknown canvas.Texture) *Texture {
if unknown == nil {
return nil
}
if tx, ok := unknown.(*Texture); ok { if tx, ok := unknown.(*Texture); ok {
return tx return tx
} else { } else {

View File

@@ -5,11 +5,13 @@ import "git.tebibyte.media/tomo/tomo/canvas"
type canvasBox struct { type canvasBox struct {
*box *box
userDrawer canvas.Drawer
} }
func (backend *Backend) NewCanvasBox () tomo.CanvasBox { func (backend *Backend) NewCanvasBox () tomo.CanvasBox {
this := &canvasBox { } this := &canvasBox { }
this.box = backend.newBox(this) this.box = backend.newBox(this)
this.drawer = this
return this return this
} }
@@ -18,10 +20,16 @@ func (this *canvasBox) Box () tomo.Box {
} }
func (this *canvasBox) SetDrawer (drawer canvas.Drawer) { func (this *canvasBox) SetDrawer (drawer canvas.Drawer) {
this.drawer = drawer this.userDrawer = drawer
this.invalidateDraw() this.invalidateDraw()
} }
func (this *canvasBox) Invalidate () { func (this *canvasBox) Invalidate () {
this.invalidateDraw() 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 package x
import "image" import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
@@ -12,11 +13,11 @@ type containerBox struct {
hAlign, vAlign tomo.Align hAlign, vAlign tomo.Align
contentBounds image.Rectangle contentBounds image.Rectangle
scroll image.Point scroll image.Point
capture [4]bool
gap image.Point gap image.Point
children []tomo.Box children []tomo.Box
layout tomo.Layout layout tomo.Layout
propagateEvents bool
on struct { on struct {
contentBoundsChange event.FuncBroadcaster contentBoundsChange event.FuncBroadcaster
@@ -24,11 +25,23 @@ type containerBox struct {
} }
func (backend *Backend) NewContainerBox() tomo.ContainerBox { func (backend *Backend) NewContainerBox() tomo.ContainerBox {
this := &containerBox { propagateEvents: true } this := &containerBox { }
this.box = backend.newBox(this) this.box = backend.newBox(this)
return this return this
} }
func (this *containerBox) SetColor (c color.Color) {
if this.color == c { return }
this.box.SetColor(c)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTexture (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTexture(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetOverflow (horizontal, vertical bool) { func (this *containerBox) SetOverflow (horizontal, vertical bool) {
if this.hOverflow == horizontal && this.vOverflow == vertical { return } if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal this.hOverflow = horizontal
@@ -48,7 +61,7 @@ func (this *containerBox) ContentBounds () image.Rectangle {
} }
func (this *containerBox) ScrollTo (point image.Point) { func (this *containerBox) ScrollTo (point image.Point) {
// TODO: constrain scroll if this.scroll == point { return }
this.scroll = point this.scroll = point
this.invalidateLayout() this.invalidateLayout()
} }
@@ -57,8 +70,20 @@ func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback) return this.on.contentBoundsChange.Connect(callback)
} }
func (this *containerBox) SetPropagateEvents (propagate bool) { func (this *containerBox) CaptureDND (capture bool) {
this.propagateEvents = propagate 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) { func (this *containerBox) SetGap (gap image.Point) {
@@ -71,7 +96,7 @@ func (this *containerBox) SetGap (gap image.Point) {
func (this *containerBox) Add (child tomo.Object) { func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.GetBox()) 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() box.flushActionQueue()
this.children = append(this.children, box) this.children = append(this.children, box)
@@ -83,7 +108,7 @@ func (this *containerBox) Delete (child tomo.Object) {
box := assertAnyBox(child.GetBox()) 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()
@@ -96,10 +121,14 @@ func (this *containerBox) Insert (child, before tomo.Object) {
beforeBox := assertAnyBox(before.GetBox()) 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 {
this.children = append(this.children, tomo.Box(box))
} else {
this.children = insert(this.children, index, tomo.Box(box))
}
box.setParent(this) box.setParent(this)
this.children = insert(this.children, index, tomo.Box(box))
this.invalidateLayout() this.invalidateLayout()
this.invalidateMinimum() this.invalidateMinimum()
} }
@@ -132,41 +161,44 @@ func (this *containerBox) SetLayout (layout tomo.Layout) {
func (this *containerBox) Draw (can canvas.Canvas) { func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
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...) {
if this.fillTransparent && this.parent != nil { clipped := can.Clip(tile)
this.parent.drawBackgroundPart(can.Clip(tile)) if this.transparent() && this.parent != nil {
} this.parent.drawBackgroundPart(clipped)
if this.texture == nil || !this.texture.Opaque() {
pen.Rectangle(tile)
}
if this.texture != nil {
// TODO draw texture
} }
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) { func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
if this.fillTransparent && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen := can.Pen() pen := can.Pen()
pen.Fill(this.color) pen.Fill(this.color)
pen.Texture(this.texture)
if this.texture == nil || !this.texture.Opaque() {
pen.Rectangle(can.Bounds()) if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
} }
if this.texture != nil { pen.Rectangle(this.innerClippingBounds)
// TODO draw texture }
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)
}
} }
} }
@@ -195,10 +227,14 @@ func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
} }
} }
func (this *containerBox) boundedLayoutHints () tomo.LayoutHints {
hints := this.layoutHints()
hints.Bounds = this.ContentBounds().Add(this.InnerBounds().Min)
return hints
}
func (this *containerBox) layoutHints () tomo.LayoutHints { func (this *containerBox) layoutHints () tomo.LayoutHints {
innerBounds := this.InnerBounds().Sub(this.scroll)
return tomo.LayoutHints { return tomo.LayoutHints {
Bounds: innerBounds,
OverflowX: this.hOverflow, OverflowX: this.hOverflow,
OverflowY: this.vOverflow, OverflowY: this.vOverflow,
AlignX: this.hAlign, AlignX: this.hAlign,
@@ -210,10 +246,12 @@ func (this *containerBox) layoutHints () tomo.LayoutHints {
func (this *containerBox) contentMinimum () image.Point { func (this *containerBox) contentMinimum () image.Point {
minimum := this.box.contentMinimum() minimum := this.box.contentMinimum()
if this.layout != nil { if this.layout != nil {
minimum = minimum.Add ( layoutMinimum := this.layout.MinimumSize (
this.layout.MinimumSize ( this.boundedLayoutHints(),
this.layoutHints(), this.children)
this.children)) if this.hOverflow { layoutMinimum.X = 0 }
if this.vOverflow { layoutMinimum.Y = 0 }
minimum = minimum.Add(layoutMinimum)
} }
return minimum return minimum
} }
@@ -221,14 +259,59 @@ func (this *containerBox) contentMinimum () image.Point {
func (this *containerBox) doLayout () { func (this *containerBox) doLayout () {
this.box.doLayout() this.box.doLayout()
previousContentBounds := this.contentBounds previousContentBounds := this.contentBounds
// by default, use innerBounds for contentBounds. if a direction
// overflows, use the layout's minimum size for it.
var minimum image.Point
if this.layout != nil { if this.layout != nil {
this.layout.Arrange(this.layoutHints(), this.children) minimum = this.layout.MinimumSize (
this.layoutHints(),
this.children)
} }
innerBounds := this.InnerBounds()
this.contentBounds = innerBounds
if this.hOverflow { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X }
if this.vOverflow { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y }
// offset the content bounds by the scroll so children can be positioned
// accordingly.
this.constrainScroll()
this.contentBounds = this.contentBounds.Add(this.scroll).Sub(innerBounds.Min)
// arrange children
if this.layout != nil {
this.layout.Arrange(this.boundedLayoutHints(), this.children)
}
if previousContentBounds != this.contentBounds { if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast() this.on.contentBoundsChange.Broadcast()
} }
} }
func (this *containerBox) 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 < innerBounds.Dx() - width {
this.scroll.X = innerBounds.Dx() - width
}
// Y
if height <= innerBounds.Dy() {
this.scroll.Y = 0
} else if this.scroll.Y > 0 {
this.scroll.Y = 0
} else if this.scroll.Y < innerBounds.Dy() - height {
this.scroll.Y = innerBounds.Dy() - height
}
}
func (this *containerBox) recursiveRedo () { func (this *containerBox) recursiveRedo () {
this.doLayout() this.doLayout()
this.doDraw() this.doDraw()
@@ -237,15 +320,15 @@ func (this *containerBox) recursiveRedo () {
} }
} }
func (this *containerBox) boxUnder (point image.Point) anyBox { func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
if this.propagateEvents { if !this.capture[category] {
for _, box := range this.children { for _, box := range this.children {
candidate := box.(anyBox).boxUnder(point) candidate := box.(anyBox).boxUnder(point, category)
if candidate != nil { return candidate } 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 { func (this *containerBox) propagate (callback func (anyBox) bool) bool {
@@ -259,11 +342,15 @@ func (this *containerBox) propagate (callback func (anyBox) bool) bool {
func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool { func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
if !callback(this) { return false} if !callback(this) { return false}
for _, box := range this.children { for _, box := range this.children {
box := box.(anyBox) box := box.(anyBox)
if !box.propagateAlt(callback) { return false } if !box.propagateAlt(callback) { return false }
} }
return true 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 "github.com/jezek/xgbutil/xevent"
import "git.tebibyte.media/tomo/tomo/input" 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 { var buttonCodeTable = map[xproto.Keysym] input.Key {
0xFFFFFF: input.KeyNone, 0xFFFFFF: input.KeyNone,
@@ -208,7 +233,7 @@ func (window *window) handleKeyPress (
} else if key == input.KeyEscape && window.shy { } else if key == input.KeyEscape && window.shy {
window.Close() window.Close()
} else if window.focused != nil { } 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) window.updateModifiers(keyEvent.State)
if window.focused != nil { 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 { if !insideWindow && window.shy && !scrolling {
window.Close() window.Close()
} else if scrolling { } else if scrolling {
// TODO underneath := window.boxUnder(point, eventCategoryScroll)
// underneath := window.scrollTargetChildAt(point) if underneath != nil {
// if underneath != nil { sum := scrollSum { }
// if child, ok := underneath.element.(ability.ScrollTarget); ok { sum.add(buttonEvent.Detail, window, buttonEvent.State)
// sum := scrollSum { } window.compressScrollSum(buttonEvent, &sum)
// sum.add(buttonEvent.Detail, window, buttonEvent.State) underneath.handleScroll(float64(sum.x), float64(sum.y))
// window.compressScrollSum(buttonEvent, &sum) }
// child.HandleScroll (
// point, float64(sum.x), float64(sum.y),
// modifiers)
// }
// }
} else { } else {
underneath := window.boxUnder(point) underneath := window.boxUnder(point, eventCategoryMouse)
window.drags[buttonEvent.Detail] = underneath window.drags[buttonEvent.Detail] = underneath
if underneath != nil { if underneath != nil {
underneath.handleMouseDown(input.Button(buttonEvent.Detail)) underneath.handleMouseDown(input.Button(buttonEvent.Detail))
@@ -318,12 +338,14 @@ func (window *window) handleMotionNotify (
handled = true handled = true
} }
underneath := window.boxUnder(image.Pt(x, y)) underneath := window.boxUnder(image.Pt(x, y), eventCategoryMouse)
window.hover(underneath) if underneath != nil {
window.hover(underneath)
if !handled { if !handled {
underneath.handleMouseMove() underneath.handleMouseMove()
}
} }
} }
func (window *window) compressExpose ( func (window *window) compressExpose (
@@ -391,6 +413,33 @@ func (window *window) compressConfigureNotify (
return 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 ( func (window *window) compressMotionNotify (
firstEvent xproto.MotionNotifyEvent, firstEvent xproto.MotionNotifyEvent,
) ( ) (

4
go.mod
View File

@@ -3,8 +3,8 @@ module git.tebibyte.media/tomo/x
go 1.20 go 1.20
require ( require (
git.tebibyte.media/tomo/tomo v0.26.1 git.tebibyte.media/tomo/tomo v0.31.0
git.tebibyte.media/tomo/typeset v0.5.2 git.tebibyte.media/tomo/typeset v0.7.1
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

8
go.sum
View File

@@ -1,8 +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/tomo v0.26.1 h1:V5ciRuixMYb79aAawgquFEfJ1icyEmMKBKFPWwi94NE= git.tebibyte.media/tomo/tomo v0.31.0 h1:LHPpj3AWycochnC8F441aaRNS6Tq6w6WnBrp/LGjyhM=
git.tebibyte.media/tomo/tomo v0.26.1/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps= git.tebibyte.media/tomo/tomo v0.31.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/typeset v0.5.2 h1:qHxN62/VDnrAouOuzxLmLleQNwAebshrfVYvtoOnAG4= git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
git.tebibyte.media/tomo/typeset v0.5.2/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= git.tebibyte.media/tomo/typeset v0.7.1/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=

View File

@@ -37,21 +37,24 @@ type parent interface {
canvas () canvas.Canvas canvas () canvas.Canvas
notifyMinimumSizeChange (anyBox) notifyMinimumSizeChange (anyBox)
drawBackgroundPart (canvas.Canvas) drawBackgroundPart (canvas.Canvas)
captures (eventCategory) bool
} }
type anyBox interface { type anyBox interface {
tomo.Box tomo.Box
canvas.Drawer canvas.Drawer
doDraw () doDraw ()
doLayout () doLayout ()
doMinimumSize () doMinimumSize ()
contentMinimum () image.Point contentMinimum () image.Point
setParent (parent) setParent (parent)
getParent () parent
flushActionQueue () flushActionQueue ()
recursiveRedo () recursiveRedo ()
canBeFocused () bool canBeFocused () bool
boxUnder (image.Point) anyBox boxUnder (image.Point, eventCategory) anyBox
transparent () bool
propagate (func (anyBox) bool) bool propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool propagateAlt (func (anyBox) bool) bool
@@ -66,7 +69,7 @@ type anyBox interface {
handleMouseMove () handleMouseMove ()
handleMouseDown (input.Button) handleMouseDown (input.Button)
handleMouseUp (input.Button) handleMouseUp (input.Button)
// handleScroll (float64, float64) handleScroll (float64, float64)
handleKeyDown (input.Key, bool) handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool) handleKeyUp (input.Key, bool)
} }
@@ -122,10 +125,10 @@ func (window *window) invalidateLayout (box anyBox) {
func (window *window) focus (box anyBox) { func (window *window) focus (box anyBox) {
if window.focused == box { return } if window.focused == box { return }
previous := window.focused previous := window.focused
window.focused = box window.focused = box
if previous != nil { if previous != nil {
previous.handleFocusLeave() previous.handleFocusLeave()
} }
@@ -136,10 +139,10 @@ func (window *window) focus (box anyBox) {
func (window *window) hover (box anyBox) { func (window *window) hover (box anyBox) {
if window.hovered == box { return } if window.hovered == box { return }
previous := window.hovered previous := window.hovered
window.hovered = box window.hovered = box
if previous != nil { if previous != nil {
previous.handleMouseLeave() previous.handleMouseLeave()
} }
@@ -152,9 +155,36 @@ func (window *window) anyFocused () bool {
return window.focused != nil 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 } 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 () { func (this *window) focusNext () {
@@ -213,7 +243,7 @@ func (window *window) afterEvent () {
childBounds = childBounds.Sub(childBounds.Min) childBounds = childBounds.Sub(childBounds.Min)
window.root.SetBounds(childBounds) window.root.SetBounds(childBounds)
// full relayout/redraw // full relayout/redraw
if window.root != nil { if window.root != nil {
window.root.recursiveRedo() window.root.recursiveRedo()
} }

View File

@@ -23,6 +23,7 @@ type textBox struct {
face font.Face face font.Face
wrap bool wrap bool
hAlign tomo.Align hAlign tomo.Align
vAlign tomo.Align
selectable bool selectable bool
selecting bool selecting bool
@@ -59,7 +60,7 @@ func (this *textBox) ContentBounds () image.Rectangle {
} }
func (this *textBox) ScrollTo (point image.Point) { func (this *textBox) ScrollTo (point image.Point) {
// TODO: constrain scroll if this.scroll == point { return }
this.scroll = point this.scroll = point
this.invalidateLayout() this.invalidateLayout()
} }
@@ -115,6 +116,7 @@ func (this *textBox) Select (dot text.Dot) {
if this.dot == dot { return } if this.dot == dot { return }
this.SetFocused(true) this.SetFocused(true)
this.dot = dot this.dot = dot
this.scrollToDot()
this.on.dotChange.Broadcast() this.on.dotChange.Broadcast()
this.invalidateDraw() this.invalidateDraw()
} }
@@ -128,16 +130,10 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie {
} }
func (this *textBox) SetAlign (x, y tomo.Align) { func (this *textBox) SetAlign (x, y tomo.Align) {
if this.hAlign == x { return } if this.hAlign == x && this.vAlign == y { return }
this.hAlign = x this.hAlign = x
this.vAlign = y
switch x { this.drawer.SetAlign(typeset.Align(x), typeset.Align(y))
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.invalidateDraw() this.invalidateDraw()
} }
@@ -146,6 +142,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() {
@@ -221,7 +222,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
func (this *textBox) textOffset () image.Point { func (this *textBox) textOffset () image.Point {
return this.InnerBounds().Min. return this.InnerBounds().Min.
Sub(this.scroll). Add(this.scroll).
Sub(this.drawer.LayoutBoundsSpace().Min) Sub(this.drawer.LayoutBoundsSpace().Min)
} }
@@ -270,16 +271,13 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
} }
func (this *textBox) contentMinimum () image.Point { func (this *textBox) contentMinimum () image.Point {
minimum := image.Pt ( minimum := this.drawer.MinimumSize()
this.drawer.Em().Round(),
this.drawer.LineHeight().Round())
textSize := this.drawer.MinimumSize() if this.hOverflow || this.wrap {
if !this.hOverflow && !this.wrap { minimum.X = this.drawer.Em().Round()
minimum.X = textSize.X
} }
if !this.vOverflow { if this.vOverflow {
minimum.Y = textSize.Y minimum.Y = this.drawer.LineHeight().Round()
} }
return minimum.Add(this.box.contentMinimum()) return minimum.Add(this.box.contentMinimum())
@@ -290,12 +288,63 @@ func (this *textBox) doLayout () {
previousContentBounds := this.contentBounds previousContentBounds := this.contentBounds
innerBounds := this.InnerBounds() innerBounds := this.InnerBounds()
this.drawer.SetMaxWidth(innerBounds.Dx()) this.drawer.SetWidth(innerBounds.Dx())
this.drawer.SetMaxHeight(innerBounds.Dy()) this.drawer.SetHeight(innerBounds.Dy())
this.contentBounds = this.normalizedLayoutBoundsSpace().Sub(this.scroll) this.contentBounds = this.normalizedLayoutBoundsSpace()
this.constrainScroll()
this.contentBounds = this.contentBounds.Add(this.scroll)
// println(this.InnerBounds().String(), this.contentBounds.String())
if previousContentBounds != this.contentBounds { if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast() 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 < innerBounds.Dx() - width {
this.scroll.X = innerBounds.Dx() - width
}
// Y
if height <= innerBounds.Dy() {
this.scroll.Y = 0
} else if this.scroll.Y > 0 {
this.scroll.Y = 0
} else if this.scroll.Y < innerBounds.Dy() - height {
this.scroll.Y = innerBounds.Dy() - height
}
}
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)
}

View File

@@ -4,6 +4,6 @@ import "image"
import "git.tebibyte.media/tomo/x/canvas" import "git.tebibyte.media/tomo/x/canvas"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
func (backend Backend) NewTexture (source image.Image) canvas.Texture { func (backend Backend) NewTexture (source image.Image) canvas.TextureCloser {
return xcanvas.NewTextureFrom(source) 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

@@ -73,6 +73,20 @@ func (backend *Backend) NewWindow (
return output, err 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 ( func (backend *Backend) newWindow (
bounds image.Rectangle, bounds image.Rectangle,
override bool, override bool,

View File

@@ -1,17 +0,0 @@
// Plugin x provides the X11 backend as a plugin.
package main
import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/tomo"
func init () {
tomo.Register(0, tomo.Factory(x.NewBackend))
}
func Name () string {
return "X"
}
func Description () string {
return "Provides an X11 backend."
}