14 Commits

15 changed files with 308 additions and 103 deletions

View File

@@ -1,6 +1,6 @@
# x # x
[![Go Reference](https://pkg.go.dev/badge/pkg.go.dev/git.tebibyte.media/tomo/x.svg)](https://pkg.go.dev/pkg.go.dev/git.tebibyte.media/tomo/x) [![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/x.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/x)
An X11 backend for Tomo. An X11 backend for Tomo.

11
box.go
View File

@@ -259,6 +259,11 @@ func (this *box) handleMouseUp (button input.Button) {
listener(button) listener(button)
} }
} }
func (this *box) handleScroll (x, y float64) {
for _, listener := range this.on.scroll.Listeners() {
listener(x, y)
}
}
func (this *box) handleKeyDown (key input.Key, numberPad bool) { func (this *box) handleKeyDown (key input.Key, numberPad bool) {
for _, listener := range this.on.keyDown.Listeners() { for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad) listener(key, numberPad)
@@ -374,6 +379,10 @@ func (this *box) setParent (parent parent) {
this.parent = parent this.parent = parent
} }
func (this *box) getParent () parent {
return this.parent
}
func (this *box) flushActionQueue () { func (this *box) flushActionQueue () {
if this.parent == nil || this.parent.window() == nil { return } if this.parent == nil || this.parent.window() == nil { return }
@@ -412,7 +421,7 @@ func (this *box) canBeFocused () bool {
return this.focusable return this.focusable
} }
func (this *box) boxUnder (point image.Point) anyBox { func (this *box) boxUnder (point image.Point, category eventCategory) anyBox {
if point.In(this.bounds) { if point.In(this.bounds) {
return this.outer return this.outer
} else { } else {

View File

@@ -97,7 +97,7 @@ func (this *pen) Path (points ...image.Point) {
if this.fill.A > 0 { if this.fill.A > 0 {
this.fillPolygon(this.fill, points...) this.fillPolygon(this.fill, points...)
} }
} else if this.closed { } else if this.closed && len(points) > 2 {
if this.stroke.A > 0 { if this.stroke.A > 0 {
this.strokePolygon(this.stroke, points...) this.strokePolygon(this.stroke, points...)
} }

View File

@@ -3,6 +3,7 @@ package xcanvas
import "image" import "image"
import "github.com/jezek/xgbutil/xgraphics" import "github.com/jezek/xgbutil/xgraphics"
// TODO: clip the line to the bounds
func (this *pen) line ( func (this *pen) line (
c xgraphics.BGRA, c xgraphics.BGRA,
min image.Point, min image.Point,

View File

@@ -1,6 +1,8 @@
package xcanvas package xcanvas
import "image" import "image"
import "image/color"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
// Texture is a read-only image texture that can be quickly written to a canvas. // Texture is a read-only image texture that can be quickly written to a canvas.
@@ -40,16 +42,43 @@ func NewTextureFrom (source image.Image) *Texture {
return texture return texture
} }
func (this *Texture) BGRAAt (x, y int) xgraphics.BGRA {
if !(image.Point{ x, y }.In(this.rect)) {
return xgraphics.BGRA { }
}
index := this.PixOffset(x, y)
return xgraphics.BGRA {
B: this.pix[index ],
G: this.pix[index + 1],
R: this.pix[index + 2],
A: this.pix[index + 3],
}
}
func (this *Texture) At (x, y int) color.Color {
return this.BGRAAt(x, y)
}
// Bounds returns the bounding rectangle of this texture. // Bounds returns the bounding rectangle of this texture.
func (this *Texture) Bounds () image.Rectangle { func (this *Texture) Bounds () image.Rectangle {
return this.rect return this.rect
} }
func (this *Texture) ColorModel () color.Model {
return xgraphics.BGRAModel
}
// Opaque reports whether or not the texture is fully opaque. // Opaque reports whether or not the texture is fully opaque.
func (this *Texture) Opaque () bool { func (this *Texture) Opaque () bool {
return !this.transparent return !this.transparent
} }
func (this *Texture) PixOffset (x, y int) int {
x = wrap(x, this.rect.Min.X, this.rect.Max.X)
y = wrap(y, this.rect.Min.Y, this.rect.Max.Y)
return x * 4 + y * this.stride
}
// Close frees the texture from memory. // Close frees the texture from memory.
func (this *Texture) Close () error { func (this *Texture) Close () error {
// i lied we dont actually need to close this, but we will once this // i lied we dont actually need to close this, but we will once this
@@ -64,14 +93,11 @@ func (this *Texture) Clip (bounds image.Rectangle) canvas.Texture {
return &clipped return &clipped
} }
func (this *Texture) PixOffset (x, y int) int {
x = wrap(x, this.rect.Min.X, this.rect.Max.X)
y = wrap(y, this.rect.Min.Y, this.rect.Max.Y)
return x * 4 + y * this.stride
}
// AssertTexture checks if a given canvas.Texture is a texture from this package. // AssertTexture checks if a given canvas.Texture is a texture from this package.
func AssertTexture (unknown canvas.Texture) *Texture { func AssertTexture (unknown canvas.Texture) *Texture {
if unknown == nil {
return nil
}
if tx, ok := unknown.(*Texture); ok { if tx, ok := unknown.(*Texture); ok {
return tx return tx
} else { } else {

View File

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

View File

@@ -1,6 +1,7 @@
package x package x
import "image" import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
@@ -12,11 +13,11 @@ type containerBox struct {
hAlign, vAlign tomo.Align hAlign, vAlign tomo.Align
contentBounds image.Rectangle contentBounds image.Rectangle
scroll image.Point scroll image.Point
capture [4]bool
gap image.Point gap image.Point
children []tomo.Box children []tomo.Box
layout tomo.Layout layout tomo.Layout
propagateEvents bool
on struct { on struct {
contentBoundsChange event.FuncBroadcaster contentBoundsChange event.FuncBroadcaster
@@ -24,11 +25,21 @@ type containerBox struct {
} }
func (backend *Backend) NewContainerBox() tomo.ContainerBox { func (backend *Backend) NewContainerBox() tomo.ContainerBox {
this := &containerBox { propagateEvents: true } this := &containerBox { }
this.box = backend.newBox(this) this.box = backend.newBox(this)
return this return this
} }
func (this *containerBox) SetColor (c color.Color) {
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) { func (this *containerBox) SetOverflow (horizontal, vertical bool) {
if this.hOverflow == horizontal && this.vOverflow == vertical { return } if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal this.hOverflow = horizontal
@@ -57,8 +68,20 @@ func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback) return this.on.contentBoundsChange.Connect(callback)
} }
func (this *containerBox) SetPropagateEvents (propagate bool) { func (this *containerBox) CaptureDND (capture bool) {
this.propagateEvents = propagate this.capture[eventCategoryDND] = capture
}
func (this *containerBox) CaptureMouse (capture bool) {
this.capture[eventCategoryMouse] = capture
}
func (this *containerBox) CaptureScroll (capture bool) {
this.capture[eventCategoryScroll] = capture
}
func (this *containerBox) CaptureKeyboard (capture bool) {
this.capture[eventCategoryKeyboard] = capture
} }
func (this *containerBox) SetGap (gap image.Point) { func (this *containerBox) SetGap (gap image.Point) {
@@ -96,10 +119,14 @@ func (this *containerBox) Insert (child, before tomo.Object) {
beforeBox := assertAnyBox(before.GetBox()) beforeBox := assertAnyBox(before.GetBox())
index := indexOf(this.children, tomo.Box(beforeBox)) index := indexOf(this.children, tomo.Box(beforeBox))
if index < 0 { return }
box.setParent(this) if index < 0 {
this.children = append(this.children, tomo.Box(box))
} else {
this.children = insert(this.children, index, tomo.Box(box)) this.children = insert(this.children, index, tomo.Box(box))
}
box.setParent(this)
this.invalidateLayout() this.invalidateLayout()
this.invalidateMinimum() this.invalidateMinimum()
} }
@@ -162,6 +189,17 @@ func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
pen.Rectangle(this.innerClippingBounds) pen.Rectangle(this.innerClippingBounds)
} }
func (this *containerBox) invalidateTransparentChildren () {
window := this.window()
if window == nil { return }
for _, box := range this.children {
box := assertAnyBox(box)
if box.transparent() {
window.invalidateDraw(box)
}
}
}
func (this *containerBox) flushActionQueue () { func (this *containerBox) flushActionQueue () {
for _, box := range this.children { for _, box := range this.children {
box.(anyBox).flushActionQueue() box.(anyBox).flushActionQueue()
@@ -229,15 +267,15 @@ func (this *containerBox) recursiveRedo () {
} }
} }
func (this *containerBox) boxUnder (point image.Point) anyBox { func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
if this.propagateEvents { if !this.capture[category] {
for _, box := range this.children { for _, box := range this.children {
candidate := box.(anyBox).boxUnder(point) candidate := box.(anyBox).boxUnder(point, category)
if candidate != nil { return candidate } if candidate != nil { return candidate }
} }
} }
return this.box.boxUnder(point) return this.box.boxUnder(point, category)
} }
func (this *containerBox) propagate (callback func (anyBox) bool) bool { func (this *containerBox) propagate (callback func (anyBox) bool) bool {
@@ -259,3 +297,7 @@ func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
return true 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 "github.com/jezek/xgbutil/xevent"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
type scrollSum struct {
x, y int
}
// TODO: this needs to be configurable, we need a config api
const scrollDistance = 16
func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
if xgbkb.StateToModifiers(state).Shift {
switch button {
case 4: sum.x -= scrollDistance
case 5: sum.x += scrollDistance
case 6: sum.y -= scrollDistance
case 7: sum.y += scrollDistance
}
} else {
switch button {
case 4: sum.y -= scrollDistance
case 5: sum.y += scrollDistance
case 6: sum.x -= scrollDistance
case 7: sum.x += scrollDistance
}
}
}
var buttonCodeTable = map[xproto.Keysym] input.Key { var buttonCodeTable = map[xproto.Keysym] input.Key {
0xFFFFFF: input.KeyNone, 0xFFFFFF: input.KeyNone,
@@ -208,7 +233,7 @@ func (window *window) handleKeyPress (
} else if key == input.KeyEscape && window.shy { } else if key == input.KeyEscape && window.shy {
window.Close() window.Close()
} else if window.focused != nil { } else if window.focused != nil {
window.focused.handleKeyDown(key, numberPad) window.keyboardTarget().handleKeyDown(key, numberPad)
} }
} }
@@ -241,7 +266,7 @@ func (window *window) handleKeyRelease (
window.updateModifiers(keyEvent.State) window.updateModifiers(keyEvent.State)
if window.focused != nil { if window.focused != nil {
window.focused.handleKeyUp(key, numberPad) window.keyboardTarget().handleKeyUp(key, numberPad)
} }
} }
@@ -261,20 +286,15 @@ func (window *window) handleButtonPress (
if !insideWindow && window.shy && !scrolling { if !insideWindow && window.shy && !scrolling {
window.Close() window.Close()
} else if scrolling { } else if scrolling {
// TODO underneath := window.boxUnder(point, eventCategoryScroll)
// underneath := window.scrollTargetChildAt(point) if underneath != nil {
// if underneath != nil { sum := scrollSum { }
// if child, ok := underneath.element.(ability.ScrollTarget); ok { sum.add(buttonEvent.Detail, window, buttonEvent.State)
// sum := scrollSum { } window.compressScrollSum(buttonEvent, &sum)
// sum.add(buttonEvent.Detail, window, buttonEvent.State) underneath.handleScroll(float64(sum.x), float64(sum.y))
// window.compressScrollSum(buttonEvent, &sum) }
// child.HandleScroll (
// point, float64(sum.x), float64(sum.y),
// modifiers)
// }
// }
} else { } else {
underneath := window.boxUnder(point) underneath := window.boxUnder(point, eventCategoryMouse)
window.drags[buttonEvent.Detail] = underneath window.drags[buttonEvent.Detail] = underneath
if underneath != nil { if underneath != nil {
underneath.handleMouseDown(input.Button(buttonEvent.Detail)) underneath.handleMouseDown(input.Button(buttonEvent.Detail))
@@ -318,12 +338,14 @@ func (window *window) handleMotionNotify (
handled = true handled = true
} }
underneath := window.boxUnder(image.Pt(x, y)) underneath := window.boxUnder(image.Pt(x, y), eventCategoryMouse)
if underneath != nil {
window.hover(underneath) window.hover(underneath)
if !handled { if !handled {
underneath.handleMouseMove() underneath.handleMouseMove()
} }
}
} }
func (window *window) compressExpose ( func (window *window) compressExpose (
@@ -391,6 +413,33 @@ func (window *window) compressConfigureNotify (
return return
} }
func (window *window) compressScrollSum (
firstEvent xproto.ButtonPressEvent,
sum *scrollSum,
) {
window.backend.x.Sync()
xevent.Read(window.backend.x, false)
for index, untypedEvent := range xevent.Peek(window.backend.x) {
if untypedEvent.Err != nil { continue }
typedEvent, ok := untypedEvent.Event.(xproto.ButtonPressEvent)
if !ok { continue }
if firstEvent.Event == typedEvent.Event &&
typedEvent.Detail >= 4 &&
typedEvent.Detail <= 7 {
sum.add(typedEvent.Detail, window, typedEvent.State)
defer func (index int) {
xevent.DequeueAt(window.backend.x, index)
} (index)
}
}
return
}
func (window *window) compressMotionNotify ( func (window *window) compressMotionNotify (
firstEvent xproto.MotionNotifyEvent, firstEvent xproto.MotionNotifyEvent,
) ( ) (

4
go.mod
View File

@@ -3,8 +3,8 @@ module git.tebibyte.media/tomo/x
go 1.20 go 1.20
require ( require (
git.tebibyte.media/tomo/tomo v0.27.0 git.tebibyte.media/tomo/tomo v0.31.0
git.tebibyte.media/tomo/typeset v0.5.2 git.tebibyte.media/tomo/typeset v0.7.1
git.tebibyte.media/tomo/xgbkb v1.0.1 git.tebibyte.media/tomo/xgbkb v1.0.1
github.com/jezek/xgb v1.1.0 github.com/jezek/xgb v1.1.0
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0

8
go.sum
View File

@@ -1,8 +1,8 @@
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q= git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/tomo v0.27.0 h1:gCwxQe0qm1hZLfHkMI3OccNMC/lB1cfs4BbaMz/bXug= git.tebibyte.media/tomo/tomo v0.31.0 h1:LHPpj3AWycochnC8F441aaRNS6Tq6w6WnBrp/LGjyhM=
git.tebibyte.media/tomo/tomo v0.27.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps= git.tebibyte.media/tomo/tomo v0.31.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/typeset v0.5.2 h1:qHxN62/VDnrAouOuzxLmLleQNwAebshrfVYvtoOnAG4= git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
git.tebibyte.media/tomo/typeset v0.5.2/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE= git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE=
git.tebibyte.media/tomo/xgbkb v1.0.1/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw= git.tebibyte.media/tomo/xgbkb v1.0.1/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw=
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA=

View File

@@ -37,6 +37,7 @@ type parent interface {
canvas () canvas.Canvas canvas () canvas.Canvas
notifyMinimumSizeChange (anyBox) notifyMinimumSizeChange (anyBox)
drawBackgroundPart (canvas.Canvas) drawBackgroundPart (canvas.Canvas)
captures (eventCategory) bool
} }
type anyBox interface { type anyBox interface {
@@ -48,10 +49,12 @@ type anyBox interface {
doMinimumSize () doMinimumSize ()
contentMinimum () image.Point contentMinimum () image.Point
setParent (parent) setParent (parent)
getParent () parent
flushActionQueue () flushActionQueue ()
recursiveRedo () recursiveRedo ()
canBeFocused () bool canBeFocused () bool
boxUnder (image.Point) anyBox boxUnder (image.Point, eventCategory) anyBox
transparent () bool
propagate (func (anyBox) bool) bool propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool propagateAlt (func (anyBox) bool) bool
@@ -66,7 +69,7 @@ type anyBox interface {
handleMouseMove () handleMouseMove ()
handleMouseDown (input.Button) handleMouseDown (input.Button)
handleMouseUp (input.Button) handleMouseUp (input.Button)
// handleScroll (float64, float64) handleScroll (float64, float64)
handleKeyDown (input.Key, bool) handleKeyDown (input.Key, bool)
handleKeyUp (input.Key, bool) handleKeyUp (input.Key, bool)
} }
@@ -152,9 +155,36 @@ func (window *window) anyFocused () bool {
return window.focused != nil return window.focused != nil
} }
func (this *window) boxUnder (point image.Point) anyBox { type eventCategory int; const (
eventCategoryDND eventCategory = iota
eventCategoryMouse
eventCategoryScroll
eventCategoryKeyboard
)
func (this *window) boxUnder (point image.Point, category eventCategory) anyBox {
if this.root == nil { return nil } if this.root == nil { return nil }
return this.root.boxUnder(point) return this.root.boxUnder(point, category)
}
func (this *window) captures (eventCategory) bool {
return false
}
func (this *window) keyboardTarget () anyBox {
focused := this.window().focused
if focused == nil { return nil }
parent := focused.getParent()
for {
parentBox, ok := parent.(anyBox)
if !ok { break }
if parent.captures(eventCategoryKeyboard) {
return parentBox
}
parent = parentBox.getParent()
}
return focused
} }
func (this *window) focusNext () { func (this *window) focusNext () {

View File

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

View File

@@ -73,6 +73,20 @@ func (backend *Backend) NewWindow (
return output, err return output, err
} }
func (backend *Backend) NewPlainWindow (
bounds image.Rectangle,
) (
output tomo.MainWindow,
err error,
) {
backend.assert()
window, err := backend.newWindow(bounds, false)
window.setType("dock")
output = mainWindow { window: window }
return output, err
}
func (backend *Backend) newWindow ( func (backend *Backend) newWindow (
bounds image.Rectangle, bounds image.Rectangle,
override bool, override bool,

View File

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