41 Commits

Author SHA1 Message Date
cd72ae5bdd Add support for scrolling flow layouts to ContainerBox impl 2024-05-17 15:03:59 -04:00
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
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
19 changed files with 1313 additions and 331 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
```

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()
}
}

300
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"
@@ -15,10 +16,16 @@ type box struct {
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,
} }
if outer == nil {
box.drawer = box box.drawer = box
box.outer = 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,54 @@ 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) SetBorder (border ...tomo.Border) { func (this *box) SetTexture (texture canvas.Texture) {
this.border = border if this.texture == texture { return }
this.texture = xcanvas.AssertTexture(texture)
this.invalidateDraw()
}
func (this *box) SetBorder (borders ...tomo.Border) {
previousBorderSum := this.borderSum()
previousBorders := this.border
this.border = borders
// only invalidate the layout if the border is sized differently
if this.borderSum() != previousBorderSum {
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() 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) {
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 +169,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 +246,154 @@ 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) handleScroll (x, y float64) {
for _, listener := range this.on.scroll.Listeners() {
listener(x, y)
}
}
func (this *box) handleKeyDown (key input.Key, numberPad bool) {
for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad)
}
}
func (this *box) handleKeyUp (key input.Key, numberPad bool) {
for _, listener := range this.on.keyUp.Listeners() {
listener(key, numberPad)
}
}
// -------------------------------------------------------------------------- // // -------------------------------------------------------------------------- //
func (this *box) Draw (can canvas.Canvas) { 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(this.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) doDraw () { func (this *box) contentMinimum () image.Point {
if this.canvas == nil { return } var minimum image.Point
if this.drawer != nil { minimum.X += this.padding.Horizontal()
this.drawBorders(this.canvas) minimum.Y += this.padding.Vertical()
this.drawer.Draw(this.canvas.Clip(this.innerClippingBounds())) 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)
} }
} }
// var drawcnt int
func (this *box) doDraw () {
// println("DRAW", drawcnt)
// drawcnt ++
if this.canvas == nil { return }
if this.drawer != nil {
this.drawBorders(this.canvas)
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 }
@@ -273,6 +407,21 @@ func (this *box) setParent (parent parent) {
this.parent = parent this.parent = parent
} }
func (this *box) getParent () parent {
return this.parent
}
func (this *box) flushActionQueue () {
if this.parent == nil || this.parent.window() == nil { return }
if this.minSizeQueued {
this.invalidateMinimum()
}
if this.focusQueued != nil {
this.SetFocused(*this.focusQueued)
}
}
func (this *box) recursiveRedo () { func (this *box) recursiveRedo () {
this.doLayout() this.doLayout()
this.doDraw() this.doDraw()
@@ -288,17 +437,19 @@ 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 {
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 {
@@ -306,62 +457,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,32 +65,46 @@ 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 {
} else if this.closed { this.fillPolygon(this.fill, points...)
this.gfx.StrokePolygon(this.stroke[:], this.weight, points...) }
} else if this.closed && len(points) > 2 {
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...)
}
} }
} }
@@ -101,13 +116,18 @@ 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
}

95
canvas/line.go Normal file
View File

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

47
canvas/plot.go Normal file
View File

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

106
canvas/texture.go Normal file
View File

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

View File

@@ -5,14 +5,14 @@ import "git.tebibyte.media/tomo/tomo/canvas"
type canvasBox struct { type canvasBox struct {
*box *box
userDrawer canvas.Drawer
} }
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)
} this.drawer = this
box.outer = box return this
return box
} }
func (this *canvasBox) Box () tomo.Box { func (this *canvasBox) Box () tomo.Box {
@@ -20,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,17 +25,21 @@ type containerBox struct {
} }
func (backend *Backend) NewContainerBox() tomo.ContainerBox { func (backend *Backend) NewContainerBox() tomo.ContainerBox {
box := &containerBox { this := &containerBox { }
box: backend.NewBox().(*box), this.box = backend.newBox(this)
propagateEvents: true, return this
}
box.drawer = box
box.outer = box
return box
} }
func (this *containerBox) Box () tomo.Box { func (this *containerBox) SetColor (c color.Color) {
return this 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) {
@@ -56,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()
} }
@@ -65,50 +70,67 @@ 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) {
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 }
box.setParent(this) if index < 0 {
this.children = append(this.children, tomo.Box(box))
} else {
this.children = insert(this.children, index, tomo.Box(box)) this.children = insert(this.children, index, tomo.Box(box))
}
box.setParent(this)
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Clear () { func (this *containerBox) Clear () {
@@ -117,7 +139,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,22 +156,57 @@ 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) invalidateTransparentChildren () {
window := this.window()
if window == nil { return }
for _, box := range this.children {
box := assertAnyBox(box)
if box.transparent() {
window.invalidateDraw(box)
}
}
}
func (this *containerBox) flushActionQueue () {
for _, box := range this.children {
box.(anyBox).flushActionQueue()
}
this.box.flushActionQueue()
} }
func (this *containerBox) window () *window { func (this *containerBox) window () *window {
@@ -162,8 +219,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 {
@@ -172,9 +228,7 @@ func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
} }
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,
@@ -183,31 +237,89 @@ 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 layoutMinimum := this.layout.MinimumSize (
this.layoutHints(),
this.children)
if this.hOverflow { layoutMinimum.X = 0 }
if this.vOverflow { layoutMinimum.Y = 0 }
minimum = minimum.Add(layoutMinimum)
} }
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 () {
this.box.doLayout() this.box.doLayout()
previousContentBounds := this.contentBounds previousContentBounds := this.contentBounds
// by default, use innerBounds (translated to 0, 0) 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.Sub(innerBounds.Min)
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 }
// arrange children
if this.layout != nil {
layoutHints := this.layoutHints()
layoutHints.Bounds = this.contentBounds
this.layout.Arrange(layoutHints, this.children)
}
// build an accurate contentBounds by unioning the bounds of all child
// boxes
this.contentBounds = image.Rectangle { }
for _, box := range this.children {
bounds := box.Bounds()
this.contentBounds = this.contentBounds.Union(bounds)
}
// constrain the scroll
this.constrainScroll()
// offset children and contentBounds by scroll
for _, box := range this.children {
box.SetBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
}
this.contentBounds = this.contentBounds.Add(this.scroll)
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()
@@ -216,15 +328,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 {
@@ -246,3 +358,7 @@ func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
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))
@@ -293,6 +313,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,11 +338,16 @@ func (window *window) handleMotionNotify (
handled = true handled = true
} }
underneath := window.boxUnder(image.Pt(x, y), eventCategoryMouse)
if underneath != nil {
window.hover(underneath)
if !handled { if !handled {
window.boxUnder(image.Pt(x, y)).handleMouseMove() underneath.handleMouseMove()
} }
} }
}
func (window *window) compressExpose ( func (window *window) compressExpose (
firstEvent xproto.ExposeEvent, firstEvent xproto.ExposeEvent,
) ( ) (
@@ -387,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,
) ( ) (

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.31.0
git.tebibyte.media/tomo/tomo v0.20.0 git.tebibyte.media/tomo/typeset v0.7.1
git.tebibyte.media/tomo/typeset v0.5.2
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.31.0 h1:LHPpj3AWycochnC8F441aaRNS6Tq6w6WnBrp/LGjyhM=
git.tebibyte.media/tomo/ggfx v0.4.0/go.mod h1:zPoz8BdVQyG2KhEmeGFQBK66V71i6Kj8oVFbrZaCwRA= git.tebibyte.media/tomo/tomo v0.31.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/tomo v0.20.0 h1:dJJWmCAj/8XtbxjiCkgykP2vNCeP5zuvMEKGChbQ0AI= git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
git.tebibyte.media/tomo/tomo v0.20.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/typeset v0.5.2 h1:qHxN62/VDnrAouOuzxLmLleQNwAebshrfVYvtoOnAG4=
git.tebibyte.media/tomo/typeset v0.5.2/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,25 @@ type parent interface {
window () *window window () *window
canvas () canvas.Canvas canvas () canvas.Canvas
notifyMinimumSizeChange (anyBox) notifyMinimumSizeChange (anyBox)
drawBackgroundPart (canvas.Canvas)
captures (eventCategory) bool
} }
type anyBox interface { type anyBox interface {
tomo.Box tomo.Box
canvas.Drawer
doDraw () doDraw ()
doLayout () doLayout ()
doMinimumSize ()
contentMinimum () image.Point
setParent (parent) setParent (parent)
getParent () parent
flushActionQueue ()
recursiveRedo () recursiveRedo ()
canBeFocused () bool canBeFocused () bool
boxUnder (image.Point) anyBox boxUnder (image.Point, eventCategory) anyBox
recalculateMinimumSize () transparent () bool
propagate (func (anyBox) bool) bool propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool propagateAlt (func (anyBox) bool) bool
@@ -56,12 +64,12 @@ 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)
// handleScroll (float64, float64) handleScroll (float64, float64)
handleKeyDown (input.Key, bool) handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool) handleKeyUp (input.Key, bool)
} }
@@ -81,12 +89,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 +107,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 +119,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,22 +130,61 @@ 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
} }
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 () {
@@ -202,6 +252,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

@@ -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
@@ -39,14 +40,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) {
@@ -61,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()
} }
@@ -74,7 +73,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 +87,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()
} }
@@ -117,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()
} }
@@ -130,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()
} }
@@ -148,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() {
@@ -167,10 +166,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()
@@ -183,6 +183,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
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:
@@ -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)
} }
@@ -269,25 +270,17 @@ 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 := 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()
} }
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 () {
@@ -295,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)
}

9
texture.go Normal file
View File

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

View File

@@ -1,5 +1,7 @@
package x 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
// minimums, we need to start at the deeper parts of the layout tree and
// go upward towards the top. for the layouts, we need to start at the
// top of the layout tree and progressively go deeper. this will
// eliminate redundant layout calculations.
needMinimum boxSet
needLayout boxSet needLayout boxSet
needDraw boxSet
needRedo bool needRedo bool
minimumClean bool
} }
func (backend *Backend) NewWindow ( func (backend *Backend) NewWindow (
@@ -63,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,
@@ -129,7 +153,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 +346,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 +376,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) doMinimumSize () {
window.minimumClean = true
size := image.Point { }
if window.root != nil {
size = window.root.MinimumSize()
} }
func (window *window) setMinimumSize (size image.Point) {
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

@@ -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(x.NewBackend)
}
func Name () string {
return "X"
}
func Description () string {
return "Provides an X11 backend."
}