Compare commits

...

22 Commits
v0.7.4 ... 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
16 changed files with 267 additions and 72 deletions

View File

@ -3,12 +3,3 @@
[![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
```

76
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,12 +122,20 @@ func (this *box) SetColor (c color.Color) {
this.invalidateDraw()
}
func (this *box) SetTexture (texture canvas.Texture) {
if this.texture == texture { return }
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) 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
@ -299,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(this.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) {
@ -316,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)
@ -384,7 +419,7 @@ func (this *box) doDraw () {
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))
}
}
@ -397,7 +432,7 @@ func (this *box) doLayout () {
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) {
@ -429,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 () {
@ -442,7 +477,7 @@ func (this *box) invalidateMinimum () {
this.minSizeQueued = true
return
}
this.parent.window().invalidateMinimum(this.outer)
this.window().invalidateMinimum(this.outer)
}
func (this *box) canBeFocused () bool {
@ -469,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 }

View File

@ -86,8 +86,8 @@ 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

View File

@ -31,5 +31,5 @@ func (this *canvasBox) Invalidate () {
func (this *canvasBox) Draw (can canvas.Canvas) {
this.box.Draw(can)
this.userDrawer.Draw (
can.Clip(this.padding.Apply(this.innerClippingBounds)))
can.SubCanvas(this.padding.Apply(this.innerClippingBounds)))
}

View File

@ -36,9 +36,15 @@ func (this *containerBox) SetColor (c color.Color) {
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTexture (texture canvas.Texture) {
func (this *containerBox) SetTextureTile (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTexture(texture)
this.box.SetTextureTile(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTextureTile(texture)
this.invalidateTransparentChildren()
}
@ -104,7 +110,7 @@ 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 }
@ -167,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)
}
@ -227,12 +233,6 @@ func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
}
}
func (this *containerBox) boundedLayoutHints () tomo.LayoutHints {
hints := this.layoutHints()
hints.Bounds = this.ContentBounds().Add(this.InnerBounds().Min)
return hints
}
func (this *containerBox) layoutHints () tomo.LayoutHints {
return tomo.LayoutHints {
OverflowX: this.hOverflow,
@ -247,7 +247,7 @@ func (this *containerBox) contentMinimum () image.Point {
minimum := this.box.contentMinimum()
if this.layout != nil {
layoutMinimum := this.layout.MinimumSize (
this.boundedLayoutHints(),
this.layoutHints(),
this.children)
if this.hOverflow { layoutMinimum.X = 0 }
if this.vOverflow { layoutMinimum.Y = 0 }
@ -260,8 +260,8 @@ func (this *containerBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
// by default, use innerBounds for contentBounds. if a direction
// overflows, use the layout's minimum size for it.
// 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 {
minimum = this.layout.MinimumSize (
@ -269,19 +269,33 @@ func (this *containerBox) doLayout () {
this.children)
}
innerBounds := this.InnerBounds()
this.contentBounds = 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 }
// offset the content bounds by the scroll so children can be positioned
// accordingly.
this.constrainScroll()
this.contentBounds = this.contentBounds.Add(this.scroll).Sub(innerBounds.Min)
// arrange children
if this.layout != nil {
this.layout.Arrange(this.boundedLayoutHints(), this.children)
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()
@ -321,6 +335,8 @@ func (this *containerBox) recursiveRedo () {
}
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, category)
@ -328,7 +344,7 @@ func (this *containerBox) boxUnder (point image.Point, category eventCategory) a
}
}
return this.box.boxUnder(point, category)
return this
}
func (this *containerBox) propagate (callback func (anyBox) bool) bool {

View File

@ -161,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

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
}

2
go.mod
View File

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

4
go.sum
View File

@ -1,6 +1,6 @@
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/tomo v0.31.0 h1:LHPpj3AWycochnC8F441aaRNS6Tq6w6WnBrp/LGjyhM=
git.tebibyte.media/tomo/tomo v0.31.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
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=

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

@ -241,7 +241,9 @@ 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
if window.root != nil {

View File

@ -227,7 +227,6 @@ func (this *textBox) textOffset () image.Point {
}
func (this *textBox) handleFocusLeave () {
this.dot = text.EmptyDot(0)
this.on.dotChange.Broadcast()
this.invalidateDraw()
this.box.handleFocusLeave()

View File

@ -32,6 +32,7 @@ type window struct {
modalParent *window
hasModal bool
shy bool
visible bool
metrics struct {
bounds image.Rectangle
@ -265,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 () {
@ -285,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()
@ -369,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
}