61 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
f9eaab89ff Rewrite readme 2023-09-04 02:53:04 -04:00
Sasha Koshka
f48bf0609f Upgraded tomo version 2023-09-04 02:33:47 -04:00
ab80658cd9 Textured rectangles are working (if a bit slow) 2023-08-29 17:23:24 -04:00
e2a370259b WIP texture drawing 2023-08-29 15:52:24 -04:00
b5f67c65b0 Cleaned up code relating to transparency 2023-08-25 02:51:07 -04:00
8ed1352fd4 Fixed non-cannonized rectangles being drawn 2023-08-24 17:15:34 -04:00
77ccde9e9b Upgrade tomo version 2023-08-24 15:54:22 -04:00
94db4e8ead Transparency support!! 2023-08-23 19:21:28 -04:00
2f0259d913 WIP moving ggfx stuff into xcanvas 2023-08-22 21:24:38 -04:00
8401b5d0f9 Add support for transparency
Need to break away from ggfx to do this, probably put everything
in xcanvas.
2023-08-22 13:17:48 -04:00
45fa282b61 Texture now checks for transparent pixels 2023-08-21 22:39:13 -04:00
fae918a196 Added textures to backend 2023-08-21 00:35:55 -04:00
3cffcfd4e5 Upgraded tomo version 2023-08-21 00:34:57 -04:00
ff8875535d Changed the way minimum sizes are calculated
Boxes that need their minimum size to be updated now use a map like
for layout and drawing. Size set with MinimumSize is now treated as
separate from the content size and the larger size is used.
2023-08-17 23:20:08 -04:00
00629a863d Behavior relating to hovering is more solid 2023-08-12 12:15:34 -04:00
e126c01055 Add support for mouse hover events 2023-08-12 12:10:47 -04:00
19dbc73968 Fixed keyboard navigation 2023-08-12 11:55:25 -04:00
2de079d063 I forgor 2023-08-12 01:03:34 -04:00
009ae811d3 Box.Box now returns the correct value 2023-08-08 12:56:49 -04:00
5ba72cd1e5 Upgrade tomo version 2023-08-08 12:12:23 -04:00
Sasha Koshka
c6ee141d88 Upgrade typeset 2023-08-07 21:14:12 -04:00
23 changed files with 1565 additions and 385 deletions

View File

@@ -1,3 +1,5 @@
# 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.

View File

