13 Commits

16 changed files with 434 additions and 128 deletions

View File

@@ -1,3 +1,14 @@
# x
WIP X backend for Tomo.
[![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/x.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/x)
An X11 backend for Tomo.
## Installation
```
cd x/x
go build -buildmode=plugin .
mkdir -p ~/.local/lib/tomo/plugins
mv x.so ~/.local/lib/tomo/plugins
```

53
box.go
View File

@@ -17,6 +17,7 @@ type box struct {
bounds image.Rectangle
minSize image.Point
userMinSize image.Point
innerClippingBounds image.Rectangle
minSizeQueued bool
focusQueued *bool
@@ -25,8 +26,6 @@ type box struct {
border []tomo.Border
color color.Color
texture *xcanvas.Texture
fillTransparent bool
dndData data.Data
dndAccept []data.Mime
@@ -56,7 +55,7 @@ type box struct {
func (backend *Backend) newBox (outer anyBox) *box {
box := &box {
backend: backend,
color: color.White,
color: color.Transparent,
outer: outer,
drawer: outer,
}
@@ -86,7 +85,7 @@ func (this *box) Bounds () image.Rectangle {
}
func (this *box) InnerBounds () image.Rectangle {
return this.padding.Apply(this.innerClippingBounds())
return this.padding.Apply(this.innerClippingBounds)
}
func (this *box) MinimumSize () image.Point {
@@ -104,10 +103,6 @@ func (this *box) borderSum () tomo.Inset {
return sum
}
func (this *box) innerClippingBounds () image.Rectangle {
return this.borderSum().Apply(this.bounds)
}
func (this *box) SetBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
@@ -118,19 +113,16 @@ func (this *box) SetColor (c color.Color) {
if c == nil { c = color.Transparent }
if this.color == c { return }
this.color = c
this.determineFillTransparency()
this.invalidateDraw()
}
func (this *box) SetTexture (texture canvas.Texture) {
this.texture = xcanvas.AssertTexture(texture)
this.determineFillTransparency()
this.invalidateDraw()
}
func (this *box) SetBorder (border ...tomo.Border) {
this.border = border
this.determineFillTransparency()
this.invalidateLayout()
this.invalidateMinimum()
}
@@ -267,6 +259,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)
@@ -283,12 +280,12 @@ func (this *box) Draw (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
pen.Fill(this.color)
if this.texture == nil || !this.texture.Opaque() {
pen.Rectangle(this.bounds)
}
if this.texture != nil {
// TODO drawR texture
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(can.Bounds())
}
func (this *box) drawBorders (can canvas.Canvas) {
@@ -298,8 +295,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)
_, _, _, a := c.RGBA()
if a != 0xFFFF && this.parent != nil {
if transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.Clip(area))
}
pen.Fill(c)
@@ -364,11 +360,12 @@ 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.Clip(this.innerClippingBounds))
}
}
func (this *box) doLayout () {
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 }
@@ -382,6 +379,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 }
@@ -420,7 +421,7 @@ 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 {
@@ -428,13 +429,6 @@ func (this *box) boxUnder (point image.Point) anyBox {
}
}
func (this *box) determineFillTransparency () {
_, _, _, a := this.color.RGBA()
this.fillTransparent =
a != 0xFFFF &&
!(this.texture != nil && this.texture.Opaque())
}
func (this *box) propagate (callback func (anyBox) bool) bool {
return callback(this.outer)
}
@@ -442,3 +436,8 @@ func (this *box) propagate (callback func (anyBox) bool) bool {
func (this *box) propagateAlt (callback func (anyBox) bool) bool {
return callback(this.outer)
}
func (this *box) transparent () bool {
return transparent(this.color) &&
(this.texture == nil || !this.texture.Opaque())
}

View File

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

View File

@@ -4,6 +4,65 @@ import "sort"
import "image"
import "github.com/jezek/xgbutil/xgraphics"
func (this *pen) textureRectangle (bounds image.Rectangle) {
if this.texture.Opaque() {
this.textureRectangleOpaque(bounds)
} else {
this.textureRectangleTransparent(bounds)
}
}
func (this *pen) textureRectangleOpaque (bounds image.Rectangle) {
dstBounds := bounds.Intersect(this.image.Bounds())
var pos image.Point
dst := this.image.Pix
src := this.texture.pix
offset := this.texture.rect.Min.Sub(bounds.Min)
for pos.Y = dstBounds.Min.Y; pos.Y < dstBounds.Max.Y; pos.Y ++ {
for pos.X = dstBounds.Min.X; pos.X < dstBounds.Max.X; pos.X ++ {
srcPos := pos.Add(offset)
dstIndex := this.image.PixOffset(pos.X, pos.Y)
srcIndex := this.texture.PixOffset(srcPos.X, srcPos.Y)
dst[dstIndex + 0] = src[srcIndex + 0]
dst[dstIndex + 1] = src[srcIndex + 1]
dst[dstIndex + 2] = src[srcIndex + 2]
dst[dstIndex + 3] = src[srcIndex + 3]
}}
}
func (this *pen) textureRectangleTransparent (bounds image.Rectangle) {
dstBounds := bounds.Intersect(this.image.Bounds())
var pos image.Point
dst := this.image.Pix
src := this.texture.pix
offset := this.texture.rect.Min.Sub(bounds.Min)
for pos.Y = dstBounds.Min.Y; pos.Y < dstBounds.Max.Y; pos.Y ++ {
for pos.X = dstBounds.Min.X; pos.X < dstBounds.Max.X; pos.X ++ {
srcPos := pos.Add(offset)
dstIndex := this.image.PixOffset(pos.X, pos.Y)
srcIndex := this.texture.PixOffset(srcPos.X, srcPos.Y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
B: dst[dstIndex + 0],
G: dst[dstIndex + 1],
R: dst[dstIndex + 2],
A: dst[dstIndex + 3],
}, xgraphics.BGRA {
B: src[srcIndex + 0],
G: src[srcIndex + 1],
R: src[srcIndex + 2],
A: src[srcIndex + 3],
})
dst[dstIndex + 0] = pixel.B
dst[dstIndex + 1] = pixel.G
dst[dstIndex + 2] = pixel.R
dst[dstIndex + 3] = pixel.A
}}
}
func (this *pen) fillRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
if c.A == 255 {
this.fillRectangleOpaque(c, bounds)
@@ -51,7 +110,7 @@ func (this *pen) strokeRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
this.fillRectangle(c, bounds)
return
}
top := image.Rect (
bounds.Min.X,
bounds.Min.Y,
@@ -82,7 +141,7 @@ func (this *pen) strokeRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
// the polygon filling algorithm is adapted from:
// https://www.alienryderflex.com/polygon_fill/
// (if you write C like that i will disassemble you)
func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) {
if len(points) < 3 { return }
@@ -143,7 +202,7 @@ func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) {
context.fillPolygonHotTransparent()
}
}
}
type fillingContext struct {
@@ -167,7 +226,7 @@ func (context *fillingContext) fillPolygonHotOpaque () {
// constrain boundaries to image size
if left < context.min { left = context.min }
if right > context.max { right = context.max }
// fill pixels in between
for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y)
@@ -192,7 +251,7 @@ func (context *fillingContext) fillPolygonHotTransparent () {
// constrain boundaries to image size
if left < context.min { left = context.min }
if right > context.max { right = context.max }
// fill pixels in between
for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y)
@@ -227,3 +286,11 @@ func (this *pen) polyLine (c xgraphics.BGRA, points ...image.Point) {
prevPoint = point
}
}
func wrap (n, min, max int) int {
max -= min
n -= min
n %= max
if n < 0 { n += max }
return n + min
}

