40 Commits

Author SHA1 Message Date
9214f70b61 Fix centered texture rendering 2024-05-26 16:54:00 -04:00
36ac66b644 Add texture example 2024-05-26 16:53:49 -04:00
c13db1217d Fixed a null ptr deref in system.go 2024-05-26 15:33:40 -04:00
c0124bf232 Add some very basic examples 2024-05-26 15:33:15 -04:00
9dc929f545 Update window.go 2024-05-26 15:20:25 -04:00
0f9c27c19a Fix ContainerBox's SetTexture methods 2024-05-26 15:20:11 -04:00
74fd3fdd55 Rename ContainerBox.Delete to ContainerBox.Remove 2024-05-26 15:18:56 -04:00
398ad08867 Replace references to Canvas.Clip 2024-05-26 15:18:27 -04:00
c92377f50b Add Visible, SetVisible to Window 2024-05-26 15:16:31 -04:00
8b44526c94 Add Visible, SetVisible to Box 2024-05-26 15:14:40 -04:00
a5830c9823 Add SetTextureCenter 2024-05-26 15:04:58 -04:00
dd201f1b5f Add stub for SurfaceBox 2024-05-26 14:55:33 -04:00
8f47da654c Update canvas 2024-05-26 00:55:20 -04:00
bb4080bd73 Update Tomo API 2024-05-26 00:40:47 -04:00
c0c4bdb266 TextBox remembers dot when unfocused
Remedy #8
2024-05-20 12:58:02 -04:00
b8ce9d15f7 Remove plugin install instructions from README.md 2024-05-17 23:45:38 -04:00
05f3ebc9e5 Remedy #1 2024-05-17 23:19:03 -04:00
996d747d45 Remedy #4 2024-05-17 15:36:49 -04:00
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
19 changed files with 657 additions and 164 deletions

View File