@@ -48,9 +48,7 @@ func (backend *Backend) Run () error {
case <- pingQuit:
return nil // FIXME: if we exited due to an error say so
}
for _, window := range backend.windows {
window.afterEvent()
}
backend.afterEvent()
}
}
@@ -78,3 +76,9 @@ func (backend *Backend) Do (callback func ()) {
func (backend *Backend) assert () {
if backend == nil { panic("nil backend") }
}
func (backend *Backend) afterEvent () {
for _, window := range backend.windows {
window.afterEvent()
}
}

431
box.go
View File

@@ -3,22 +3,36 @@ package x
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/x/canvas"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/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
visible bool
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
padding tomo.Inset
border []tomo.Border
color color.Color
texture *xcanvas.Texture
textureMode textureMode
dndData data.Data
dndAccept []data.Mime
@@ -45,18 +59,28 @@ type box struct {
}
}
func (backend *Backend) NewBox() tomo.Box {
func (backend *Backend) newBox (outer anyBox) *box {
box := &box {
backend: backend,
color: color.White,
color: color.Transparent,
outer: outer,
drawer: outer,
}
box.drawer = box
box.outer = box
if outer == nil {
box.drawer = box
box.outer = box
}
box.invalidateMinimum()
return box
}
func (this *box) Box () tomo.Box {
return this
func (backend *Backend) NewBox() tomo.Box {
box := backend.newBox(nil)
return box
}
func (this *box) GetBox () tomo.Box {
return this.outer
}
func (this *box) Window () tomo.Window {
@@ -68,7 +92,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 {
@@ -86,10 +110,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
@@ -97,31 +117,72 @@ func (this *box) SetBounds (bounds image.Rectangle) {
}
func (this *box) SetColor (c color.Color) {
if c == nil { c = color.Transparent }
if this.color == c { return }
this.color = c
this.invalidateDraw()
}
func (this *box) SetBorder (border ...tomo.Border) {
this.border = border
this.invalidateLayout()
this.recalculateMinimumSize()
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
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) {
if this.minSize == size { return }
this.minSize = size
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
if this.userMinSize == size { return }
this.userMinSize = size
this.invalidateMinimum()
}
func (this *box) SetPadding (padding tomo.Inset) {
if this.padding == padding { return }
this.padding = padding
this.invalidateLayout()
this.recalculateMinimumSize()
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) {
@@ -133,9 +194,11 @@ func (this *box) SetDNDAccept (types ...data.Mime) {
}
func (this *box) SetFocused (focused bool) {
if this.parent == nil { return }
window := this.parent.window()
if window == nil { return }
if this.parent == nil || this.parent.window () == nil {
focusedCopy := focused
this.focusQueued = &focusedCopy
return
}
if !this.focusable { return }
if this.Focused () && !focused {
@@ -208,124 +271,21 @@ func (this *box) OnKeyDown (callback func(key input.Key, numberPad bool)) event.
func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Cookie {
return this.on.keyUp.Connect(callback)
}
// -------------------------------------------------------------------------- //
func (this *box) Draw (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
pen.Fill(this.color)
pen.Rectangle(this.bounds)
}
func (this *box) drawBorders (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
bounds := this.bounds
for _, border := range this.border {
pen.Fill(border.Color[tomo.SideTop])
pen.Rectangle(image.Rect (
bounds.Min.X,
bounds.Min.Y,
bounds.Max.X,
bounds.Min.Y + border.Width[tomo.SideTop]))
pen.Fill(border.Color[tomo.SideBottom])
pen.Rectangle(image.Rect (
bounds.Min.X,
bounds.Max.Y - border.Width[tomo.SideBottom],
bounds.Max.X,
bounds.Max.Y))
pen.Fill(border.Color[tomo.SideLeft])
pen.Rectangle(image.Rect (
bounds.Min.X,
bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Min.X + border.Width[tomo.SideLeft],
bounds.Max.Y - border.Width[tomo.SideBottom]))
pen.Fill(border.Color[tomo.SideRight])
pen.Rectangle(image.Rect (
bounds.Max.X - border.Width[tomo.SideRight],
bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Max.X,
bounds.Max.Y - border.Width[tomo.SideBottom]))
bounds = border.Width.Apply(bounds)
}
}
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()))
}
}
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)
}
func (this *box) setParent (parent parent) {
if this.parent != parent && this.Focused() {
this.SetFocused(false)
}
this.parent = parent
}
func (this *box) recursiveRedo () {
this.doLayout()
this.doDraw()
}
func (this *box) invalidateLayout () {
if this.parent == nil || this.parent.window() == nil { return }
this.parent.window().invalidateLayout(this.outer)
}
func (this *box) invalidateDraw () {
if this.parent == nil || this.parent.window() == nil { return }
this.parent.window().invalidateDraw(this.outer)
}
func (this *box) recalculateMinimumSize () {
if this.outer != anyBox(this) {
this.outer.recalculateMinimumSize()
}
}
func (this *box) canBeFocused () bool {
return this.focusable
}
func (this *box) boxUnder (point image.Point) anyBox {
if point.In(this.bounds) {
return this.outer
} else {
return nil
}
}
func (this *box) handleFocusEnter () {
this.on.focusEnter.Broadcast()
}
func (this *box) handleFocusLeave () {
this.on.focusLeave.Broadcast()
}
func (this *box) handleMouseEnter () {
this.on.mouseEnter.Broadcast()
}
func (this *box) handleMouseLeave () {
this.on.mouseLeave.Broadcast()
}
func (this *box) handleMouseMove () {
this.on.mouseMove.Broadcast()
}
func (this *box) handleMouseDown (button input.Button) {
if this.focusable {
this.SetFocused(true)
@@ -339,29 +299,224 @@ func (this *box) handleMouseDown (button input.Button) {
listener(button)
}
}
func (this *box) handleMouseUp (button input.Button) {
for _, listener := range this.on.mouseUp.Listeners() {
listener(button)
}
}
func (this *box) handleScroll (x, y float64) {
for _, listener := range this.on.scroll.Listeners() {
listener(x, y)
}
}
func (this *box) handleKeyDown (key input.Key, numberPad bool) {
for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad)
}
}
func (this *box) handleKeyUp (key input.Key, numberPad bool) {
for _, listener := range this.on.keyUp.Listeners() {
listener(key, numberPad)
}
}
// -------------------------------------------------------------------------- //
func (this *box) Draw (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
bounds := this.Bounds()
// background
pen.Fill(this.color)
if this.textureMode == textureModeTile {
pen.Texture(this.texture)
}
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
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) {
if can == nil { return }
pen := can.Pen()
bounds := this.bounds
rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1)
if transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.SubCanvas(area))
}
pen.Fill(c)
pen.Rectangle(area)
}
for _, border := range this.border {
rectangle (
bounds.Min.X,
bounds.Min.Y,
bounds.Max.X,
bounds.Min.Y + border.Width[tomo.SideTop],
border.Color[tomo.SideTop])
rectangle (
bounds.Min.X,
bounds.Max.Y - border.Width[tomo.SideBottom],
bounds.Max.X,
bounds.Max.Y,
border.Color[tomo.SideBottom])
rectangle (
bounds.Min.X,
bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Min.X + border.Width[tomo.SideLeft],
bounds.Max.Y - border.Width[tomo.SideBottom],
border.Color[tomo.SideLeft])
rectangle (
bounds.Max.X - border.Width[tomo.SideRight],
bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Max.X,
bounds.Max.Y - border.Width[tomo.SideBottom],
border.Color[tomo.SideRight])
bounds = border.Width.Apply(bounds)
}
}
func (this *box) contentMinimum () image.Point {
var minimum image.Point
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
return minimum
}
func (this *box) doMinimumSize () {
this.minSize = this.outer.contentMinimum()
if this.minSize.X < this.userMinSize.X {
this.minSize.X = this.userMinSize.X
}
if this.minSize.Y < this.userMinSize.Y {
this.minSize.Y = this.userMinSize.Y
}
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
}
// var drawcnt int
func (this *box) doDraw () {
// println("DRAW", drawcnt)
// drawcnt ++
if this.canvas == nil { return }
if this.drawer != nil {
this.drawBorders(this.canvas)
this.drawer.Draw(this.canvas.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.SubCanvas(this.bounds)
}
func (this *box) setParent (parent parent) {
if this.parent != parent && this.Focused() {
this.SetFocused(false)
}
this.parent = parent
}
func (this *box) getParent () parent {
return this.parent
}
func (this *box) flushActionQueue () {
if this.parent == nil || this.parent.window() == nil { return }
if this.minSizeQueued {
this.invalidateMinimum()
}
if this.focusQueued != nil {
this.SetFocused(*this.focusQueued)
}
}
func (this *box) recursiveRedo () {
this.doLayout()
this.doDraw()
}
func (this *box) invalidateLayout () {
if this.parent == nil || this.parent.window() == nil { return }
this.window().invalidateLayout(this.outer)
}
func (this *box) invalidateDraw () {
if this.parent == nil || this.parent.window() == nil { return }
this.window().invalidateDraw(this.outer)
}
func (this *box) invalidateMinimum () {
if this.parent == nil || this.parent.window() == nil {
this.minSizeQueued = true
return
}
this.window().invalidateMinimum(this.outer)
}
func (this *box) canBeFocused () bool {
return this.focusable
}
func (this *box) boxUnder (point image.Point, category eventCategory) anyBox {
if point.In(this.bounds) {
return this.outer
} else {
return nil
}
}
func (this *box) propagate (callback func (anyBox) bool) bool {
return callback(this)
return callback(this.outer)
}
func (this *box) propagateAlt (callback func (anyBox) bool) bool {
return callback(this)
return callback(this.outer)
}
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

@@ -4,22 +4,23 @@ import "image"
import "image/color"
import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto"
import "git.tebibyte.media/tomo/ggfx"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/tomo/tomo/canvas"
// Canvas satisfies the canvas.Canvas interface. It draws to an xgraphics.Image.
// It must be closed after use.
type Canvas struct {
*xgraphics.Image
}
// New creates a new canvas from a bounding rectangle.
func New (x *xgbutil.XUtil, bounds image.Rectangle) *Canvas {
return NewFrom(xgraphics.New(x, bounds))
// NewCanvas creates a new canvas from a bounding rectangle.
func NewCanvas (x *xgbutil.XUtil, bounds image.Rectangle) *Canvas {
return NewCanvasFrom(xgraphics.New(x, bounds))
}
// NewFrom creates a new canvas from an existing xgraphics.Image.
func NewFrom (image *xgraphics.Image) *Canvas {
// NewCanvasFrom creates a new canvas from an existing xgraphics.Image. Note
// that calling Close() on the resulting canvas will destroy this image.
func NewCanvasFrom (image *xgraphics.Image) *Canvas {
if image == nil { return nil }
return &Canvas { image }
}
@@ -28,17 +29,11 @@ func NewFrom (image *xgraphics.Image) *Canvas {
func (this *Canvas) Pen () canvas.Pen {
return &pen {
image: this.Image,
gfx: ggfx.Image[uint8] {
Pix: this.Image.Pix,
Stride: this.Image.Stride,
Rect: this.Image.Rect,
Width: 4,
},
}
}
// 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 }
@@ -53,6 +48,12 @@ func (this *Canvas) Push (window xproto.Window) {
this.XExpPaint(window, this.Bounds().Min.X, this.Bounds().Min.Y)
}
// Close frees this canvas from the X server.
func (this *Canvas) Close () {
this.assert()
this.Image.Destroy()
}
func (this *Canvas) assert () {
if this == nil { panic("nil canvas") }
}
@@ -64,50 +65,69 @@ func (this *Canvas) assert () {
type pen struct {
image *xgraphics.Image
gfx ggfx.Image[uint8]
closed bool
endCap canvas.Cap
joint canvas.Joint
weight int
align canvas.StrokeAlign
stroke [4]uint8
fill [4]uint8
closed bool
endCap canvas.Cap
joint canvas.Joint
weight int
align canvas.StrokeAlign
stroke xgraphics.BGRA
fill xgraphics.BGRA
texture *Texture
}
func (this *pen) Rectangle (bounds image.Rectangle) {
bounds = bounds.Canon()
if this.weight == 0 {
this.gfx.FillRectangle(this.fill[:], bounds)
if this.fill.A > 0 && !this.textureObscures() {
this.fillRectangle(this.fill, bounds)
}
if this.texture != nil {
this.textureRectangle(bounds)
}
} else {
this.gfx.StrokeRectangle(this.stroke[:], this.weight, bounds)
if this.stroke.A > 0 {
this.strokeRectangle(this.stroke, bounds)
}
}
}
func (this *pen) Path (points ...image.Point) {
if this.weight == 0 {
this.gfx.FillPolygon(this.fill[:], points...)
} else if this.closed {
this.gfx.StrokePolygon(this.stroke[:], this.weight, points...)
if this.fill.A > 0 {
this.fillPolygon(this.fill, points...)
}
} else if this.closed && len(points) > 2 {
if this.stroke.A > 0 {
this.strokePolygon(this.stroke, points...)
}
} else {
this.gfx.PolyLine(this.stroke[:], this.weight, points...)
if this.stroke.A > 0 {
this.polyLine(this.stroke, points...)
}
}
}
func (this *pen) Closed (closed bool) { this.closed = closed }
func (this *pen) Cap (endCap canvas.Cap) { this.endCap = endCap }
func (this *pen) Joint (joint canvas.Joint) { this.joint = joint }
func (this *pen) StrokeWeight (weight int) { this.weight = weight }
func (this *pen) StrokeAlign (align canvas.StrokeAlign) { this.align = align }
func (this *pen) Stroke (stroke color.Color) { this.stroke = convertColor(stroke) }
func (this *pen) Fill (fill color.Color) { this.fill = convertColor(fill) }
func (this *pen) Stroke (stroke color.Color) { this.stroke = convertColor(stroke) }
func (this *pen) Fill (fill color.Color) { this.fill = convertColor(fill) }
func (this *pen) Texture (texture canvas.Texture) { this.texture = AssertTexture(texture) }
func convertColor (c color.Color) [4]uint8 {
func (this *pen) textureObscures () bool {
return this.texture != nil && this.texture.Opaque()
}
func convertColor (c color.Color) xgraphics.BGRA {
r, g, b, a := c.RGBA()
return [4]uint8 {
uint8(b >> 8),
uint8(g >> 8),
uint8(r >> 8),
uint8(a >> 8),
return xgraphics.BGRA {
B: uint8(b >> 8),
G: uint8(g >> 8),
R: uint8(r >> 8),
A: uint8(a >> 8),
}
}

296
canvas/draw.go Normal file
View File

@@ -0,0 +1,296 @@
package xcanvas
import "sort"
import "image"
import "github.com/jezek/xgbutil/xgraphics"
func (this *pen) textureRectangle (bounds image.Rectangle) {
if this.texture.Opaque() {
this.textureRectangleOpaque(bounds)
} else {
this.textureRectangleTransparent(bounds)
}
}
func (this *pen) textureRectangleOpaque (bounds image.Rectangle) {
dstBounds := bounds.Intersect(this.image.Bounds())
var pos image.Point
dst := this.image.Pix
src := this.texture.pix
offset := this.texture.rect.Min.Sub(bounds.Min)
for pos.Y = dstBounds.Min.Y; pos.Y < dstBounds.Max.Y; pos.Y ++ {
for pos.X = dstBounds.Min.X; pos.X < dstBounds.Max.X; pos.X ++ {
srcPos := pos.Add(offset)
dstIndex := this.image.PixOffset(pos.X, pos.Y)
srcIndex := this.texture.PixOffset(srcPos.X, srcPos.Y)
dst[dstIndex + 0] = src[srcIndex + 0]
dst[dstIndex + 1] = src[srcIndex + 1]
dst[dstIndex + 2] = src[srcIndex + 2]
dst[dstIndex + 3] = src[srcIndex + 3]
}}
}
func (this *pen) textureRectangleTransparent (bounds image.Rectangle) {
dstBounds := bounds.Intersect(this.image.Bounds())
var pos image.Point
dst := this.image.Pix
src := this.texture.pix
offset := this.texture.rect.Min.Sub(bounds.Min)
for pos.Y = dstBounds.Min.Y; pos.Y < dstBounds.Max.Y; pos.Y ++ {
for pos.X = dstBounds.Min.X; pos.X < dstBounds.Max.X; pos.X ++ {
srcPos := pos.Add(offset)
dstIndex := this.image.PixOffset(pos.X, pos.Y)
srcIndex := this.texture.PixOffset(srcPos.X, srcPos.Y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
B: dst[dstIndex + 0],
G: dst[dstIndex + 1],
R: dst[dstIndex + 2],
A: dst[dstIndex + 3],
}, xgraphics.BGRA {
B: src[srcIndex + 0],
G: src[srcIndex + 1],
R: src[srcIndex + 2],
A: src[srcIndex + 3],
})
dst[dstIndex + 0] = pixel.B
dst[dstIndex + 1] = pixel.G
dst[dstIndex + 2] = pixel.R
dst[dstIndex + 3] = pixel.A
}}
}
func (this *pen) fillRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
if c.A == 255 {
this.fillRectangleOpaque(c, bounds)
} else {
this.fillRectangleTransparent(c, bounds)
}
}
func (this *pen) fillRectangleOpaque (c xgraphics.BGRA, bounds image.Rectangle) {
bounds = bounds.Intersect(this.image.Bounds())
var pos image.Point
for pos.Y = bounds.Min.Y; pos.Y < bounds.Max.Y; pos.Y ++ {
for pos.X = bounds.Min.X; pos.X < bounds.Max.X; pos.X ++ {
index := this.image.PixOffset(pos.X, pos.Y)
this.image.Pix[index + 0] = c.B
this.image.Pix[index + 1] = c.G
this.image.Pix[index + 2] = c.R
this.image.Pix[index + 3] = c.A
}}
}
func (this *pen) fillRectangleTransparent (c xgraphics.BGRA, bounds image.Rectangle) {
bounds = bounds.Intersect(this.image.Bounds())
var pos image.Point
for pos.Y = bounds.Min.Y; pos.Y < bounds.Max.Y; pos.Y ++ {
for pos.X = bounds.Min.X; pos.X < bounds.Max.X; pos.X ++ {
index := this.image.PixOffset(pos.X, pos.Y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
B: this.image.Pix[index + 0],
G: this.image.Pix[index + 1],
R: this.image.Pix[index + 2],
A: this.image.Pix[index + 3],
}, c)
this.image.Pix[index + 0] = pixel.B
this.image.Pix[index + 1] = pixel.G
this.image.Pix[index + 2] = pixel.R
this.image.Pix[index + 3] = pixel.A
}}
}
func (this *pen) strokeRectangle (c xgraphics.BGRA, bounds image.Rectangle) {
if this.weight > bounds.Dx() / 2 || this.weight > bounds.Dy() / 2 {
this.fillRectangle(c, bounds)
return
}
top := image.Rect (
bounds.Min.X,
bounds.Min.Y,
bounds.Max.X,
bounds.Min.Y + this.weight)
bottom := image.Rect (
bounds.Min.X,
bounds.Max.Y - this.weight,
bounds.Max.X,
bounds.Max.Y)
left := image.Rect (
bounds.Min.X,
bounds.Min.Y + this.weight,
bounds.Min.X + this.weight,
bounds.Max.Y - this.weight)
right := image.Rect (
bounds.Max.X - this.weight,
bounds.Min.Y + this.weight,
bounds.Max.X,
bounds.Max.Y - this.weight)
this.fillRectangle(c, top,)
this.fillRectangle(c, bottom,)
this.fillRectangle(c, left,)
this.fillRectangle(c, right,)
}
// the polygon filling algorithm is adapted from:
// https://www.alienryderflex.com/polygon_fill/
// (if you write C like that i will disassemble you)
func (this *pen) fillPolygon (c xgraphics.BGRA, points ...image.Point) {
if len(points) < 3 { return }
// figure out the bounds of the polygon so we don't test empty space
var area image.Rectangle
area.Min = points[0]
area.Max = points[0]
for _, point := range points[1:] {
if point.X < area.Min.X { area.Min.X = point.X }
if point.Y < area.Min.Y { area.Min.Y = point.Y }
if point.X > area.Max.X { area.Max.X = point.X }
if point.Y > area.Max.Y { area.Max.Y = point.Y }
}
area = this.image.Bounds().Intersect(area)
if area.Empty() { return }
context := fillingContext {
image: this.image,
color: this.fill,
min: area.Min.X,
max: area.Max.X,
boundaries: make([]int, len(points)),
points: points,
}
for context.y = area.Min.Y; context.y < area.Max.Y; context.y ++ {
// build boundary list
boundaryCount := 0
prevPoint := points[len(points) - 1]
for _, point := range points {
fy := float64(context.y)
fPointX := float64(point.X)
fPointY := float64(point.Y)
fPrevX := float64(prevPoint.X)
fPrevY := float64(prevPoint.Y)
addboundary :=
(fPointY < fy && fPrevY >= fy) ||
(fPrevY < fy && fPointY >= fy)
if addboundary {
context.boundaries[boundaryCount] = int (
fPointX +
(fy - fPointY) /
(fPrevY - fPointY) *
(fPrevX - fPointX))
boundaryCount ++
}
prevPoint = point
}
// sort boundary list
cutBoundaries := context.boundaries[:boundaryCount]
sort.Ints(cutBoundaries)
// fill pixels between boundary pairs
if c.A == 255 {
context.fillPolygonHotOpaque()
} else {
context.fillPolygonHotTransparent()
}
}
}
type fillingContext struct {
image *xgraphics.Image
color xgraphics.BGRA
min, max int
y int
boundaries []int
points []image.Point
}
func (context *fillingContext) fillPolygonHotOpaque () {
for index := 0; index < len(context.boundaries); index += 2 {
left := context.boundaries[index]
right := context.boundaries[index + 1]
// stop if we have exited the polygon
if left >= context.max { break }
// begin filling if we are within the polygon
if right > context.min {
// constrain boundaries to image size
if left < context.min { left = context.min }
if right > context.max { right = context.max }
// fill pixels in between
for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y)
context.image.Pix[index + 0] = context.color.B
context.image.Pix[index + 1] = context.color.G
context.image.Pix[index + 2] = context.color.R
context.image.Pix[index + 3] = context.color.A
}
}
}
}
func (context *fillingContext) fillPolygonHotTransparent () {
for index := 0; index < len(context.boundaries); index += 2 {
left := context.boundaries[index]
right := context.boundaries[index + 1]
// stop if we have exited the polygon
if left >= context.max { break }
// begin filling if we are within the polygon
if right > context.min {
// constrain boundaries to image size
if left < context.min { left = context.min }
if right > context.max { right = context.max }
// fill pixels in between
for x := left; x < right; x ++ {
index := context.image.PixOffset(x, context.y)
pixel := xgraphics.BlendBGRA(xgraphics.BGRA {
B: context.image.Pix[index + 0],
G: context.image.Pix[index + 1],
R: context.image.Pix[index + 2],
A: context.image.Pix[index + 3],
}, context.color)
context.image.Pix[index + 0] = pixel.B
context.image.Pix[index + 1] = pixel.G
context.image.Pix[index + 2] = pixel.R
context.image.Pix[index + 3] = pixel.A
}
}
}
}
func (this *pen) strokePolygon (c xgraphics.BGRA, points ...image.Point) {
prevPoint := points[len(points) - 1]
for _, point := range points {
this.line(c, prevPoint, point)
prevPoint = point
}
}
func (this *pen) polyLine (c xgraphics.BGRA, points ...image.Point) {
if len(points) < 2 { return }
prevPoint := points[0]
for _, point := range points[1:] {
this.line(c, prevPoint, point)
prevPoint = point
}
}
func wrap (n, min, max int) int {
max -= min
n -= min
n %= max
if n < 0 { n += max }
return n + min
}

95
canvas/line.go Normal file
View File

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

47
canvas/plot.go Normal file
View File

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

106
canvas/texture.go Normal file
View File

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

View File

@@ -5,14 +5,14 @@ import "git.tebibyte.media/tomo/tomo/canvas"
type canvasBox struct {
*box
userDrawer canvas.Drawer
}
func (backend *Backend) NewCanvasBox () tomo.CanvasBox {
box := &canvasBox {
box: backend.NewBox().(*box),
}
box.outer = box
return box
this := &canvasBox { }
this.box = backend.newBox(this)
this.drawer = this
return this
}
func (this *canvasBox) Box () tomo.Box {
@@ -20,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,17 +25,27 @@ type containerBox struct {
}
func (backend *Backend) NewContainerBox() tomo.ContainerBox {
box := &containerBox {
box: backend.NewBox().(*box),
propagateEvents: true,
}
box.drawer = box
box.outer = box
return box
this := &containerBox { }
this.box = backend.newBox(this)
return this
}
func (this *containerBox) Box () tomo.Box {
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) {
@@ -56,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()
}
@@ -65,50 +76,67 @@ 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) {
if this.gap == gap { return }
this.gap = gap
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.Box())
box := assertAnyBox(child.GetBox())
if indexOf(this.children, tomo.Box(box)) > -1 { return }
box.setParent(this)
box.flushActionQueue()
this.children = append(this.children, box)
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Delete (child tomo.Object) {
box := assertAnyBox(child.Box())
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()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Insert (child, before tomo.Object) {
box := assertAnyBox(child.Box())
box := assertAnyBox(child.GetBox())
if indexOf(this.children, tomo.Box(box)) > -1 { return }
beforeBox := assertAnyBox(before.Box())
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.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Clear () {
@@ -117,7 +145,7 @@ func (this *containerBox) Clear () {
}
this.children = nil
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Length () int {
@@ -134,24 +162,59 @@ func (this *containerBox) At (index int) tomo.Object {
func (this *containerBox) SetLayout (layout tomo.Layout) {
this.layout = layout
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return }
this.drawBorders(can)
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...) {
pen.Rectangle(tile)
clipped := can.SubCanvas(tile)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(clipped)
}
if clipped == nil { continue }
pen := clipped.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
pen.Rectangle(this.innerClippingBounds)
}
}
func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(this.innerClippingBounds)
}
func (this *containerBox) invalidateTransparentChildren () {
window := this.window()
if window == nil { return }
for _, box := range this.children {
box := assertAnyBox(box)
if box.transparent() {
window.invalidateDraw(box)
}
}
}
func (this *containerBox) flushActionQueue () {
for _, box := range this.children {
box.(anyBox).flushActionQueue()
}
this.box.flushActionQueue()
}
func (this *containerBox) window () *window {
if this.parent == nil { return nil }
return this.parent.window()
@@ -162,8 +225,7 @@ func (this *containerBox) canvas () canvas.Canvas {
}
func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.recalculateMinimumSize()
this.invalidateMinimum()
size := child.MinimumSize()
bounds := child.Bounds()
if bounds.Dx() < size.X || bounds.Dy() < size.Y {
@@ -172,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,
@@ -183,31 +243,89 @@ func (this *containerBox) layoutHints () tomo.LayoutHints {
}
}
func (this *containerBox) recalculateMinimumSize () {
if this.layout == nil {
this.SetMinimumSize(image.Point { })
return
func (this *containerBox) contentMinimum () image.Point {
minimum := this.box.contentMinimum()
if this.layout != nil {
layoutMinimum := this.layout.MinimumSize (
this.layoutHints(),
this.children)
if this.hOverflow { layoutMinimum.X = 0 }
if this.vOverflow { layoutMinimum.Y = 0 }
minimum = minimum.Add(layoutMinimum)
}
minimum := this.layout.MinimumSize(this.layoutHints(), this.children)
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
return minimum
}
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()
@@ -216,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 {
@@ -238,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]
}

100
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))
@@ -293,6 +314,7 @@ func (window *window) handleButtonRelease (
window.updateModifiers(buttonEvent.State)
window.updateMousePosition(buttonEvent.EventX, buttonEvent.EventY)
dragging := window.drags[buttonEvent.Detail]
window.drags[buttonEvent.Detail] = nil
if dragging != nil {
dragging.handleMouseUp(input.Button(buttonEvent.Detail))
@@ -317,9 +339,14 @@ func (window *window) handleMotionNotify (
handled = true
}
if !handled {
window.boxUnder(image.Pt(x, y)).handleMouseMove()
underneath := window.boxUnder(image.Pt(x, y), eventCategoryMouse)
if underneath != nil {
window.hover(underneath)
if !handled {
underneath.handleMouseMove()
}
}
}
func (window *window) compressExpose (
@@ -387,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
}

7
go.mod
View File

@@ -3,13 +3,12 @@ module git.tebibyte.media/tomo/x
go 1.20
require (
git.tebibyte.media/tomo/ggfx v0.4.0
git.tebibyte.media/tomo/tomo v0.18.0
git.tebibyte.media/tomo/typeset v0.5.0
git.tebibyte.media/tomo/tomo v0.33.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
golang.org/x/image v0.9.0
golang.org/x/image v0.11.0
)
require (

16
go.sum
View File

@@ -1,10 +1,8 @@
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/ggfx v0.4.0 h1:3aUHeGS/yYWRV/zCDubBsXnik5ygkMnj/VgrM5Z75A4=
git.tebibyte.media/tomo/ggfx v0.4.0/go.mod h1:zPoz8BdVQyG2KhEmeGFQBK66V71i6Kj8oVFbrZaCwRA=
git.tebibyte.media/tomo/tomo v0.18.0 h1:PdkX9hVV0TZWZzHn6fLx+Vq6W6DwDYuaog4E7djm7FE=
git.tebibyte.media/tomo/tomo v0.18.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY=
git.tebibyte.media/tomo/typeset v0.5.0 h1:EyLFDIIsMKYm+DLB404zbG6JMDYLvWBBbLiHJoAUlKQ=
git.tebibyte.media/tomo/typeset v0.5.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/tomo v0.33.0 h1:BBm1oRsogBLeqVKeevNqG9RPCOdmbGeiQM/9hd2GHE8=
git.tebibyte.media/tomo/tomo v0.33.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=
@@ -18,8 +16,8 @@ github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0/go.mod h1:AHecLyFNy6
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g=
golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -42,7 +40,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

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

104
system.go
View File

@@ -36,17 +36,25 @@ type parent interface {
window () *window
canvas () canvas.Canvas
notifyMinimumSizeChange (anyBox)
drawBackgroundPart (canvas.Canvas)
captures (eventCategory) bool
}
type anyBox interface {
tomo.Box
doDraw ()
doLayout ()
setParent (parent)
recursiveRedo ()
canBeFocused () bool
boxUnder (image.Point) anyBox
recalculateMinimumSize ()
canvas.Drawer
doDraw ()
doLayout ()
doMinimumSize ()
contentMinimum () image.Point
setParent (parent)
getParent () parent
flushActionQueue ()
recursiveRedo ()
canBeFocused () bool
boxUnder (image.Point, eventCategory) anyBox
transparent () bool
propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool
@@ -56,12 +64,12 @@ type anyBox interface {
// handleDndEnter ()
// handleDndLeave ()
// handleDndDrop (data.Data)
// handleMouseEnter ()
// handleMouseLeave ()
handleMouseEnter ()
handleMouseLeave ()
handleMouseMove ()
handleMouseDown (input.Button)
handleMouseUp (input.Button)
// handleScroll (float64, float64)
handleScroll (float64, float64)
handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool)
}
@@ -81,12 +89,13 @@ func (window *window) SetRoot (root tomo.Object) {
if root == nil {
window.root = nil
} else {
box := assertAnyBox(root.Box())
box := assertAnyBox(root.GetBox())
box.setParent(window)
box.flushActionQueue()
window.invalidateLayout(box)
window.root = box
}
window.recalculateMinimumSize()
window.minimumClean = false
}
func (window *window) window () *window {
@@ -98,7 +107,11 @@ func (window *window) canvas () canvas.Canvas {
}
func (window *window) notifyMinimumSizeChange (anyBox) {
window.recalculateMinimumSize()
window.minimumClean = false
}
func (window *window) invalidateMinimum (box anyBox) {
window.needMinimum.Add(box)
}
func (window *window) invalidateDraw (box anyBox) {
@@ -106,35 +119,72 @@ func (window *window) invalidateDraw (box anyBox) {
}
func (window *window) invalidateLayout (box anyBox) {
// TODO: use a priority queue for this and have the value be the amount
// of parents a box has
window.needLayout.Add(box)
window.invalidateDraw(box)
}
func (window *window) focus (box anyBox) {
if window.focused == box { return }
previous := window.focused
window.focused = box
if previous != nil {
window.invalidateDraw(previous)
previous.handleFocusLeave()
}
if box != nil && box.canBeFocused() {
window.invalidateDraw(box)
box.handleFocusEnter()
}
}
func (window *window) hover (box anyBox) {
if window.hovered == box { return }
previous := window.hovered
window.hovered = box
if previous != nil {
previous.handleMouseLeave()
}
if box != nil {
box.handleMouseEnter()
}
}
func (window *window) anyFocused () bool {
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 () {
@@ -191,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()
}
@@ -202,6 +254,12 @@ func (window *window) afterEvent () {
return
}
for len(window.needMinimum) > 0 {
window.needMinimum.Pop().doMinimumSize()
}
if !window.minimumClean {
window.doMinimumSize()
}
for len(window.needLayout) > 0 {
window.needLayout.Pop().doLayout()
}

View File

@@ -13,7 +13,7 @@ import "git.tebibyte.media/tomo/tomo/canvas"
type textBox struct {
*box
hOverflow, vOverflow bool
contentBounds image.Rectangle
scroll image.Point
@@ -23,15 +23,16 @@ type textBox struct {
face font.Face
wrap bool
hAlign tomo.Align
vAlign tomo.Align
selectable bool
selecting bool
selectStart int
dot text.Dot
dotColor color.Color
drawer typeset.Drawer
on struct {
contentBoundsChange event.FuncBroadcaster
dotChange event.FuncBroadcaster
@@ -39,14 +40,12 @@ type textBox struct {
}
func (backend *Backend) NewTextBox() tomo.TextBox {
box := &textBox {
box: backend.NewBox().(*box),
this := &textBox {
textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
}
box.box.drawer = box
box.outer = box
return box
this.box = backend.newBox(this)
return this
}
func (this *textBox) SetOverflow (horizontal, vertical bool) {
@@ -61,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()
}
@@ -74,7 +73,7 @@ func (this *textBox) SetText (text string) {
if this.text == text { return }
this.text = text
this.drawer.SetText([]rune(text))
this.recalculateMinimumSize()
this.invalidateMinimum()
this.invalidateLayout()
}
@@ -88,14 +87,14 @@ func (this *textBox) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.drawer.SetFace(face)
this.recalculateMinimumSize()
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.drawer.SetWrap(wrap)
this.recalculateMinimumSize()
this.invalidateMinimum()
this.invalidateLayout()
}
@@ -117,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()
}
@@ -130,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()
}
@@ -148,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() {
@@ -167,10 +166,11 @@ func fixPt (point image.Point) fixed.Point26_6 {
}
func (this *textBox) drawDot (can canvas.Canvas) {
if this.face == nil { return }
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(this.textColor)
pen.StrokeWeight(1)
bounds := this.InnerBounds()
metrics := this.face.Metrics()
@@ -180,11 +180,12 @@ func (this *textBox) drawDot (can canvas.Canvas) {
height := this.drawer.LineHeight().Round()
ascent := fixed.Point26_6 { Y: metrics.Descent }
descent := fixed.Point26_6 { Y: metrics.Ascent }
switch {
case dot.Empty():
pen.StrokeWeight(1)
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
case start.Y == end.Y:
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
@@ -192,11 +193,11 @@ func (this *textBox) drawDot (can canvas.Canvas) {
Min: roundPt(start.Add(ascent)),
Max: roundPt(end.Sub(descent)),
})
default:
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
rect := image.Rectangle {
Min: roundPt(start.Add(ascent)),
Max: roundPt(start.Sub(descent)),
@@ -221,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()
@@ -269,25 +269,17 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
return bounds.Sub(bounds.Min)
}
func (this *textBox) recalculateMinimumSize () {
minimum := image.Pt (
this.drawer.Em().Round(),
this.drawer.LineHeight().Round())
func (this *textBox) contentMinimum () image.Point {
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()
}
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
return minimum.Add(this.box.contentMinimum())
}
func (this *textBox) doLayout () {
@@ -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.contentBounds = this.normalizedLayoutBoundsSpace().Sub(this.scroll)
this.drawer.SetWidth(innerBounds.Dx())
this.drawer.SetHeight(innerBounds.Dy())
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)
}

9
texture.go Normal file
View File

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

View File

@@ -1,5 +1,7 @@
package x
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

@@ -8,6 +8,7 @@ import "git.tebibyte.media/tomo/x/canvas"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/ewmh"
@@ -31,6 +32,7 @@ type window struct {
modalParent *window
hasModal bool
shy bool
visible bool
metrics struct {
bounds image.Rectangle
@@ -44,10 +46,19 @@ type window struct {
root anyBox
focused anyBox
hovered anyBox
needDraw boxSet
needLayout boxSet
needRedo bool
// TODO: needMinimum and needLayout should be priority queues. for the
// minimums, we need to start at the deeper parts of the layout tree and
// go upward towards the top. for the layouts, we need to start at the
// top of the layout tree and progressively go deeper. this will
// eliminate redundant layout calculations.
needMinimum boxSet
needLayout boxSet
needDraw boxSet
needRedo bool
minimumClean bool
}
func (backend *Backend) NewWindow (
@@ -63,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,
@@ -129,7 +154,7 @@ func (backend *Backend) newWindow (
// Connect(backend.x, window.xWindow.Id)
window.metrics.bounds = bounds
window.setMinimumSize(image.Pt(8, 8))
window.doMinimumSize()
backend.windows[window.xWindow.Id] = window
@@ -241,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 () {
@@ -261,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()
@@ -322,7 +354,7 @@ func (window *window) reallocateCanvas () {
if window.xCanvas != nil {
window.xCanvas.Destroy()
}
window.xCanvas = xcanvas.NewFrom(xgraphics.New (
window.xCanvas = xcanvas.NewCanvasFrom(xgraphics.New (
window.backend.x,
image.Rect (
0, 0,
@@ -345,22 +377,26 @@ func (window *window) pushRegion (region image.Rectangle) {
return
}
subCanvas := window.xCanvas.Clip(region)
subCanvas := window.xCanvas.SubCanvas(region)
if subCanvas == nil {
return
}
subCanvas.(*xcanvas.Canvas).Push(window.xWindow.Id)
}
func (window *window) recalculateMinimumSize () {
rootMinimum := image.Point { }
if window.root != nil {
rootMinimum = window.root.MinimumSize()
}
window.setMinimumSize(rootMinimum)
func (window *window) drawBackgroundPart (canvas.Canvas) {
// no-op for now? maybe eventually windows will be able to have a
// background
}
func (window *window) setMinimumSize (size image.Point) {
func (window *window) doMinimumSize () {
window.minimumClean = true
size := image.Point { }
if window.root != nil {
size = window.root.MinimumSize()
}
if size.X < 8 { size.X = 8 }
if size.Y < 8 { size.Y = 8 }
icccm.WmNormalHintsSet (

View File

@@ -1,17 +0,0 @@
// Plugin x provides the X11 backend as a plugin.
package main
import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/tomo"
func init () {
tomo.Register(x.NewBackend)
}
func Name () string {
return "X"
}
func Description () string {
return "Provides an X11 backend."
}