View File

@@ -3,6 +3,7 @@ package xcanvas
import "image"
import "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.
@@ -17,7 +19,7 @@ func NewTextureFrom (source image.Image) *Texture {
bounds := source.Bounds()
texture := &Texture {
pix: make([]uint8, bounds.Dx() * bounds.Dy() * 4),
stride: bounds.Dx(),
stride: bounds.Dx() * 4,
rect: bounds.Sub(bounds.Min),
}
@@ -36,15 +38,47 @@ func NewTextureFrom (source image.Image) *Texture {
texture.transparent = true
}
}}
return texture
}
func (this *Texture) BGRAAt (x, y int) xgraphics.BGRA {
if !(image.Point{ x, y }.In(this.rect)) {
return xgraphics.BGRA { }
}
index := this.PixOffset(x, y)
return xgraphics.BGRA {
B: this.pix[index ],
G: this.pix[index + 1],
R: this.pix[index + 2],
A: this.pix[index + 3],
}
}
func (this *Texture) At (x, y int) color.Color {
return this.BGRAAt(x, y)
}
// Bounds returns the bounding rectangle of this texture.
func (this *Texture) Bounds () image.Rectangle {
return this.rect
}
func (this *Texture) ColorModel () color.Model {
return xgraphics.BGRAModel
}
// Opaque reports whether or not the texture is fully opaque.
func (this *Texture) Opaque () bool {
return !this.transparent
}
func (this *Texture) PixOffset (x, y int) int {
x = wrap(x, this.rect.Min.X, this.rect.Max.X)
y = wrap(y, this.rect.Min.Y, this.rect.Max.Y)
return x * 4 + y * this.stride
}
// Close frees the texture from memory.
func (this *Texture) Close () error {
// i lied we dont actually need to close this, but we will once this
@@ -61,6 +95,9 @@ func (this *Texture) Clip (bounds image.Rectangle) canvas.Texture {
// AssertTexture checks if a given canvas.Texture is a texture from this package.
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.Clip(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,21 @@ 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) {
this.box.SetColor(c)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTexture (texture canvas.Texture) {
this.box.SetTexture(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetOverflow (horizontal, vertical bool) {
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal
@@ -57,8 +68,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 +94,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)
@@ -83,7 +106,7 @@ func (this *containerBox) Delete (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()
@@ -97,7 +120,7 @@ func (this *containerBox) Insert (child, before tomo.Object) {
beforeBox := assertAnyBox(before.GetBox())
index := indexOf(this.children, tomo.Box(beforeBox))
if index < 0 { return }
box.setParent(this)
this.children = insert(this.children, index, tomo.Box(box))
this.invalidateLayout()
@@ -132,41 +155,44 @@ func (this *containerBox) SetLayout (layout tomo.Layout) {
func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
pen.Fill(this.color)
rocks := make([]image.Rectangle, len(this.children))
for index, box := range this.children {
rocks[index] = box.Bounds()
}
for _, tile := range canvas.Shatter(this.bounds, rocks...) {
if this.fillTransparent && this.parent != nil {
this.parent.drawBackgroundPart(can.Clip(tile))
}
if this.texture == nil || !this.texture.Opaque() {
pen.Rectangle(tile)
}
if this.texture != nil {
// TODO draw texture
clipped := can.Clip(tile)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(clipped)
}
if clipped == nil { continue }
pen := clipped.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
pen.Rectangle(this.innerClippingBounds)
}
}
func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
if can == nil { return }
if this.fillTransparent && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen := can.Pen()
pen.Fill(this.color)
if this.texture == nil || !this.texture.Opaque() {
pen.Rectangle(can.Bounds())
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
if this.texture != nil {
// TODO draw texture
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)
}
}
}
@@ -237,15 +263,15 @@ 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 !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.box.boxUnder(point, category)
}
func (this *containerBox) propagate (callback func (anyBox) bool) bool {
@@ -259,11 +285,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]
}

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,
@@ -208,7 +233,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 +266,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 +286,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,7 +338,7 @@ func (window *window) handleMotionNotify (
handled = true
}
underneath := window.boxUnder(image.Pt(x, y))
underneath := window.boxUnder(image.Pt(x, y), eventCategoryMouse)
window.hover(underneath)
if !handled {
@@ -391,6 +411,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,
) (

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.26.1
git.tebibyte.media/tomo/typeset v0.5.2
git.tebibyte.media/tomo/tomo v0.29.0
git.tebibyte.media/tomo/typeset v0.7.0
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.26.1 h1:V5ciRuixMYb79aAawgquFEfJ1icyEmMKBKFPWwi94NE=
git.tebibyte.media/tomo/tomo v0.26.1/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.29.0 h1:uvdPaEQYcWH965y85SjIKwhLklnTbs+x6MRwLfdRvfo=
git.tebibyte.media/tomo/tomo v0.29.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/typeset v0.7.0 h1:JFpEuGmN6R2XSCvkINYxpH0AyYUqqs+dZYr6OSd91y0=
git.tebibyte.media/tomo/typeset v0.7.0/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=

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 () {
@@ -213,7 +243,7 @@ func (window *window) afterEvent () {
childBounds = childBounds.Sub(childBounds.Min)
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()
}
@@ -146,6 +142,11 @@ func (this *textBox) Draw (can canvas.Canvas) {
this.drawBorders(can)
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(can.Bounds())
if this.selectable && this.Focused() {
@@ -270,16 +271,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())
@@ -290,12 +288,62 @@ 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)
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 > width - innerBounds.Dx() {
this.scroll.X = width - innerBounds.Dx()
}
// Y
if height <= innerBounds.Dy() {
this.scroll.Y = 0
} else if this.scroll.Y < 0 {
this.scroll.Y = 0
} else if this.scroll.Y > height - innerBounds.Dy() {
this.scroll.Y = height - innerBounds.Dy()
}
}
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

@@ -1,5 +1,7 @@
package x
import "image/color"
func indexOf[T comparable] (haystack []T, needle T) int {
for index, test := range haystack {
if test == needle {
@@ -18,3 +20,8 @@ func insert[T any] (slice []T, index int, element T) []T {
slice[index] = element
return slice
}
func transparent (c color.Color) bool {
_, _, _, a := c.RGBA()
return a != 0xFFFF
}

View File

@@ -73,6 +73,20 @@ func (backend *Backend) NewWindow (
return output, err
}
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,