@@ -1,14 +1,5 @@
# x # x
[![Go Reference](https://pkg.go.dev/badge/pkg.go.dev/git.tebibyte.media/tomo/x.svg)](https://pkg.go.dev/pkg.go.dev/git.tebibyte.media/tomo/x) [![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. 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
```

132
box.go
View File

@@ -9,23 +9,30 @@ 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 "git.tebibyte.media/tomo/tomo/canvas"
type textureMode int; const (
textureModeTile textureMode = iota
textureModeCenter
)
type box struct { type box struct {
backend *Backend backend *Backend
parent parent parent parent
outer anyBox outer anyBox
bounds image.Rectangle visible bool
minSize image.Point bounds image.Rectangle
userMinSize image.Point minSize image.Point
userMinSize image.Point
innerClippingBounds image.Rectangle innerClippingBounds image.Rectangle
minSizeQueued bool minSizeQueued bool
focusQueued *bool focusQueued *bool
padding tomo.Inset padding tomo.Inset
border []tomo.Border border []tomo.Border
color color.Color color color.Color
texture *xcanvas.Texture texture *xcanvas.Texture
textureMode textureMode
dndData data.Data dndData data.Data
dndAccept []data.Mime dndAccept []data.Mime
@@ -116,15 +123,43 @@ func (this *box) SetColor (c color.Color) {
this.invalidateDraw() this.invalidateDraw()
} }
func (this *box) SetTexture (texture canvas.Texture) { func (this *box) SetTextureTile (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeTile { return }
this.textureMode = textureModeTile
this.texture = xcanvas.AssertTexture(texture) this.texture = xcanvas.AssertTexture(texture)
this.invalidateDraw() this.invalidateDraw()
} }
func (this *box) SetBorder (border ...tomo.Border) { func (this *box) SetTextureCenter (texture canvas.Texture) {
this.border = border if this.texture == texture && this.textureMode == textureModeCenter { return }
this.invalidateLayout() this.texture = xcanvas.AssertTexture(texture)
this.invalidateMinimum() this.textureMode = textureModeCenter
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.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) {
@@ -140,6 +175,16 @@ func (this *box) SetPadding (padding tomo.Inset) {
this.invalidateMinimum() this.invalidateMinimum()
} }
func (this *box) SetVisible (visible bool) {
if this.visible == visible { return }
this.visible = visible
this.invalidateMinimum()
}
func (this *box) Visible () bool {
return this.visible
}
func (this *box) SetDNDData (dat data.Data) { func (this *box) SetDNDData (dat data.Data) {
this.dndData = dat this.dndData = dat
} }
@@ -259,6 +304,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)
@@ -274,13 +324,34 @@ func (this *box) handleKeyUp (key input.Key, numberPad bool) {
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()
bounds := this.Bounds()
// background
pen.Fill(this.color) pen.Fill(this.color)
pen.Texture(this.texture) if this.textureMode == textureModeTile {
pen.Texture(this.texture)
}
if this.transparent() && this.parent != nil { if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can) this.parent.drawBackgroundPart(can)
} }
pen.Rectangle(can.Bounds()) pen.Rectangle(bounds)
// centered texture
if this.textureMode == textureModeCenter {
textureBounds := this.texture.Bounds()
textureOrigin :=
bounds.Min.
Add(image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)).
Sub(image.Pt (
textureBounds.Dx() / 2,
textureBounds.Dy() / 2))
pen.Fill(color.Transparent)
pen.Texture(this.texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
}
} }
func (this *box) drawBorders (can canvas.Canvas) { func (this *box) drawBorders (can canvas.Canvas) {
@@ -291,7 +362,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)
if transparent(c) && this.parent != nil { if transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.Clip(area)) this.parent.drawBackgroundPart(can.SubCanvas(area))
} }
pen.Fill(c) pen.Fill(c)
pen.Rectangle(area) pen.Rectangle(area)
@@ -351,20 +422,28 @@ 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.SubCanvas(this.innerClippingBounds))
} }
} }
// var laycnt int
func (this *box) doLayout () { func (this *box) doLayout () {
// println("LAYOUT", laycnt)
// laycnt ++
this.innerClippingBounds = this.borderSum().Apply(this.bounds) 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 }
this.canvas = parentCanvas.Clip(this.bounds) this.canvas = parentCanvas.SubCanvas(this.bounds)
} }
func (this *box) setParent (parent parent) { func (this *box) setParent (parent parent) {
@@ -374,6 +453,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 }
@@ -392,12 +475,12 @@ func (this *box) recursiveRedo () {
func (this *box) invalidateLayout () { func (this *box) invalidateLayout () {
if this.parent == nil || this.parent.window() == nil { return } if this.parent == nil || this.parent.window() == nil { return }
this.parent.window().invalidateLayout(this.outer) this.window().invalidateLayout(this.outer)
} }
func (this *box) invalidateDraw () { func (this *box) invalidateDraw () {
if this.parent == nil || this.parent.window() == nil { return } if this.parent == nil || this.parent.window() == nil { return }
this.parent.window().invalidateDraw(this.outer) this.window().invalidateDraw(this.outer)
} }
func (this *box) invalidateMinimum () { func (this *box) invalidateMinimum () {
@@ -405,14 +488,14 @@ func (this *box) invalidateMinimum () {
this.minSizeQueued = true this.minSizeQueued = true
return return
} }
this.parent.window().invalidateMinimum(this.outer) this.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 {
@@ -432,3 +515,8 @@ func (this *box) transparent () bool {
return transparent(this.color) && return transparent(this.color) &&
(this.texture == nil || !this.texture.Opaque()) (this.texture == nil || !this.texture.Opaque())
} }
func (this *box) window () *window {
if this.parent == nil { return nil }
return this.parent.window()
}

View File

@@ -32,8 +32,8 @@ func (this *Canvas) Pen () canvas.Pen {
} }
} }
// Clip returns a sub-canvas of this canvas. // SubCanvas returns a subset of this canvas that points to the same data.
func (this *Canvas) Clip (bounds image.Rectangle) canvas.Canvas { func (this *Canvas) SubCanvas (bounds image.Rectangle) canvas.Canvas {
this.assert() this.assert()
subImage := this.Image.SubImage(bounds) subImage := this.Image.SubImage(bounds)
if subImage == nil { return nil } if subImage == nil { return nil }
@@ -97,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...)
} }

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.
@@ -40,16 +42,43 @@ func NewTextureFrom (source image.Image) *Texture {
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. // Bounds returns the bounding rectangle of this texture.
func (this *Texture) Bounds () image.Rectangle { func (this *Texture) Bounds () image.Rectangle {
return this.rect 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
@@ -57,21 +86,18 @@ func (this *Texture) Close () error {
return nil return nil
} }
// Clip returns a subset of this texture that points to the same data. // SubTexture returns a subset of this texture that points to the same data.
func (this *Texture) Clip (bounds image.Rectangle) canvas.Texture { func (this *Texture) SubTexture (bounds image.Rectangle) canvas.Texture {
clipped := *this clipped := *this
clipped.rect = bounds clipped.rect = bounds
return &clipped return &clipped
} }
func (this *Texture) PixOffset (x, y int) int {
x = wrap(x, this.rect.Min.X, this.rect.Max.X)
y = wrap(y, this.rect.Min.Y, this.rect.Max.Y)
return x * 4 + y * this.stride
}
// AssertTexture checks if a given canvas.Texture is a texture from this package. // 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.SubCanvas(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,29 @@ 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) SetTextureTile (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTextureTile(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTextureTile(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 +67,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 +76,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 +102,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)
@@ -79,11 +110,11 @@ func (this *containerBox) Add (child tomo.Object) {
this.invalidateMinimum() this.invalidateMinimum()
} }
func (this *containerBox) Delete (child tomo.Object) { func (this *containerBox) Remove (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 +127,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()
} }
@@ -138,7 +173,7 @@ func (this *containerBox) Draw (can canvas.Canvas) {
rocks[index] = box.Bounds() rocks[index] = box.Bounds()
} }
for _, tile := range canvas.Shatter(this.bounds, rocks...) { for _, tile := range canvas.Shatter(this.bounds, rocks...) {
clipped := can.Clip(tile) clipped := can.SubCanvas(tile)
if this.transparent() && this.parent != nil { if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(clipped) this.parent.drawBackgroundPart(clipped)
} }
@@ -155,13 +190,24 @@ func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
pen := can.Pen() pen := can.Pen()
pen.Fill(this.color) pen.Fill(this.color)
pen.Texture(this.texture) pen.Texture(this.texture)
if this.transparent() && this.parent != nil { if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can) this.parent.drawBackgroundPart(can)
} }
pen.Rectangle(this.innerClippingBounds) 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 () { func (this *containerBox) flushActionQueue () {
for _, box := range this.children { for _, box := range this.children {
box.(anyBox).flushActionQueue() box.(anyBox).flushActionQueue()
@@ -188,9 +234,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,
@@ -202,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.layoutHints(),
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
} }
@@ -213,14 +259,73 @@ 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 (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()
@@ -229,15 +334,17 @@ 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 !point.In(this.bounds) { return nil }
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
} }
func (this *containerBox) propagate (callback func (anyBox) bool) bool { func (this *containerBox) propagate (callback func (anyBox) bool) bool {
@@ -251,11 +358,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]
}

102
event.go
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,
@@ -136,16 +161,17 @@ func (window *window) handleConfigureNotify (
if window.root == nil { return } if window.root == nil { return }
configureEvent := *event.ConfigureNotifyEvent configureEvent := *event.ConfigureNotifyEvent
configureEvent = window.compressConfigureNotify(configureEvent)
newWidth := int(configureEvent.Width) oldBounds := window.metrics.bounds
newHeight := int(configureEvent.Height)
sizeChanged :=
window.metrics.bounds.Dx() != newWidth ||
window.metrics.bounds.Dy() != newHeight
window.updateBounds() window.updateBounds()
newBounds := window.metrics.bounds
sizeChanged :=
oldBounds.Dx() != newBounds.Dx() ||
oldBounds.Dy() != newBounds.Dy()
if sizeChanged { if sizeChanged {
configureEvent = window.compressConfigureNotify(configureEvent)
window.reallocateCanvas() window.reallocateCanvas()
// TODO figure out what to do with this // TODO figure out what to do with this
@@ -208,7 +234,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 +267,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 +287,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 +339,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 +414,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,
) ( ) (

18
examples/blank/main.go Normal file
View File

@@ -0,0 +1,18 @@
package main
import "image"
import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/tomo"
func main () {
tomo.Register(0, x.NewBackend)
err := tomo.Run(run)
if err != nil { panic(err) }
}
func run () {
window, err := tomo.NewWindow(image.Rect(0, 0, 200, 300))
if err != nil { panic(err) }
window.OnClose(tomo.Stop)
window.SetVisible(true)
}

29
examples/text/main.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import "image"
import "image/color"
import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/font/basicfont"
func main () {
tomo.Register(0, x.NewBackend)
err := tomo.Run(run)
if err != nil { panic(err) }
}
func run () {
window, err := tomo.NewWindow(image.Rectangle { })
if err != nil { panic(err) }
text := tomo.NewTextBox()
text.SetText("hello, world!")
text.SetTextColor(color.White)
text.SetColor(color.Black)
text.SetFace(basicfont.Face7x13)
text.SetPadding(tomo.I(8))
window.SetRoot(text)
window.OnClose(tomo.Stop)
window.SetVisible(true)
}

83
examples/texture/main.go Normal file
View File

@@ -0,0 +1,83 @@
package main
import "math"
import "image"
import "math/rand"
import "image/color"
import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/tomo"
func main () {
tomo.Register(0, x.NewBackend)
err := tomo.Run(run)
if err != nil { panic(err) }
}
func run () {
window, err := tomo.NewWindow(image.Rect(0, 0, 256, 256))
if err != nil { panic(err) }
texture := tomo.NewTexture(coolTexture())
box := tomo.NewBox()
box.SetColor(color.Black)
box.SetTextureCenter(texture)
box.SetBorder (
tomo.Border {
Color: [4] color.Color {
color.Black, color.Black,
color.Black, color.Black },
Width: tomo.I(2),
},
tomo.Border {
Color: [4] color.Color {
color.White, color.White,
color.White, color.White },
Width: tomo.I(2),
})
window.SetRoot(box)
window.OnClose(tomo.Stop)
window.SetVisible(true)
}
func coolTexture () image.Image {
// this picture IS COOL because i spent AN HOUR on it when i could have
// been FIXING BUGS!
speedX := 0.015
speedY := 0.035
bitmap := image.NewRGBA (image.Rect(0, 0, 200, 200))
for y := bitmap.Bounds().Min.Y; y < bitmap.Bounds().Max.Y; y++ {
for x := bitmap.Bounds().Min.X; x < bitmap.Bounds().Max.X; x++ {
value := ((
math.Sin(float64(y) * speedY) +
math.Cos(float64(x) * speedX)) + 2) / 4
value *= 0.7
r := value * 0.7 + 0.3
g := math.Sin(value * 7) * 0.7
b := (1 - value) * 0.7
noise := math.Mod(rand.Float64(), 1)
noise = math.Pow(noise, 2) + noise * 0.5
noise *= 0.05
channel := func (f float64) uint8 {
contrast := 1.4
f = f * (contrast) - ((contrast - 1) / 2)
if f < 0 { f = 0 }
if f > 1 { f = 1 }
return uint8(f * 255)
}
bitmap.Set(x, y, color.RGBA {
R: channel(r + noise),
G: channel(g + noise),
B: channel(b + noise),
A: 255,
})
}}
return bitmap
}

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.27.0 git.tebibyte.media/tomo/tomo v0.33.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.27.0 h1:gCwxQe0qm1hZLfHkMI3OccNMC/lB1cfs4BbaMz/bXug= git.tebibyte.media/tomo/tomo v0.33.0 h1:BBm1oRsogBLeqVKeevNqG9RPCOdmbGeiQM/9hd2GHE8=
git.tebibyte.media/tomo/tomo v0.27.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps= git.tebibyte.media/tomo/tomo v0.33.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=

8
surfacebox.go Normal file
View File

@@ -0,0 +1,8 @@
package x
import "errors"
import "git.tebibyte.media/tomo/tomo"
func (backend *Backend) NewSurfaceBox() (tomo.SurfaceBox, error) {
return nil, errors.New("SurfaceBox not implemented yet")
}

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 () {
@@ -211,9 +241,11 @@ func (window *window) afterEvent () {
// set child bounds // set child bounds
childBounds := window.metrics.bounds childBounds := window.metrics.bounds
childBounds = childBounds.Sub(childBounds.Min) childBounds = childBounds.Sub(childBounds.Min)
window.root.SetBounds(childBounds) if window.root != nil {
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()
} }
@@ -226,12 +222,11 @@ 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)
} }
func (this *textBox) handleFocusLeave () { func (this *textBox) handleFocusLeave () {
this.dot = text.EmptyDot(0)
this.on.dotChange.Broadcast() this.on.dotChange.Broadcast()
this.invalidateDraw() this.invalidateDraw()
this.box.handleFocusLeave() this.box.handleFocusLeave()
@@ -275,16 +270,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())
@@ -295,12 +287,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

@@ -32,6 +32,7 @@ type window struct {
modalParent *window modalParent *window
hasModal bool hasModal bool
shy bool shy bool
visible bool
metrics struct { metrics struct {
bounds image.Rectangle bounds image.Rectangle
@@ -73,6 +74,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,
@@ -251,14 +266,21 @@ func (window *window) Paste (callback func (data.Data, error), accept ...data.Mi
// TODO // TODO
} }
func (window *window) Show () { func (window *window) SetVisible (visible bool) {
window.xWindow.Map() if window.visible == visible { return }
if window.shy { window.grabInput() } window.visible = visible
if window.visible {
window.xWindow.Map()
if window.shy { window.grabInput() }
} else {
window.xWindow.Unmap()
if window.shy { window.ungrabInput() }
}
} }
func (window *window) Hide () { func (window *window) Visible () bool {
window.xWindow.Unmap() return window.visible
if window.shy { window.ungrabInput() }
} }
func (window *window) Close () { func (window *window) Close () {
@@ -271,7 +293,7 @@ func (window *window) Close () {
// we are a modal dialog, so unlock the parent // we are a modal dialog, so unlock the parent
window.modalParent.hasModal = false window.modalParent.hasModal = false
} }
window.Hide() window.SetVisible(false)
window.SetRoot(nil) window.SetRoot(nil)
delete(window.backend.windows, window.xWindow.Id) delete(window.backend.windows, window.xWindow.Id)
window.xWindow.Destroy() window.xWindow.Destroy()
@@ -355,7 +377,7 @@ func (window *window) pushRegion (region image.Rectangle) {
return return
} }
subCanvas := window.xCanvas.Clip(region) subCanvas := window.xCanvas.SubCanvas(region)
if subCanvas == nil { if subCanvas == nil {
return return
} }

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."
}