43 Commits
v0.4.0 ... main

Author SHA1 Message Date
e38cbe7205 Box doesn't crash when cetering a nil texture 2024-05-27 15:20:11 -04:00
09cec81f30 Upgrade Tomo API 2024-05-27 15:17:33 -04:00
0799f3645b Make boxes visible by default 2024-05-26 17:28:17 -04:00
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 646 additions and 164 deletions

View File

@@ -1,14 +1,5 @@
# 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.
## Installation
```
cd x/x
go build -buildmode=plugin .
mkdir -p ~/.local/lib/tomo/plugins
mv x.so ~/.local/lib/tomo/plugins
```

121
box.go
View File

@@ -9,23 +9,29 @@ import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
type textureMode int; const (
textureModeTile textureMode = iota
textureModeCenter
)
type box struct {
backend *Backend
parent parent
outer anyBox
bounds image.Rectangle
minSize image.Point
userMinSize image.Point
bounds image.Rectangle
minSize image.Point
userMinSize image.Point
innerClippingBounds image.Rectangle
minSizeQueued bool
focusQueued *bool
padding tomo.Inset
border []tomo.Border
color color.Color
texture *xcanvas.Texture
padding tomo.Inset
border []tomo.Border
color color.Color
texture *xcanvas.Texture
textureMode textureMode
dndData data.Data
dndAccept []data.Mime
@@ -116,15 +122,43 @@ func (this *box) SetColor (c color.Color) {
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.invalidateDraw()
}
func (this *box) SetBorder (border ...tomo.Border) {
this.border = border
this.invalidateLayout()
this.invalidateMinimum()
func (this *box) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeCenter { return }
this.texture = xcanvas.AssertTexture(texture)
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) {
@@ -259,6 +293,11 @@ func (this *box) handleMouseUp (button input.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) {
for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad)
@@ -274,13 +313,34 @@ func (this *box) handleKeyUp (key input.Key, numberPad bool) {
func (this *box) Draw (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
bounds := this.Bounds()
// background
pen.Fill(this.color)
pen.Texture(this.texture)
if this.textureMode == textureModeTile {
pen.Texture(this.texture)
}
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(can.Bounds())
pen.Rectangle(bounds)
// centered texture
if this.textureMode == textureModeCenter && this.texture != nil {
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) {
@@ -291,7 +351,7 @@ func (this *box) drawBorders (can canvas.Canvas) {
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))
this.parent.drawBackgroundPart(can.SubCanvas(area))
}
pen.Fill(c)
pen.Rectangle(area)
@@ -351,20 +411,28 @@ func (this *box) doMinimumSize () {
}
}
// 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))
this.drawer.Draw(this.canvas.SubCanvas(this.innerClippingBounds))
}
}
// var laycnt int
func (this *box) doLayout () {
// println("LAYOUT", laycnt)
// laycnt ++
this.innerClippingBounds = this.borderSum().Apply(this.bounds)
if this.parent == nil { this.canvas = nil; return }
parentCanvas := this.parent.canvas()
if parentCanvas == nil { this.canvas = nil; return }
this.canvas = parentCanvas.Clip(this.bounds)
this.canvas = parentCanvas.SubCanvas(this.bounds)
}
func (this *box) setParent (parent parent) {
@@ -374,6 +442,10 @@ func (this *box) setParent (parent parent) {
this.parent = parent
}
func (this *box) getParent () parent {
return this.parent
}
func (this *box) flushActionQueue () {
if this.parent == nil || this.parent.window() == nil { return }
@@ -392,12 +464,12 @@ func (this *box) recursiveRedo () {
func (this *box) invalidateLayout () {
if this.parent == nil || this.parent.window() == nil { return }
this.parent.window().invalidateLayout(this.outer)
this.window().invalidateLayout(this.outer)
}
func (this *box) invalidateDraw () {
if this.parent == nil || this.parent.window() == nil { return }
this.parent.window().invalidateDraw(this.outer)
this.window().invalidateDraw(this.outer)
}
func (this *box) invalidateMinimum () {
@@ -405,14 +477,14 @@ func (this *box) invalidateMinimum () {
this.minSizeQueued = true
return
}
this.parent.window().invalidateMinimum(this.outer)
this.window().invalidateMinimum(this.outer)
}
func (this *box) canBeFocused () bool {
return this.focusable
}
func (this *box) boxUnder (point image.Point) anyBox {
func (this *box) boxUnder (point image.Point, category eventCategory) anyBox {
if point.In(this.bounds) {
return this.outer
} else {
@@ -432,3 +504,8 @@ func (this *box) transparent () bool {
return transparent(this.color) &&
(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.
func (this *Canvas) Clip (bounds image.Rectangle) canvas.Canvas {
// SubCanvas returns a subset of this canvas that points to the same data.
func (this *Canvas) SubCanvas (bounds image.Rectangle) canvas.Canvas {
this.assert()
subImage := this.Image.SubImage(bounds)
if subImage == nil { return nil }
@@ -97,7 +97,7 @@ func (this *pen) Path (points ...image.Point) {
if this.fill.A > 0 {
this.fillPolygon(this.fill, points...)
}
} else if this.closed {
} else if this.closed && len(points) > 2 {
if this.stroke.A > 0 {
this.strokePolygon(this.stroke, points...)
}

View File

@@ -3,6 +3,7 @@ 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,
@@ -17,11 +18,11 @@ func (this *pen) line (
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()

View File

@@ -1,6 +1,8 @@
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.
@@ -40,16 +42,43 @@ func NewTextureFrom (source image.Image) *Texture {
return texture
}
func (this *Texture) BGRAAt (x, y int) xgraphics.BGRA {
if !(image.Point{ x, y }.In(this.rect)) {
return xgraphics.BGRA { }
}
index := this.PixOffset(x, y)
return xgraphics.BGRA {
B: this.pix[index ],
G: this.pix[index + 1],
R: this.pix[index + 2],
A: this.pix[index + 3],
}
}
func (this *Texture) At (x, y int) color.Color {
return this.BGRAAt(x, y)
}
// Bounds returns the bounding rectangle of this texture.
func (this *Texture) Bounds () image.Rectangle {
return this.rect
}
func (this *Texture) ColorModel () color.Model {
return xgraphics.BGRAModel
}
// Opaque reports whether or not the texture is fully opaque.
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
@@ -57,21 +86,18 @@ func (this *Texture) Close () error {
return nil
}
// Clip returns a subset of this texture that points to the same data.
func (this *Texture) Clip (bounds image.Rectangle) canvas.Texture {
// SubTexture returns a subset of this texture that points to the same data.
func (this *Texture) SubTexture (bounds image.Rectangle) canvas.Texture {
clipped := *this
clipped.rect = bounds
return &clipped
}
func (this *Texture) PixOffset (x, y int) int {
x = wrap(x, this.rect.Min.X, this.rect.Max.X)
y = wrap(y, this.rect.Min.Y, this.rect.Max.Y)
return x * 4 + y * this.stride
}
// AssertTexture checks if a given canvas.Texture is a texture from this package.
func AssertTexture (unknown canvas.Texture) *Texture {
if unknown == nil {
return nil
}
if tx, ok := unknown.(*Texture); ok {
return tx
} else {

View File

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

View File

@@ -1,6 +1,7 @@
package x
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
@@ -12,11 +13,11 @@ type containerBox struct {
hAlign, vAlign tomo.Align
contentBounds image.Rectangle
scroll image.Point
capture [4]bool
gap image.Point
children []tomo.Box
layout tomo.Layout
propagateEvents bool
on struct {
contentBoundsChange event.FuncBroadcaster
@@ -24,11 +25,29 @@ type containerBox struct {
}
func (backend *Backend) NewContainerBox() tomo.ContainerBox {
this := &containerBox { propagateEvents: true }
this := &containerBox { }
this.box = backend.newBox(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) {
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal
@@ -48,7 +67,7 @@ func (this *containerBox) ContentBounds () image.Rectangle {
}
func (this *containerBox) ScrollTo (point image.Point) {
// TODO: constrain scroll
if this.scroll == point { return }
this.scroll = point
this.invalidateLayout()
}
@@ -57,8 +76,20 @@ func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback)
}
func (this *containerBox) SetPropagateEvents (propagate bool) {
this.propagateEvents = propagate
func (this *containerBox) CaptureDND (capture bool) {
this.capture[eventCategoryDND] = capture
}
func (this *containerBox) CaptureMouse (capture bool) {
this.capture[eventCategoryMouse] = capture
}
func (this *containerBox) CaptureScroll (capture bool) {
this.capture[eventCategoryScroll] = capture
}
func (this *containerBox) CaptureKeyboard (capture bool) {
this.capture[eventCategoryKeyboard] = capture
}
func (this *containerBox) SetGap (gap image.Point) {
@@ -71,7 +102,7 @@ func (this *containerBox) SetGap (gap image.Point) {
func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.GetBox())
if indexOf(this.children, tomo.Box(box)) > -1 { return }
box.setParent(this)
box.flushActionQueue()
this.children = append(this.children, box)
@@ -79,11 +110,11 @@ func (this *containerBox) Add (child tomo.Object) {
this.invalidateMinimum()
}
func (this *containerBox) Delete (child tomo.Object) {
func (this *containerBox) Remove (child tomo.Object) {
box := assertAnyBox(child.GetBox())
index := indexOf(this.children, tomo.Box(box))
if index < 0 { return }
box.setParent(nil)
this.children = remove(this.children, index)
this.invalidateLayout()
@@ -96,10 +127,14 @@ func (this *containerBox) Insert (child, before tomo.Object) {
beforeBox := assertAnyBox(before.GetBox())
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)
this.children = insert(this.children, index, tomo.Box(box))
this.invalidateLayout()
this.invalidateMinimum()
}
@@ -138,7 +173,7 @@ func (this *containerBox) Draw (can canvas.Canvas) {
rocks[index] = box.Bounds()
}
for _, tile := range canvas.Shatter(this.bounds, rocks...) {
clipped := can.Clip(tile)
clipped := can.SubCanvas(tile)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(clipped)
}
@@ -155,13 +190,24 @@ func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
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()
@@ -188,9 +234,7 @@ func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
}
func (this *containerBox) layoutHints () tomo.LayoutHints {
innerBounds := this.InnerBounds().Sub(this.scroll)
return tomo.LayoutHints {
Bounds: innerBounds,
OverflowX: this.hOverflow,
OverflowY: this.vOverflow,
AlignX: this.hAlign,
@@ -202,10 +246,12 @@ func (this *containerBox) layoutHints () tomo.LayoutHints {
func (this *containerBox) contentMinimum () image.Point {
minimum := this.box.contentMinimum()
if this.layout != nil {
minimum = minimum.Add (
this.layout.MinimumSize (
this.layoutHints(),
this.children))
layoutMinimum := this.layout.MinimumSize (
this.layoutHints(),
this.children)
if this.hOverflow { layoutMinimum.X = 0 }
if this.vOverflow { layoutMinimum.Y = 0 }
minimum = minimum.Add(layoutMinimum)
}
return minimum
}
@@ -213,14 +259,73 @@ func (this *containerBox) contentMinimum () image.Point {
func (this *containerBox) doLayout () {
this.box.doLayout()
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 {
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 {
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 () {
this.doLayout()
this.doDraw()
@@ -229,15 +334,17 @@ func (this *containerBox) recursiveRedo () {
}
}
func (this *containerBox) boxUnder (point image.Point) anyBox {
if this.propagateEvents {
func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
if !point.In(this.bounds) { return nil }
if !this.capture[category] {
for _, box := range this.children {
candidate := box.(anyBox).boxUnder(point)
candidate := box.(anyBox).boxUnder(point, category)
if candidate != nil { return candidate }
}
}
return this.box.boxUnder(point)
return this
}
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 {
if !callback(this) { return false}
for _, box := range this.children {
box := box.(anyBox)
if !box.propagateAlt(callback) { return false }
}
return true
}
func (this *containerBox) captures (category eventCategory) bool {
return this.capture[category]
}

102
event.go
View File

@@ -8,6 +8,31 @@ import "git.tebibyte.media/tomo/xgbkb"
import "github.com/jezek/xgbutil/xevent"
import "git.tebibyte.media/tomo/tomo/input"
type scrollSum struct {
x, y int
}
// TODO: this needs to be configurable, we need a config api
const scrollDistance = 16
func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
if xgbkb.StateToModifiers(state).Shift {
switch button {
case 4: sum.x -= scrollDistance
case 5: sum.x += scrollDistance
case 6: sum.y -= scrollDistance
case 7: sum.y += scrollDistance
}
} else {
switch button {
case 4: sum.y -= scrollDistance
case 5: sum.y += scrollDistance
case 6: sum.x -= scrollDistance
case 7: sum.x += scrollDistance
}
}
}
var buttonCodeTable = map[xproto.Keysym] input.Key {
0xFFFFFF: input.KeyNone,
@@ -136,16 +161,17 @@ func (window *window) handleConfigureNotify (
if window.root == nil { return }
configureEvent := *event.ConfigureNotifyEvent
configureEvent = window.compressConfigureNotify(configureEvent)
newWidth := int(configureEvent.Width)
newHeight := int(configureEvent.Height)
sizeChanged :=
window.metrics.bounds.Dx() != newWidth ||
window.metrics.bounds.Dy() != newHeight
oldBounds := window.metrics.bounds
window.updateBounds()
newBounds := window.metrics.bounds
sizeChanged :=
oldBounds.Dx() != newBounds.Dx() ||
oldBounds.Dy() != newBounds.Dy()
if sizeChanged {
configureEvent = window.compressConfigureNotify(configureEvent)
window.reallocateCanvas()
// TODO figure out what to do with this
@@ -208,7 +234,7 @@ func (window *window) handleKeyPress (
} else if key == input.KeyEscape && window.shy {
window.Close()
} else if window.focused != nil {
window.focused.handleKeyDown(key, numberPad)
window.keyboardTarget().handleKeyDown(key, numberPad)
}
}
@@ -241,7 +267,7 @@ func (window *window) handleKeyRelease (
window.updateModifiers(keyEvent.State)
if window.focused != nil {
window.focused.handleKeyUp(key, numberPad)
window.keyboardTarget().handleKeyUp(key, numberPad)
}
}
@@ -261,20 +287,15 @@ func (window *window) handleButtonPress (
if !insideWindow && window.shy && !scrolling {
window.Close()
} else if scrolling {
// TODO
// underneath := window.scrollTargetChildAt(point)
// if underneath != nil {
// if child, ok := underneath.element.(ability.ScrollTarget); ok {
// sum := scrollSum { }
// sum.add(buttonEvent.Detail, window, buttonEvent.State)
// window.compressScrollSum(buttonEvent, &sum)
// child.HandleScroll (
// point, float64(sum.x), float64(sum.y),
// modifiers)
// }
// }
underneath := window.boxUnder(point, eventCategoryScroll)
if underneath != nil {
sum := scrollSum { }
sum.add(buttonEvent.Detail, window, buttonEvent.State)
window.compressScrollSum(buttonEvent, &sum)
underneath.handleScroll(float64(sum.x), float64(sum.y))
}
} else {
underneath := window.boxUnder(point)
underneath := window.boxUnder(point, eventCategoryMouse)
window.drags[buttonEvent.Detail] = underneath
if underneath != nil {
underneath.handleMouseDown(input.Button(buttonEvent.Detail))
@@ -318,12 +339,14 @@ func (window *window) handleMotionNotify (
handled = true
}
underneath := window.boxUnder(image.Pt(x, y))
window.hover(underneath)
if !handled {
underneath.handleMouseMove()
underneath := window.boxUnder(image.Pt(x, y), eventCategoryMouse)
if underneath != nil {
window.hover(underneath)
if !handled {
underneath.handleMouseMove()
}
}
}
func (window *window) compressExpose (
@@ -391,6 +414,33 @@ func (window *window) compressConfigureNotify (
return
}
func (window *window) compressScrollSum (
firstEvent xproto.ButtonPressEvent,
sum *scrollSum,
) {
window.backend.x.Sync()
xevent.Read(window.backend.x, false)
for index, untypedEvent := range xevent.Peek(window.backend.x) {
if untypedEvent.Err != nil { continue }
typedEvent, ok := untypedEvent.Event.(xproto.ButtonPressEvent)
if !ok { continue }
if firstEvent.Event == typedEvent.Event &&
typedEvent.Detail >= 4 &&
typedEvent.Detail <= 7 {
sum.add(typedEvent.Detail, window, typedEvent.State)
defer func (index int) {
xevent.DequeueAt(window.backend.x, index)
} (index)
}
}
return
}
func (window *window) compressMotionNotify (
firstEvent xproto.MotionNotifyEvent,
) (

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
require (
git.tebibyte.media/tomo/tomo v0.27.0
git.tebibyte.media/tomo/typeset v0.5.2
git.tebibyte.media/tomo/tomo v0.34.0
git.tebibyte.media/tomo/typeset v0.7.1
git.tebibyte.media/tomo/xgbkb v1.0.1
github.com/jezek/xgb v1.1.0
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/tomo/tomo v0.27.0 h1:gCwxQe0qm1hZLfHkMI3OccNMC/lB1cfs4BbaMz/bXug=
git.tebibyte.media/tomo/tomo v0.27.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
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/tomo v0.34.0 h1:r5yJPks9rtzdDI2RyAUdqa1qb6BebG0QFe2cTmcFi+0=
git.tebibyte.media/tomo/tomo v0.34.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
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/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw=
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
notifyMinimumSizeChange (anyBox)
drawBackgroundPart (canvas.Canvas)
captures (eventCategory) bool
}
type anyBox interface {
tomo.Box
canvas.Drawer
doDraw ()
doLayout ()
doMinimumSize ()
contentMinimum () image.Point
setParent (parent)
getParent () parent
flushActionQueue ()
recursiveRedo ()
canBeFocused () bool
boxUnder (image.Point) anyBox
boxUnder (image.Point, eventCategory) anyBox
transparent () bool
propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool
@@ -66,7 +69,7 @@ type anyBox interface {
handleMouseMove ()
handleMouseDown (input.Button)
handleMouseUp (input.Button)
// handleScroll (float64, float64)
handleScroll (float64, float64)
handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool)
}
@@ -122,10 +125,10 @@ func (window *window) invalidateLayout (box anyBox) {
func (window *window) focus (box anyBox) {
if window.focused == box { return }
previous := window.focused
window.focused = box
if previous != nil {
previous.handleFocusLeave()
}
@@ -136,10 +139,10 @@ func (window *window) focus (box anyBox) {
func (window *window) hover (box anyBox) {
if window.hovered == box { return }
previous := window.hovered
window.hovered = box
if previous != nil {
previous.handleMouseLeave()
}
@@ -152,9 +155,36 @@ func (window *window) anyFocused () bool {
return window.focused != nil
}
func (this *window) boxUnder (point image.Point) anyBox {
type eventCategory int; const (
eventCategoryDND eventCategory = iota
eventCategoryMouse
eventCategoryScroll
eventCategoryKeyboard
)
func (this *window) boxUnder (point image.Point, category eventCategory) anyBox {
if this.root == nil { return nil }
return this.root.boxUnder(point)
return this.root.boxUnder(point, category)
}
func (this *window) captures (eventCategory) bool {
return false
}
func (this *window) keyboardTarget () anyBox {
focused := this.window().focused
if focused == nil { return nil }
parent := focused.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.captures(eventCategoryKeyboard) {
return parentBox
}
parent = parentBox.getParent()
}
return focused
}
func (this *window) focusNext () {
@@ -211,9 +241,11 @@ func (window *window) afterEvent () {
// set child bounds
childBounds := window.metrics.bounds
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 {
window.root.recursiveRedo()
}

View File

@@ -23,6 +23,7 @@ type textBox struct {
face font.Face
wrap bool
hAlign tomo.Align
vAlign tomo.Align
selectable bool
selecting bool
@@ -59,7 +60,7 @@ func (this *textBox) ContentBounds () image.Rectangle {
}
func (this *textBox) ScrollTo (point image.Point) {
// TODO: constrain scroll
if this.scroll == point { return }
this.scroll = point
this.invalidateLayout()
}
@@ -115,6 +116,7 @@ func (this *textBox) Select (dot text.Dot) {
if this.dot == dot { return }
this.SetFocused(true)
this.dot = dot
this.scrollToDot()
this.on.dotChange.Broadcast()
this.invalidateDraw()
}
@@ -128,16 +130,10 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie {
}
func (this *textBox) SetAlign (x, y tomo.Align) {
if this.hAlign == x { return }
if this.hAlign == x && this.vAlign == y { return }
this.hAlign = x
switch x {
case tomo.AlignStart: this.drawer.SetAlign(typeset.AlignLeft)
case tomo.AlignMiddle: this.drawer.SetAlign(typeset.AlignCenter)
case tomo.AlignEnd: this.drawer.SetAlign(typeset.AlignRight)
case tomo.AlignEven: this.drawer.SetAlign(typeset.AlignJustify)
}
this.vAlign = y
this.drawer.SetAlign(typeset.Align(x), typeset.Align(y))
this.invalidateDraw()
}
@@ -226,12 +222,11 @@ func (this *textBox) drawDot (can canvas.Canvas) {
func (this *textBox) textOffset () image.Point {
return this.InnerBounds().Min.
Sub(this.scroll).
Add(this.scroll).
Sub(this.drawer.LayoutBoundsSpace().Min)
}
func (this *textBox) handleFocusLeave () {
this.dot = text.EmptyDot(0)
this.on.dotChange.Broadcast()
this.invalidateDraw()
this.box.handleFocusLeave()
@@ -275,16 +270,13 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
}
func (this *textBox) contentMinimum () image.Point {
minimum := image.Pt (
this.drawer.Em().Round(),
this.drawer.LineHeight().Round())
minimum := this.drawer.MinimumSize()
textSize := this.drawer.MinimumSize()
if !this.hOverflow && !this.wrap {
minimum.X = textSize.X
if this.hOverflow || this.wrap {
minimum.X = this.drawer.Em().Round()
}
if !this.vOverflow {
minimum.Y = textSize.Y
if this.vOverflow {
minimum.Y = this.drawer.LineHeight().Round()
}
return minimum.Add(this.box.contentMinimum())
@@ -295,12 +287,63 @@ func (this *textBox) doLayout () {
previousContentBounds := this.contentBounds
innerBounds := this.InnerBounds()
this.drawer.SetMaxWidth(innerBounds.Dx())
this.drawer.SetMaxHeight(innerBounds.Dy())
this.drawer.SetWidth(innerBounds.Dx())
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 {
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/tomo/canvas"
func (backend Backend) NewTexture (source image.Image) canvas.Texture {
func (backend Backend) NewTexture (source image.Image) canvas.TextureCloser {
return xcanvas.NewTextureFrom(source)
}

View File

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