Update code for internal system

This commit is contained in:
Sasha Koshka 2024-07-25 13:01:15 -04:00
parent 9b61600f31
commit 196afbc2f3
12 changed files with 785 additions and 495 deletions

View File

@ -0,0 +1,38 @@
package system
import "git.tebibyte.media/tomo/tomo"
type attrHierarchy [T tomo.Attr] struct {
style T
user T
userExists bool
}
func (this *attrHierarchy[T]) SetStyle (style T) (different bool) {
styleEquals := this.style.Equals(style)
this.style = style
return !styleEquals && !this.userExists
}
func (this *attrHierarchy[T]) SetUser (user T) (different bool) {
userEquals := this.user.Equals(user)
this.user = user
this.userExists = true
return !userEquals
}
func (this *attrHierarchy[T]) Set (attr T, user bool) (different bool) {
if user {
return this.SetUser(attr)
} else {
return this.SetStyle(attr)
}
}
func (this *attrHierarchy[T]) Value () T {
if this.userExists {
return this.user
} else {
return this.style
}
}

View File

@ -9,66 +9,64 @@ import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util" import "git.tebibyte.media/tomo/backend/internal/util"
type textureMode int; const (
textureModeTile textureMode = iota
textureModeCenter
)
type box struct { type box struct {
system *System system *System
parent parent parent parent
outer anyBox outer anyBox
role tomo.Role tags util.Set[string]
styleCookie event.Cookie role tomo.Role
lastStyleNonce int lastStyleNonce int
lastIconsNonce int lastIconsNonce int
styleApplicator *styleApplicator
minSize util.Memo[image.Point]
bounds image.Rectangle bounds image.Rectangle
minSize image.Point
userMinSize image.Point
innerClippingBounds image.Rectangle innerClippingBounds image.Rectangle
minSizeQueued bool focusQueued *bool
focusQueued *bool
padding tomo.Inset attrColor attrHierarchy[tomo.AttrColor]
border []tomo.Border attrTexture attrHierarchy[tomo.AttrTexture]
color color.Color attrTextureMode attrHierarchy[tomo.AttrTextureMode]
texture canvas.Texture attrBorder attrHierarchy[tomo.AttrBorder]
textureMode textureMode attrMinimumSize attrHierarchy[tomo.AttrMinimumSize]
attrPadding attrHierarchy[tomo.AttrPadding]
dndData data.Data dndData data.Data
dndAccept []data.Mime dndAccept []data.Mime
focused bool
focusable bool focusable bool
hovered bool
focused bool
pressed bool
canvas util.Memo[canvas.Canvas] canvas util.Memo[canvas.Canvas]
drawer canvas.Drawer drawer canvas.Drawer
on struct { on struct {
focusEnter event.FuncBroadcaster focusEnter event.FuncBroadcaster
focusLeave event.FuncBroadcaster focusLeave event.FuncBroadcaster
dndEnter event.FuncBroadcaster dndEnter event.FuncBroadcaster
dndLeave event.FuncBroadcaster dndLeave event.FuncBroadcaster
dndDrop event.Broadcaster[func (data.Data)] dndDrop event.Broadcaster[func (data.Data)]
mouseEnter event.FuncBroadcaster mouseEnter event.FuncBroadcaster
mouseLeave event.FuncBroadcaster mouseLeave event.FuncBroadcaster
mouseMove event.FuncBroadcaster mouseMove event.Broadcaster[func () bool]
mouseDown event.Broadcaster[func (input.Button)] buttonDown event.Broadcaster[func (input.Button) bool]
mouseUp event.Broadcaster[func (input.Button)] buttonUp event.Broadcaster[func (input.Button) bool]
scroll event.Broadcaster[func (float64, float64)] scroll event.Broadcaster[func (float64, float64) bool]
keyDown event.Broadcaster[func (input.Key, bool)] keyDown event.Broadcaster[func (input.Key, bool) bool]
keyUp event.Broadcaster[func (input.Key, bool)] keyUp event.Broadcaster[func (input.Key, bool) bool]
styleChange event.FuncBroadcaster styleChange event.FuncBroadcaster
iconsChange event.FuncBroadcaster iconSetChange event.FuncBroadcaster
} }
} }
func (this *System) newBox (outer anyBox) *box { func (this *System) newBox (outer anyBox) *box {
box := &box { box := &box {
system: this, system: this,
color: color.Transparent,
outer: outer, outer: outer,
drawer: outer, drawer: outer,
} }
@ -82,15 +80,16 @@ func (this *System) newBox (outer anyBox) *box {
box.drawer = box box.drawer = box
box.outer = box box.outer = box
} }
box.invalidateMinimum() box.minSize = util.NewMemo(box.calculateMinimumSize)
return box return box
} }
func (this *System) NewBox () tomo.Box { func (this *System) NewBox () tomo.Box {
return this.newBox(nil) return this.newBox(nil)
} }
// ----- public methods ----------------------------------------------------- //
func (this *box) GetBox () tomo.Box { func (this *box) GetBox () tomo.Box {
return this.outer return this.outer
} }
@ -106,102 +105,13 @@ func (this *box) Bounds () image.Rectangle {
} }
func (this *box) InnerBounds () image.Rectangle { func (this *box) InnerBounds () image.Rectangle {
return this.padding.Apply(this.innerClippingBounds) return tomo.Inset(this.attrPadding.Value()).Apply(this.innerClippingBounds)
}
func (this *box) MinimumSize () image.Point {
return this.minSize
} }
func (this *box) Role () tomo.Role { func (this *box) Role () tomo.Role {
return this.role return this.role
} }
func (this *box) borderSum () tomo.Inset {
sum := tomo.Inset { }
for _, border := range this.border {
sum[0] += border.Width[0]
sum[1] += border.Width[1]
sum[2] += border.Width[2]
sum[3] += border.Width[3]
}
return sum
}
func (this *box) borderAndPaddingSum () tomo.Inset {
sum := this.borderSum()
sum[0] += this.padding[0]
sum[1] += this.padding[1]
sum[2] += this.padding[2]
sum[3] += this.padding[3]
return sum
}
func (this *box) SetBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
this.invalidateLayout()
}
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) SetTextureTile (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeTile { return }
this.textureMode = textureModeTile
this.texture = texture
this.invalidateDraw()
}
func (this *box) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture && this.textureMode == textureModeCenter { return }
this.texture = 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.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.invalidateMinimum()
}
func (this *box) SetRole (role tomo.Role) { func (this *box) SetRole (role tomo.Role) {
if this.role == role { return } if this.role == role { return }
this.role = role this.role = role
@ -209,6 +119,27 @@ func (this *box) SetRole (role tomo.Role) {
this.outer.recursiveReApply() this.outer.recursiveReApply()
} }
func (this *box) Tag (tag string) bool {
switch tag {
case "hovered": return this.hovered
case "focused": return this.focused
case "pressed": return this.pressed
default: return this.tags.Has(tag)
}
}
func (this *box) SetTag (tag string, on bool) {
if on {
this.tags.Add(tag)
} else {
delete(this.tags, tag)
}
}
func (this *box) SetAttr (attr tomo.Attr) {
this.outer.setAttr(attr, true)
}
func (this *box) SetDNDData (dat data.Data) { func (this *box) SetDNDData (dat data.Data) {
this.dndData = dat this.dndData = dat
} }
@ -233,6 +164,12 @@ func (this *box) SetFocused (focused bool) {
} }
} }
func (this *box) Focused () bool {
hierarchy := this.getHierarchy()
if hierarchy == nil { return false }
return hierarchy.isFocused(this.outer)
}
func (this *box) SetFocusable (focusable bool) { func (this *box) SetFocusable (focusable bool) {
if this.focusable == focusable { return } if this.focusable == focusable { return }
this.focusable = focusable this.focusable = focusable
@ -241,22 +178,83 @@ func (this *box) SetFocusable (focusable bool) {
} }
} }
func (this *box) Focused () bool { // ----- private methods ---------------------------------------------------- //
hierarchy := this.getHierarchy()
if hierarchy == nil { return false } func (this *box) setAttr (attr tomo.Attr, user bool) {
return hierarchy.isFocused(this.outer) switch attr := attr.(type) {
case tomo.AttrColor:
if this.attrColor.Set(attr, user) {
this.invalidateDraw()
}
case tomo.AttrTexture:
if this.attrTexture.Set(attr, user) {
this.invalidateDraw()
}
case tomo.AttrTextureMode:
if this.attrTextureMode.Set(attr, user) {
this.invalidateDraw()
}
case tomo.AttrBorder:
previousBorderSum := this.borderSum()
different := this.attrBorder.Set(attr, user)
// only invalidate the layout if the border is sized differently
if this.borderSum() != previousBorderSum {
this.invalidateLayout()
this.invalidateMinimum()
}
// if the border takes up the same amount of space, only invalidate the
// drawing if it looks different
if different {
this.invalidateDraw()
}
case tomo.AttrMinimumSize:
if this.attrMinimumSize.Set(attr, user) {
this.invalidateMinimum()
}
case tomo.AttrPadding:
if this.attrPadding.Set(attr, true) {
this.invalidateLayout()
this.invalidateMinimum()
}
}
} }
func (this *box) Modifiers () input.Modifiers { func (this *box) setBounds (bounds image.Rectangle) {
hierarchy := this.getHierarchy() if this.bounds == bounds { return }
if hierarchy == nil { return input.Modifiers { } } this.bounds = bounds
return hierarchy.getModifiers() this.invalidateLayout()
} }
func (this *box) MousePosition () image.Point { func (this *box) minimumSize () image.Point {
hierarchy := this.getHierarchy() return this.minSize.Value()
if hierarchy == nil { return image.Point { } } }
return hierarchy.getMousePosition()
func (this *box) borderSum () tomo.Inset {
sum := tomo.Inset { }
for _, border := range this.attrBorder.Value() {
sum[0] += border.Width[0]
sum[1] += border.Width[1]
sum[2] += border.Width[2]
sum[3] += border.Width[3]
}
return sum
}
func (this *box) borderAndPaddingSum () tomo.Inset {
sum := this.borderSum()
padding := this.attrPadding.Value()
sum[0] += padding[0]
sum[1] += padding[1]
sum[2] += padding[2]
sum[3] += padding[3]
return sum
} }
// ----- event handler setters ---------------------------------------------- // // ----- event handler setters ---------------------------------------------- //
@ -281,46 +279,62 @@ func (this *box) OnMouseEnter (callback func()) event.Cookie {
func (this *box) OnMouseLeave (callback func()) event.Cookie { func (this *box) OnMouseLeave (callback func()) event.Cookie {
return this.on.mouseLeave.Connect(callback) return this.on.mouseLeave.Connect(callback)
} }
func (this *box) OnMouseMove (callback func()) event.Cookie { func (this *box) OnMouseMove (callback func() bool) event.Cookie {
return this.on.mouseMove.Connect(callback) return this.on.mouseMove.Connect(callback)
} }
func (this *box) OnMouseDown (callback func(input.Button)) event.Cookie { func (this *box) OnButtonDown (callback func(input.Button) bool) event.Cookie {
return this.on.mouseDown.Connect(callback) return this.on.buttonDown.Connect(callback)
} }
func (this *box) OnMouseUp (callback func(input.Button)) event.Cookie { func (this *box) OnButtonUp (callback func(input.Button) bool) event.Cookie {
return this.on.mouseUp.Connect(callback) return this.on.buttonUp.Connect(callback)
} }
func (this *box) OnScroll (callback func(deltaX, deltaY float64)) event.Cookie { func (this *box) OnScroll (callback func(float64, float64) bool) event.Cookie {
return this.on.scroll.Connect(callback) return this.on.scroll.Connect(callback)
} }
func (this *box) OnKeyDown (callback func(key input.Key, numberPad bool)) event.Cookie { func (this *box) OnKeyDown (callback func(input.Key, bool) bool) event.Cookie {
return this.on.keyDown.Connect(callback) return this.on.keyDown.Connect(callback)
} }
func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Cookie { func (this *box) OnKeyUp (callback func(input.Key, bool) bool) event.Cookie {
return this.on.keyUp.Connect(callback) return this.on.keyUp.Connect(callback)
} }
func (this *box) OnStyleChange (callback func()) event.Cookie { func (this *box) OnStyleChange (callback func()) event.Cookie {
return this.on.styleChange.Connect(callback) return this.on.styleChange.Connect(callback)
} }
func (this *box) OnIconsChange (callback func()) event.Cookie { func (this *box) OnIconSetChange (callback func()) event.Cookie {
return this.on.iconsChange.Connect(callback) return this.on.iconSetChange.Connect(callback)
} }
func (this *box) handleFocusEnter () { func (this *box) handleFocusEnter () {
this.focused = true
this.invalidateStyle()
this.on.focusEnter.Broadcast() this.on.focusEnter.Broadcast()
} }
func (this *box) handleFocusLeave () { func (this *box) handleFocusLeave () {
this.focused = false
this.invalidateStyle()
this.on.focusLeave.Broadcast() this.on.focusLeave.Broadcast()
} }
func (this *box) handleMouseEnter () { func (this *box) handleMouseEnter () {
this.hovered = true
this.invalidateStyle()
this.on.mouseEnter.Broadcast() this.on.mouseEnter.Broadcast()
} }
func (this *box) handleMouseLeave () { func (this *box) handleMouseLeave () {
this.hovered = false
this.invalidateStyle()
this.on.mouseLeave.Broadcast() this.on.mouseLeave.Broadcast()
} }
func (this *box) handleMouseMove () { func (this *box) handleMouseMove () (caught bool) {
this.on.mouseMove.Broadcast() for _, listener := range this.on.mouseMove.Listeners() {
if listener() { caught = true }
}
return
} }
func (this *box) handleMouseDown (button input.Button) { func (this *box) handleMouseDown (button input.Button) (caught bool) {
if button == input.ButtonLeft {
this.pressed = true
this.invalidateStyle()
}
if this.focusable { if this.focusable {
this.SetFocused(true) this.SetFocused(true)
} else { } else {
@ -328,29 +342,39 @@ func (this *box) handleMouseDown (button input.Button) {
if hierarchy == nil { return } if hierarchy == nil { return }
hierarchy.focus(nil) hierarchy.focus(nil)
} }
for _, listener := range this.on.mouseDown.Listeners() { for _, listener := range this.on.buttonDown.Listeners() {
listener(button) if listener(button) { caught = true }
} }
return
} }
func (this *box) handleMouseUp (button input.Button) { func (this *box) handleMouseUp (button input.Button) (caught bool) {
for _, listener := range this.on.mouseUp.Listeners() { if button == input.ButtonLeft {
listener(button) this.pressed = false
this.invalidateStyle()
} }
for _, listener := range this.on.buttonUp.Listeners() {
if listener(button) { caught = true }
}
return
} }
func (this *box) handleScroll (x, y float64) { func (this *box) handleScroll (x, y float64) (caught bool) {
for _, listener := range this.on.scroll.Listeners() { for _, listener := range this.on.scroll.Listeners() {
listener(x, y) if listener(x, y) { caught = true }
} }
return
} }
func (this *box) handleKeyDown (key input.Key, numberPad bool) { func (this *box) handleKeyDown (key input.Key, numberPad bool) (caught bool) {
for _, listener := range this.on.keyDown.Listeners() { for _, listener := range this.on.keyDown.Listeners() {
listener(key, numberPad) if listener(key, numberPad) { caught = true }
} }
return
} }
func (this *box) handleKeyUp (key input.Key, numberPad bool) { func (this *box) handleKeyUp (key input.Key, numberPad bool) (caught bool) {
for _, listener := range this.on.keyUp.Listeners() { for _, listener := range this.on.keyUp.Listeners() {
listener(key, numberPad) if listener(key, numberPad) { caught = true }
} }
return
} }
// -------------------------------------------------------------------------- // // -------------------------------------------------------------------------- //
@ -359,19 +383,25 @@ func (this *box) Draw (can canvas.Canvas) {
pen := can.Pen() pen := can.Pen()
bounds := this.Bounds() bounds := this.Bounds()
// get values
textureMode := tomo.TextureMode(this.attrTextureMode.Value())
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
// background // background
pen.Fill(this.color)
if this.textureMode == textureModeTile {
pen.Texture(this.texture)
}
if this.transparent() && this.parent != nil { if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can) this.parent.drawBackgroundPart(can)
} }
pen.Fill(col)
if textureMode == tomo.TextureModeTile && texture != nil {
pen.Texture(texture)
}
pen.Rectangle(bounds) pen.Rectangle(bounds)
// centered texture // centered texture
if this.textureMode == textureModeCenter && this.texture != nil { if textureMode == tomo.TextureModeCenter && texture != nil {
textureBounds := this.texture.Bounds() textureBounds := texture.Bounds()
textureOrigin := textureOrigin :=
bounds.Min. bounds.Min.
Add(image.Pt ( Add(image.Pt (
@ -382,7 +412,7 @@ func (this *box) Draw (can canvas.Canvas) {
textureBounds.Dy() / 2)) textureBounds.Dy() / 2))
pen.Fill(color.Transparent) pen.Fill(color.Transparent)
pen.Texture(this.texture) pen.Texture(texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin)) pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
} }
} }
@ -394,6 +424,7 @@ func (this *box) drawBorders (can canvas.Canvas) {
rectangle := func (x0, y0, x1, y1 int, c color.Color) { rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1) area := image.Rect(x0, y0, x1, y1)
if area.Empty() { return }
if util.Transparent(c) && this.parent != nil { if util.Transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.SubCanvas(area)) this.parent.drawBackgroundPart(can.SubCanvas(area))
} }
@ -401,7 +432,7 @@ func (this *box) drawBorders (can canvas.Canvas) {
pen.Rectangle(area) pen.Rectangle(area)
} }
for _, border := range this.border { for _, border := range this.attrBorder.Value() {
rectangle ( rectangle (
bounds.Min.X, bounds.Min.X,
bounds.Min.Y, bounds.Min.Y,
@ -433,26 +464,29 @@ func (this *box) drawBorders (can canvas.Canvas) {
func (this *box) contentMinimum () image.Point { func (this *box) contentMinimum () image.Point {
var minimum image.Point var minimum image.Point
minimum.X += this.padding.Horizontal() padding := tomo.Inset(this.attrPadding.Value())
minimum.Y += this.padding.Vertical() minimum.X += padding.Horizontal()
minimum.Y += padding.Vertical()
borderSum := this.borderSum() borderSum := this.borderSum()
minimum.X += borderSum.Horizontal() minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical() minimum.Y += borderSum.Vertical()
return minimum return minimum
} }
func (this *box) doMinimumSize () { func (this *box) calculateMinimumSize () image.Point {
this.minSize = this.outer.contentMinimum() userMinSize := this.attrMinimumSize.Value()
if this.minSize.X < this.userMinSize.X { minSize := this.outer.contentMinimum()
this.minSize.X = this.userMinSize.X if minSize.X < userMinSize.X {
minSize.X = userMinSize.X
} }
if this.minSize.Y < this.userMinSize.Y { if minSize.Y < userMinSize.Y {
this.minSize.Y = this.userMinSize.Y minSize.Y = userMinSize.Y
} }
if this.parent != nil { if this.parent != nil {
this.parent.notifyMinimumSizeChange(this) this.parent.notifyMinimumSizeChange(this)
} }
return minSize
} }
// var drawcnt int // var drawcnt int
@ -477,6 +511,10 @@ func (this *box) doLayout () {
this.outer.recursiveLoseCanvas() this.outer.recursiveLoseCanvas()
} }
func (this *box) doStyle () {
this.styleApplicator.apply(this)
}
func (this *box) setParent (parent parent) { func (this *box) setParent (parent parent) {
if this.parent != parent && this.Focused() { if this.parent != parent && this.Focused() {
this.SetFocused(false) this.SetFocused(false)
@ -491,10 +529,6 @@ func (this *box) getParent () parent {
func (this *box) flushActionQueue () { func (this *box) flushActionQueue () {
if this.getHierarchy() == nil { return } if this.getHierarchy() == nil { return }
if this.minSizeQueued {
this.invalidateMinimum()
}
if this.focusQueued != nil { if this.focusQueued != nil {
this.SetFocused(*this.focusQueued) this.SetFocused(*this.focusQueued)
} }
@ -509,6 +543,12 @@ func (this *box) recursiveLoseCanvas () {
this.canvas.InvalidateTo(nil) this.canvas.InvalidateTo(nil)
} }
func (this *box) invalidateStyle () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.invalidateStyle(this.outer)
}
func (this *box) invalidateLayout () { func (this *box) invalidateLayout () {
hierarchy := this.getHierarchy() hierarchy := this.getHierarchy()
if hierarchy == nil { return } if hierarchy == nil { return }
@ -522,12 +562,7 @@ func (this *box) invalidateDraw () {
} }
func (this *box) invalidateMinimum () { func (this *box) invalidateMinimum () {
hierarchy := this.getHierarchy() this.minSize.Invalidate()
if hierarchy == nil {
this.minSizeQueued = true
} else {
hierarchy.invalidateMinimum(this.outer)
}
} }
func (this *box) recursiveReApply () { func (this *box) recursiveReApply () {
@ -538,17 +573,13 @@ func (this *box) recursiveReApply () {
// style // style
hierarchyStyleNonce := this.getStyleNonce() hierarchyStyleNonce := this.getStyleNonce()
if this.lastStyleNonce != hierarchyStyleNonce { if this.lastStyleNonce != hierarchyStyleNonce {
// remove old style // i should probably explain why we have a specific style
// applicator for every box, it's so style applicators can cache
// information about the boxes they're linked to (like all rules
// with a matching role).
this.lastStyleNonce = hierarchyStyleNonce this.lastStyleNonce = hierarchyStyleNonce
if this.styleCookie != nil { this.styleApplicator = this.getHierarchy().newStyleApplicator()
this.styleCookie.Close() this.invalidateStyle()
this.styleCookie = nil
}
// apply new one
if style := this.getStyle(); style != nil {
this.styleCookie = style.Apply(this.outer)
}
this.on.styleChange.Broadcast() this.on.styleChange.Broadcast()
} }
@ -556,7 +587,7 @@ func (this *box) recursiveReApply () {
hierarchyIconsNonce := this.getIconsNonce() hierarchyIconsNonce := this.getIconsNonce()
if this.lastIconsNonce != hierarchyIconsNonce { if this.lastIconsNonce != hierarchyIconsNonce {
this.lastIconsNonce = hierarchyIconsNonce this.lastIconsNonce = hierarchyIconsNonce
this.on.iconsChange.Broadcast() this.on.iconSetChange.Broadcast()
} }
} }
@ -564,7 +595,7 @@ func (this *box) canBeFocused () bool {
return this.focusable return this.focusable
} }
func (this *box) boxUnder (point image.Point, category eventCategory) anyBox { func (this *box) boxUnder (point image.Point) anyBox {
if point.In(this.bounds) { if point.In(this.bounds) {
return this.outer return this.outer
} else { } else {
@ -583,7 +614,7 @@ func (this *box) propagateAlt (callback func (anyBox) bool) bool {
func (this *box) transparent () bool { func (this *box) transparent () bool {
// TODO uncomment once we have // TODO uncomment once we have
// a way to detect texture transparency // a way to detect texture transparency
return util.Transparent(this.color) /*&& return util.Transparent(this.attrColor.Value()) /*&&
(this.texture == nil || !this.texture.Opaque())*/ (this.texture == nil || !this.texture.Opaque())*/
} }
@ -593,12 +624,6 @@ func (this *box) getWindow () tomo.Window {
return hierarchy.getWindow() return hierarchy.getWindow()
} }
func (this *box) getStyle () tomo.Style {
hierarchy := this.getHierarchy()
if hierarchy == nil { return nil }
return hierarchy.getStyle()
}
func (this *box) getHierarchy () *Hierarchy { func (this *box) getHierarchy () *Hierarchy {
if this.parent == nil { return nil } if this.parent == nil { return nil }
return this.parent.getHierarchy() return this.parent.getHierarchy()

View File

@ -0,0 +1,63 @@
package system
import "image"
type boxQuerier []anyBox
func (querier boxQuerier) Len () int {
return len(querier)
}
func (querier boxQuerier) MinimumSize (index int) image.Point {
if box, ok := querier.box(index); ok {
return box.minimumSize()
}
return image.Point { }
}
func (querier boxQuerier) RecommendedWidth (index int, height int) int {
if box, ok := querier.box(index); ok {
if box, ok := box.(anyContentBox); ok {
return box.recommendedWidth(height)
}
}
return 0
}
func (querier boxQuerier) RecommendedHeight (index int, width int) int {
if box, ok := querier.box(index); ok {
if box, ok := box.(anyContentBox); ok {
return box.recommendedHeight(width)
}
}
return 0
}
func (querier boxQuerier) box (index int) (anyBox, bool) {
if index < 0 || index >= len(querier) { return nil, false }
return querier[index], true
}
type boxArranger []anyBox
func (arranger boxArranger) Len () int {
return boxQuerier(arranger).Len()
}
func (arranger boxArranger) MinimumSize (index int) image.Point {
return boxQuerier(arranger).MinimumSize(index)
}
func (arranger boxArranger) RecommendedWidth (index int, height int) int {
return boxQuerier(arranger).RecommendedWidth(index, height)
}
func (arranger boxArranger) RecommendedHeight (index int, width int) int {
return boxQuerier(arranger).RecommendedHeight(index, width)
}
func (arranger boxArranger) SetBounds (index int, bounds image.Rectangle) {
if box, ok := boxQuerier(arranger).box(index); ok {
box.setBounds(bounds)
}
}

View File

@ -15,10 +15,6 @@ func (this *System) NewCanvasBox () tomo.CanvasBox {
return box return box
} }
func (this *canvasBox) Box () tomo.Box {
return this
}
func (this *canvasBox) SetDrawer (drawer canvas.Drawer) { func (this *canvasBox) SetDrawer (drawer canvas.Drawer) {
this.userDrawer = drawer this.userDrawer = drawer
this.invalidateDraw() this.invalidateDraw()
@ -32,7 +28,8 @@ func (this *canvasBox) Draw (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
this.box.Draw(can) this.box.Draw(can)
if this.userDrawer != nil { if this.userDrawer != nil {
padding := tomo.Inset(this.attrPadding.Value())
this.userDrawer.Draw ( this.userDrawer.Draw (
can.SubCanvas(this.padding.Apply(this.innerClippingBounds))) can.SubCanvas(padding.Apply(this.innerClippingBounds)))
} }
} }

View File

@ -10,14 +10,16 @@ import "git.tebibyte.media/tomo/backend/internal/util"
type containerBox struct { type containerBox struct {
*box *box
hOverflow, vOverflow bool contentBounds image.Rectangle
hAlign, vAlign tomo.Align scroll image.Point
contentBounds image.Rectangle mask bool
scroll image.Point
capture [4]bool
gap image.Point attrGap attrHierarchy[tomo.AttrGap]
children []tomo.Box attrAlign attrHierarchy[tomo.AttrAlign]
attrOverflow attrHierarchy[tomo.AttrOverflow]
attrLayout attrHierarchy[tomo.AttrLayout]
children []anyBox
layout tomo.Layout layout tomo.Layout
on struct { on struct {
@ -31,37 +33,7 @@ func (this *System) NewContainerBox () tomo.ContainerBox {
return box return box
} }
func (this *containerBox) SetColor (c color.Color) { // ----- public methods ----------------------------------------------------- //
if this.color == c { return }
this.box.SetColor(c)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTextureTile (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTextureTile(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetTextureCenter (texture canvas.Texture) {
if this.texture == texture { return }
this.box.SetTextureTile(texture)
this.invalidateTransparentChildren()
}
func (this *containerBox) SetOverflow (horizontal, vertical bool) {
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal
this.vOverflow = vertical
this.invalidateLayout()
}
func (this *containerBox) SetAlign (x, y tomo.Align) {
if this.hAlign == x && this.vAlign == y { return }
this.hAlign = x
this.vAlign = y
this.invalidateLayout()
}
func (this *containerBox) ContentBounds () image.Rectangle { func (this *containerBox) ContentBounds () image.Rectangle {
return this.contentBounds return this.contentBounds
@ -73,54 +45,13 @@ func (this *containerBox) ScrollTo (point image.Point) {
this.invalidateLayout() this.invalidateLayout()
} }
func (this *containerBox) RecommendedHeight (width int) int {
if this.layout == nil || this.vOverflow {
return this.MinimumSize().Y
} else {
return this.layout.RecommendedHeight(this.layoutHints(), this.children, width) +
this.borderAndPaddingSum().Vertical()
}
}
func (this *containerBox) RecommendedWidth (height int) int {
if this.layout == nil || this.hOverflow {
return this.MinimumSize().X
} else {
return this.layout.RecommendedWidth(this.layoutHints(), this.children, height) +
this.borderAndPaddingSum().Horizontal()
}
}
func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie { func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback) return this.on.contentBoundsChange.Connect(callback)
} }
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.invalidateMinimum()
}
func (this *containerBox) Add (child tomo.Object) { func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.GetBox()) box := assertAnyBox(child.GetBox())
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return } if util.IndexOf(this.children, box) > -1 { return }
box.setParent(this) box.setParent(this)
box.flushActionQueue() box.flushActionQueue()
@ -131,7 +62,7 @@ func (this *containerBox) Add (child tomo.Object) {
func (this *containerBox) Remove (child tomo.Object) { func (this *containerBox) Remove (child tomo.Object) {
box := assertAnyBox(child.GetBox()) box := assertAnyBox(child.GetBox())
index := util.IndexOf(this.children, tomo.Box(box)) index := util.IndexOf(this.children, box)
if index < 0 { return } if index < 0 { return }
box.setParent(nil) box.setParent(nil)
@ -142,15 +73,15 @@ func (this *containerBox) Remove (child tomo.Object) {
func (this *containerBox) Insert (child, before tomo.Object) { func (this *containerBox) Insert (child, before tomo.Object) {
box := assertAnyBox(child.GetBox()) box := assertAnyBox(child.GetBox())
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return } if util.IndexOf(this.children, box) > -1 { return }
beforeBox := assertAnyBox(before.GetBox()) beforeBox := assertAnyBox(before.GetBox())
index := util.IndexOf(this.children, tomo.Box(beforeBox)) index := util.IndexOf(this.children, beforeBox)
if index < 0 { if index < 0 {
this.children = append(this.children, tomo.Box(box)) this.children = append(this.children, box)
} else { } else {
this.children = util.Insert(this.children, index, tomo.Box(box)) this.children = util.Insert(this.children, index, box)
} }
box.setParent(this) box.setParent(this)
@ -167,7 +98,7 @@ func (this *containerBox) Clear () {
this.invalidateMinimum() this.invalidateMinimum()
} }
func (this *containerBox) Length () int { func (this *containerBox) Len () int {
return len(this.children) return len(this.children)
} }
@ -178,14 +109,19 @@ func (this *containerBox) At (index int) tomo.Object {
return this.children[index] return this.children[index]
} }
func (this *containerBox) SetLayout (layout tomo.Layout) { func (this *containerBox) SetInputMask (mask bool) {
this.layout = layout this.mask = mask
this.invalidateLayout()
this.invalidateMinimum()
} }
// ----- private methods ---------------------------------------------------- //
func (this *containerBox) Draw (can canvas.Canvas) { func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
// textureMode := tomo.TextureMode(this.attrTextureMode.Value())
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
rocks := make([]image.Rectangle, len(this.children)) rocks := make([]image.Rectangle, len(this.children))
for index, box := range this.children { for index, box := range this.children {
@ -198,17 +134,87 @@ func (this *containerBox) Draw (can canvas.Canvas) {
} }
if clipped == nil { continue } if clipped == nil { continue }
pen := clipped.Pen() pen := clipped.Pen()
pen.Fill(this.color) pen.Fill(col)
pen.Texture(this.texture) pen.Texture(texture)
pen.Rectangle(this.innerClippingBounds) pen.Rectangle(this.innerClippingBounds)
} }
} }
func (this *containerBox) setAttr (attr tomo.Attr, user bool) {
switch attr := attr.(type) {
case tomo.AttrColor:
if this.attrColor.Set(attr, user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrTexture:
if this.attrTexture.Set(attr, user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrTextureMode:
if this.attrTextureMode.Set(attr, user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrGap:
if this.attrGap.Set(attr, user) {
this.invalidateLayout()
this.invalidateMinimum()
}
case tomo.AttrAlign:
if this.attrAlign.Set(attr, user) {
this.invalidateLayout()
}
case tomo.AttrOverflow:
if this.attrOverflow.Set(attr, user) {
this.invalidateLayout()
}
case tomo.AttrLayout:
if this.attrLayout.Set(attr, user) {
this.invalidateLayout()
this.invalidateMinimum()
}
default: this.box.setAttr(attr, user)
}
}
func (this *containerBox) recommendedHeight (width int) int {
if this.layout == nil || this.attrOverflow.Value().Y {
return this.minSize.Value().Y
} else {
return this.layout.RecommendedHeight(this.layoutHints(), this.boxQuerier(), width) +
this.borderAndPaddingSum().Vertical()
}
}
func (this *containerBox) recommendedWidth (height int) int {
if this.layout == nil || this.attrOverflow.Value().X {
return this.minSize.Value().X
} else {
return this.layout.RecommendedWidth(this.layoutHints(), this.boxQuerier(), height) +
this.borderAndPaddingSum().Horizontal()
}
}
func (this *containerBox) drawBackgroundPart (can canvas.Canvas) { func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
pen := can.Pen() pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture) // textureMode := tomo.TextureMode(this.attrTextureMode.Value())
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
pen.Fill(col)
pen.Texture(texture)
if this.transparent() && this.parent != nil { if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can) this.parent.drawBackgroundPart(can)
@ -245,7 +251,7 @@ func (this *containerBox) getCanvas () canvas.Canvas {
func (this *containerBox) notifyMinimumSizeChange (child anyBox) { func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.invalidateMinimum() this.invalidateMinimum()
size := child.MinimumSize() size := child.minimumSize()
bounds := child.Bounds() bounds := child.Bounds()
if bounds.Dx() < size.X || bounds.Dy() < size.Y { if bounds.Dx() < size.X || bounds.Dy() < size.Y {
this.invalidateLayout() this.invalidateLayout()
@ -253,23 +259,27 @@ func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
} }
func (this *containerBox) layoutHints () tomo.LayoutHints { func (this *containerBox) layoutHints () tomo.LayoutHints {
overflow := this.attrOverflow.Value()
align := this.attrAlign.Value()
gap := image.Point(this.attrGap.Value())
return tomo.LayoutHints { return tomo.LayoutHints {
OverflowX: this.hOverflow, OverflowX: overflow.X,
OverflowY: this.vOverflow, OverflowY: overflow.Y,
AlignX: this.hAlign, AlignX: align.X,
AlignY: this.vAlign, AlignY: align.Y,
Gap: this.gap, Gap: gap,
} }
} }
func (this *containerBox) contentMinimum () image.Point { func (this *containerBox) contentMinimum () image.Point {
minimum := this.box.contentMinimum() overflow := this.attrOverflow.Value()
minimum := this.box.contentMinimum()
if this.layout != nil { if this.layout != nil {
layoutMinimum := this.layout.MinimumSize ( layoutMinimum := this.layout.MinimumSize (
this.layoutHints(), this.layoutHints(),
this.children) this.boxQuerier())
if this.hOverflow { layoutMinimum.X = 0 } if overflow.X { layoutMinimum.X = 0 }
if this.vOverflow { layoutMinimum.Y = 0 } if overflow.Y { layoutMinimum.Y = 0 }
minimum = minimum.Add(layoutMinimum) minimum = minimum.Add(layoutMinimum)
} }
return minimum return minimum
@ -285,18 +295,19 @@ func (this *containerBox) doLayout () {
if this.layout != nil { if this.layout != nil {
minimum = this.layout.MinimumSize ( minimum = this.layout.MinimumSize (
this.layoutHints(), this.layoutHints(),
this.children) this.boxQuerier())
} }
innerBounds := this.InnerBounds() innerBounds := this.InnerBounds()
overflow := this.attrOverflow.Value()
this.contentBounds = innerBounds.Sub(innerBounds.Min) this.contentBounds = innerBounds.Sub(innerBounds.Min)
if this.hOverflow { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X } if overflow.X { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X }
if this.vOverflow { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y } if overflow.Y { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y }
// arrange children // arrange children
if this.layout != nil { if this.layout != nil {
layoutHints := this.layoutHints() layoutHints := this.layoutHints()
layoutHints.Bounds = this.contentBounds layoutHints.Bounds = this.contentBounds
this.layout.Arrange(layoutHints, this.children) this.layout.Arrange(layoutHints, this.boxArranger())
} }
// build an accurate contentBounds by unioning the bounds of all child // build an accurate contentBounds by unioning the bounds of all child
@ -312,7 +323,7 @@ func (this *containerBox) doLayout () {
// offset children and contentBounds by scroll // offset children and contentBounds by scroll
for _, box := range this.children { for _, box := range this.children {
box.SetBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min)) assertAnyBox(box).setBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
} }
this.contentBounds = this.contentBounds.Add(this.scroll) this.contentBounds = this.contentBounds.Add(this.scroll)
@ -345,6 +356,14 @@ func (this *containerBox) constrainScroll () {
} }
} }
func (this *containerBox) boxQuerier () boxQuerier {
return boxQuerier(this.children)
}
func (this *containerBox) boxArranger () boxArranger {
return boxArranger(this.children)
}
func (this *containerBox) recursiveRedo () { func (this *containerBox) recursiveRedo () {
this.doLayout() this.doLayout()
this.doDraw() this.doDraw()
@ -367,12 +386,12 @@ func (this *containerBox) recursiveReApply () {
} }
} }
func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox { func (this *containerBox) boxUnder (point image.Point) anyBox {
if !point.In(this.bounds) { return nil } if !point.In(this.bounds) { return nil }
if !this.capture[category] { if !this.mask {
for _, box := range this.children { for _, box := range this.children {
candidate := box.(anyBox).boxUnder(point, category) candidate := box.(anyBox).boxUnder(point)
if candidate != nil { return candidate } if candidate != nil { return candidate }
} }
} }
@ -400,6 +419,6 @@ func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
return true return true
} }
func (this *containerBox) captures (category eventCategory) bool { func (this *containerBox) masks () bool {
return this.capture[category] return this.mask
} }

View File

@ -3,6 +3,10 @@ package system
import "image" import "image"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
// TODO: redo all of this because there are new event propogation rules
// TODO: once go v1.23 comes out, replace the explicit iterator calls here with
// range loops
// HandleFocusChange sets whether or not the window containing this Hierarchy // HandleFocusChange sets whether or not the window containing this Hierarchy
// has input focus. // has input focus.
func (this *Hierarchy) HandleFocusChange (focused bool) { func (this *Hierarchy) HandleFocusChange (focused bool) {
@ -20,14 +24,22 @@ func (this *Hierarchy) HandleModifiers (modifiers input.Modifiers) {
// event which triggers this comes with modifier key information, // event which triggers this comes with modifier key information,
// HandleModifiers must be called *before* HandleKeyDown. // HandleModifiers must be called *before* HandleKeyDown.
func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) { func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
caught := false
this.keyboardTargets(func (target anyBox) bool {
if target.handleKeyDown(key, numberPad) {
caught = true
return false
}
return true
})
if caught { return }
if key == input.KeyTab && this.modifiers.Alt { if key == input.KeyTab && this.modifiers.Alt {
if this.modifiers.Shift { if this.modifiers.Shift {
this.focusPrevious() this.focusPrevious()
} else { } else {
this.focusNext() this.focusNext()
} }
} else if target := this.keyboardTarget(); target != nil {
target.handleKeyDown(key, numberPad)
} }
} }
@ -35,63 +47,72 @@ func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) {
// which triggers this comes with modifier key information, HandleModifiers must // which triggers this comes with modifier key information, HandleModifiers must
// be called *before* HandleKeyUp. // be called *before* HandleKeyUp.
func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) { func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) {
if target := this.keyboardTarget(); target != nil { this.keyboardTargets(func (target anyBox) bool {
target.handleKeyUp(key, numberPad) if target.handleKeyUp(key, numberPad) {
} return false
}
return true
})
} }
// HandleMouseDown sends a mouse down event to the Box positioned underneath the // HandleMouseDown sends a mouse down event to the Boxes positioned underneath
// mouse cursor and marks it as being "dragged" by that mouse button. If the // the mouse cursor and marks them as being "dragged" by that mouse button,
// event which triggers this comes with mouse position information, // starting at the first Box to mask events and ending at the first box to
// HandleMouseMove must be called *before* HandleMouseDown. // catch the event. If the event which triggers this comes with mouse position
// information, HandleMouseMove must be called *before* HandleMouseDown.
func (this *Hierarchy) HandleMouseDown (button input.Button) { func (this *Hierarchy) HandleMouseDown (button input.Button) {
underneath := this.boxUnder(this.mousePosition, eventCategoryMouse) boxes := []anyBox { }
this.drags[button] = underneath this.boxesUnder(this.mousePosition)(func (box anyBox) bool {
if underneath != nil { boxes = append(boxes, box)
underneath.handleMouseDown(button) return !box.handleMouseDown(button)
} })
this.drags[button] = boxes
} }
// HandleMouseUp sends a mouse up event to the Box currently being "dragged" by // HandleMouseUp sends a mouse up event to the Boxes currently being "dragged"
// the specified mouse button, and marks it as being "not dragged" by that mouse // by the specified mouse button, and marks them as being "not dragged" by that
// button. If the event which triggers this comes with mouse position // mouse button. If the event which triggers this comes with mouse position
// information, HandleMouseMove must be caleld *before* HandleMouseUp // information, HandleMouseMove must be caleld *before* HandleMouseUp
func (this *Hierarchy) HandleMouseUp (button input.Button) { func (this *Hierarchy) HandleMouseUp (button input.Button) {
dragging := this.drags[button] for _, box := range this.drags[button] {
this.drags[button] = nil box.handleMouseUp(button)
if dragging != nil {
dragging.handleMouseUp(button)
} }
this.drags[button] = nil
} }
// HandleMouseMove sends a mouse move event to any Boxes currently being // HandleMouseMove sends a mouse move event to any Boxes currently being
// "dragged" by a mouse button. If none are, it sends the event to the Box which // "dragged" by a mouse button. If none are, it sends the event to the Boxes
// is underneath the mouse pointer. // which are underneath the mouse pointer, starting at the first Box to mask
// events and ending at the first box to catch the event.
func (this *Hierarchy) HandleMouseMove (position image.Point) { func (this *Hierarchy) HandleMouseMove (position image.Point) {
if this.mousePosition == position { return } if this.mousePosition == position { return }
this.mousePosition = position this.mousePosition = position
handled := false dragged := false
for _, child := range this.drags { for _, dragSet := range this.drags {
if child == nil { continue } for _, box := range dragSet {
child.handleMouseMove() if box.handleMouseMove() { break }
handled = true
}
underneath := this.boxUnder(position, eventCategoryMouse)
if underneath != nil {
this.hover(underneath)
if !handled {
underneath.handleMouseMove()
} }
dragged = true
}
if dragged { return }
// TODO we can hover over multiple boxes at once. however, any way of
// detecting this involves several slice allocations every time we
// process a MouseMove event. perhaps we just ought to suck it up and do
// it.
box := this.boxUnder(position)
if box != nil {
this.hover(box)
box.handleMouseMove()
} }
} }
// HandleScroll sends a scroll event to the Box currently underneath the mouse // HandleScroll sends a scroll event to the Box currently underneath the mouse
// cursor. // cursor. If the event which triggers this comes with mouse position
// information, HandleMouseMove must be called *before* HandleScroll.
func (this *Hierarchy) HandleScroll (x, y float64) { func (this *Hierarchy) HandleScroll (x, y float64) {
underneath := this.boxUnder(this.mousePosition, eventCategoryScroll) this.boxesUnder(this.mousePosition)(func (box anyBox) bool {
if underneath != nil { return !box.handleScroll(x, y)
underneath.handleScroll(x, y) })
}
} }

View File

@ -21,10 +21,10 @@ type Hierarchy struct {
modifiers input.Modifiers modifiers input.Modifiers
mousePosition image.Point mousePosition image.Point
drags [10]anyBox drags [10][]anyBox
minimumSize image.Point minimumSize image.Point
needMinimum util.Set[anyBox] needStyle util.Set[anyBox]
needLayout util.Set[anyBox] needLayout util.Set[anyBox]
needDraw util.Set[anyBox] needDraw util.Set[anyBox]
needRedo bool needRedo bool
@ -52,7 +52,7 @@ func (this *System) NewHierarchy (link WindowLink) *Hierarchy {
hierarchy := &Hierarchy { hierarchy := &Hierarchy {
system: this, system: this,
link: link, link: link,
needMinimum: make(util.Set[anyBox]), needStyle: make(util.Set[anyBox]),
needLayout: make(util.Set[anyBox]), needLayout: make(util.Set[anyBox]),
needDraw: make(util.Set[anyBox]), needDraw: make(util.Set[anyBox]),
} }
@ -95,6 +95,16 @@ func (this *Hierarchy) MinimumSize () image.Point {
return this.minimumSize return this.minimumSize
} }
// Modifiers returns the current modifier keys being held.
func (this *Hierarchy) Modifiers () input.Modifiers {
return this.modifiers
}
// MousePosition returns the current mouse position.
func (this *Hierarchy) MousePosition () image.Point {
return this.mousePosition
}
// AfterEvent should be called at the end of every event cycle. // AfterEvent should be called at the end of every event cycle.
func (this *Hierarchy) AfterEvent () { func (this *Hierarchy) AfterEvent () {
if this.canvas == nil { return } if this.canvas == nil { return }
@ -104,7 +114,7 @@ func (this *Hierarchy) AfterEvent () {
childBounds := this.canvas.Bounds() childBounds := this.canvas.Bounds()
childBounds = childBounds.Sub(childBounds.Min) childBounds = childBounds.Sub(childBounds.Min)
if this.root != nil { if this.root != nil {
this.root.SetBounds(childBounds) this.root.setBounds(childBounds)
} }
// full relayout/redraw // full relayout/redraw
@ -116,8 +126,8 @@ func (this *Hierarchy) AfterEvent () {
return return
} }
for len(this.needMinimum) > 0 { for len(this.needStyle) > 0 {
this.needMinimum.Pop().doMinimumSize() this.needStyle.Pop().doStyle()
} }
if !this.minimumClean { if !this.minimumClean {
this.doMinimumSize() this.doMinimumSize()
@ -146,7 +156,7 @@ func (this *Hierarchy) setStyle () {
if this.root != nil { this.root.recursiveReApply() } if this.root != nil { this.root.recursiveReApply() }
} }
func (this *Hierarchy) setIcons () { func (this *Hierarchy) setIconSet () {
if this.root != nil { this.root.recursiveReApply() } if this.root != nil { this.root.recursiveReApply() }
} }
@ -158,7 +168,7 @@ func (this *Hierarchy) getWindow () tomo.Window {
return this.link.GetWindow() return this.link.GetWindow()
} }
func (this *Hierarchy) getStyle () tomo.Style { func (this *Hierarchy) getStyle () *tomo.Style {
return this.system.style return this.system.style
} }
@ -186,8 +196,8 @@ func (this *Hierarchy) notifyMinimumSizeChange (anyBox) {
this.minimumClean = false this.minimumClean = false
} }
func (this *Hierarchy) invalidateMinimum (box anyBox) { func (this *Hierarchy) invalidateStyle (box anyBox) {
this.needMinimum.Add(box) this.needStyle.Add(box)
} }
func (this *Hierarchy) invalidateDraw (box anyBox) { func (this *Hierarchy) invalidateDraw (box anyBox) {
@ -235,35 +245,52 @@ func (this *Hierarchy) anyFocused () bool {
return this.focused != nil return this.focused != nil
} }
func (this *Hierarchy) boxUnder (point image.Point, category eventCategory) anyBox { func (this *Hierarchy) masks () bool {
if this.root == nil { return nil }
return this.root.boxUnder(point, category)
}
func (this *Hierarchy) captures (eventCategory) bool {
return false return false
} }
func (this *Hierarchy) keyboardTarget () anyBox { func (this *Hierarchy) boxUnder (point image.Point) anyBox {
if this.root == nil { return nil }
return this.root.boxUnder(point)
}
func (this *Hierarchy) parents (box anyBox) func (func (anyBox) bool) {
return func (yield func (anyBox) bool) {
for box != nil && yield(box) {
parent, ok := box.getParent().(anyBox)
if !ok { break }
box = parent
}
}
}
func (this *Hierarchy) boxesUnder (point image.Point) func (func (anyBox) bool) {
return this.parents(this.boxUnder(point))
}
func (this *Hierarchy) keyboardTargets (yield func (anyBox) bool) {
focused := this.focused focused := this.focused
if focused == nil { return nil } if focused == nil { return }
parent := focused.getParent() this.parents(this.considerMaskingParents(focused))(yield)
}
func (this *Hierarchy) considerMaskingParents (box anyBox) anyBox {
parent := box.getParent()
for { for {
parentBox, ok := parent.(anyBox) parentBox, ok := parent.(anyBox)
if !ok { break } if !ok { break }
if parent.captures(eventCategoryKeyboard) { if parent.masks() {
return parentBox return parentBox
} }
parent = parentBox.getParent() parent = parentBox.getParent()
} }
return box
return focused
} }
func (this *Hierarchy) focusNext () { func (this *Hierarchy) focusNext () {
found := !this.anyFocused() found := !this.anyFocused()
focused := false focused := false
this.propagateAlt (func (box anyBox) bool { this.propagateAlt(func (box anyBox) bool {
if found { if found {
// looking for the next box to select // looking for the next box to select
if box.canBeFocused() { if box.canBeFocused() {
@ -287,7 +314,7 @@ func (this *Hierarchy) focusNext () {
func (this *Hierarchy) focusPrevious () { func (this *Hierarchy) focusPrevious () {
var behind anyBox var behind anyBox
this.propagate (func (box anyBox) bool { this.propagate(func (box anyBox) bool {
if box == this.focused { if box == this.focused {
return false return false
} }
@ -319,8 +346,14 @@ func (this *Hierarchy) doMinimumSize () {
this.minimumSize = image.Point { } this.minimumSize = image.Point { }
if this.root != nil { if this.root != nil {
this.minimumSize = this.root.MinimumSize() this.minimumSize = this.root.minimumSize()
} }
this.link.NotifyMinimumSizeChange() this.link.NotifyMinimumSizeChange()
} }
func (this *Hierarchy) newStyleApplicator () *styleApplicator {
return &styleApplicator {
style: this.getStyle(),
}
}

View File

@ -5,14 +5,6 @@ import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
// eventCategory lists kinds of Tomo events.
type eventCategory int; const (
eventCategoryDND eventCategory = iota
eventCategoryMouse
eventCategoryScroll
eventCategoryKeyboard
)
// parent is any hierarchical type which contains other boxes. This can be a // parent is any hierarchical type which contains other boxes. This can be a
// Hierarchy, containerBox, etc. // Hierarchy, containerBox, etc.
type parent interface { type parent interface {
@ -27,9 +19,8 @@ type parent interface {
// given Canvas, filling the Canvas's entire bounds. The origin (0, 0) // given Canvas, filling the Canvas's entire bounds. The origin (0, 0)
// of the given Canvas is assumed to be the same as the parent's canvas. // of the given Canvas is assumed to be the same as the parent's canvas.
drawBackgroundPart (canvas.Canvas) drawBackgroundPart (canvas.Canvas)
// captures returns whether or not this parent captures the given event // catches returns whether or not this parent masks events.
// category. masks () bool
captures (eventCategory) bool
} }
// anyBox is any tomo.Box type that is implemented by this package. // anyBox is any tomo.Box type that is implemented by this package.
@ -44,10 +35,10 @@ type anyBox interface {
// doDraw re-paints the anyBox onto its currently held Canvas non-recursively // doDraw re-paints the anyBox onto its currently held Canvas non-recursively
// doLayout re-calculates the layout of the anyBox non-recursively // doLayout re-calculates the layout of the anyBox non-recursively
// doMinimumSize re-calculates the minimum size of the anyBox non-recursively // doStyle re-applies the box's style non-recursively
doDraw () doDraw ()
doLayout () doLayout ()
doMinimumSize () doStyle ()
// flushActionQueue performs any queued actions, like invalidating the // flushActionQueue performs any queued actions, like invalidating the
// minimum size or grabbing input focus. // minimum size or grabbing input focus.
@ -62,7 +53,9 @@ type anyBox interface {
// to check whether they have an outdated style or icon set, and if so, // to check whether they have an outdated style or icon set, and if so,
// update it and trigger the appropriate event broadcasters. // update it and trigger the appropriate event broadcasters.
recursiveReApply () recursiveReApply ()
// minimumSize returns the box's minimum size
minimumSize () image.Point
// contentMinimum returns the minimum dimensions of this box's content // contentMinimum returns the minimum dimensions of this box's content
contentMinimum () image.Point contentMinimum () image.Point
// canBeFocused returns whether or not this anyBox is capable of holding // canBeFocused returns whether or not this anyBox is capable of holding
@ -71,12 +64,18 @@ type anyBox interface {
// boxUnder returns the anyBox under the mouse pointer. It can be this // boxUnder returns the anyBox under the mouse pointer. It can be this
// anyBox, one of its children (if applicable). It must return nil if // anyBox, one of its children (if applicable). It must return nil if
// the mouse pointer is outside of this anyBox's bounds. // the mouse pointer is outside of this anyBox's bounds.
boxUnder (image.Point, eventCategory) anyBox boxUnder (image.Point) anyBox
// transparent returns whether or not this anyBox contains transparent // transparent returns whether or not this anyBox contains transparent
// pixels or not, and thus needs its parent's backround to be painted // pixels or not, and thus needs its parent's backround to be painted
// underneath it. // underneath it.
transparent () bool transparent () bool
// setBounds sets the box's bounds.
setBounds (image.Rectangle)
// setAttr sets an attribute at the user or style level depending
// on the value of user.
setAttr (attr tomo.Attr, user bool)
// propagate recursively calls a function on this anyBox, and all of its // propagate recursively calls a function on this anyBox, and all of its
// children (if applicable) The normal propagate behavior calls the // children (if applicable) The normal propagate behavior calls the
// callback on all children before calling it on this anyBox, and // callback on all children before calling it on this anyBox, and
@ -92,12 +91,21 @@ type anyBox interface {
// handleDndDrop (data.Data) // handleDndDrop (data.Data)
handleMouseEnter () handleMouseEnter ()
handleMouseLeave () handleMouseLeave ()
handleMouseMove () handleMouseMove () bool
handleMouseDown (input.Button) handleMouseDown (input.Button) bool
handleMouseUp (input.Button) handleMouseUp (input.Button) bool
handleScroll (float64, float64) handleScroll (float64, float64) bool
handleKeyDown (input.Key, bool) handleKeyDown (input.Key, bool) bool
handleKeyUp (input.Key, bool) handleKeyUp (input.Key, bool) bool
}
type anyContentBox interface {
anyBox
// recommendedWidth returns the recommended width for a given height.
recommendedWidth (int) int
// recommendedHeight returns the recommended height for a given height.
recommendedHeight (int) int
} }
func assertAnyBox (unknown tomo.Box) anyBox { func assertAnyBox (unknown tomo.Box) anyBox {

46
internal/system/style.go Normal file
View File

@ -0,0 +1,46 @@
package system
import "git.tebibyte.media/tomo/tomo"
type styleApplicator struct {
style *tomo.Style
role tomo.Role
rules []*tomo.Rule
}
func (this *styleApplicator) apply (box anyBox) {
if box.Role() != this.role {
// the role has changed, so re-cache the list of rules
this.rules = make([]*tomo.Rule, 4)
for _, rule := range this.style.Rules {
role := box.Role()
// blank fields match anything
if rule.Role.Package == "" { role.Package = "" }
if rule.Role.Object == "" { role.Object = "" }
if rule.Role == role {
this.rules = append(this.rules, &rule)
}
}
}
// compile list of attributes by searching through the cached ruleset
attrs := make(tomo.AttrSet)
for _, rule := range this.rules {
satisifed := true
for _, tag := range rule.Tags {
if !box.Tag(tag) {
satisifed = false
break
}
}
if satisifed {
attrs.MergeOver(rule.Set)
}
}
// apply that list of attributes
for _, attr := range attrs {
box.setAttr(attr, false)
}
}

View File

@ -11,7 +11,7 @@ import "git.tebibyte.media/tomo/backend/internal/util"
type System struct { type System struct {
link BackendLink link BackendLink
style tomo.Style style *tomo.Style
styleNonce int styleNonce int
iconsNonce int iconsNonce int
@ -45,7 +45,7 @@ func New (link BackendLink) *System {
// SetStyle sets the tomo.Style that is applied to objects, and notifies them // SetStyle sets the tomo.Style that is applied to objects, and notifies them
// that the style has changed. // that the style has changed.
func (this *System) SetStyle (style tomo.Style) { func (this *System) SetStyle (style *tomo.Style) {
this.style = style this.style = style
this.styleNonce ++ this.styleNonce ++
for hierarchy := range this.hierarchies { for hierarchy := range this.hierarchies {
@ -53,11 +53,11 @@ func (this *System) SetStyle (style tomo.Style) {
} }
} }
// SetIcons notifies objects that the icons have changed. // SetIconSet notifies objects that the icons have changed.
func (this *System) SetIcons (icons tomo.Icons) { func (this *System) SetIconSet (iconSet tomo.IconSet) {
this.iconsNonce ++ this.iconsNonce ++
for hierarchy := range this.hierarchies { for hierarchy := range this.hierarchies {
hierarchy.setIcons() hierarchy.setIconSet()
} }
} }

View File

@ -2,7 +2,6 @@ package system
import "image" import "image"
import "image/color" import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/math/fixed" import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset" import "git.tebibyte.media/tomo/typeset"
@ -14,22 +13,21 @@ import "git.tebibyte.media/tomo/tomo/canvas"
type textBox struct { type textBox struct {
*box *box
hOverflow, vOverflow bool
contentBounds image.Rectangle contentBounds image.Rectangle
scroll image.Point scroll image.Point
text string attrTextColor attrHierarchy[tomo.AttrTextColor]
textColor color.Color attrDotColor attrHierarchy[tomo.AttrDotColor]
face font.Face attrFace attrHierarchy[tomo.AttrFace]
wrap bool attrWrap attrHierarchy[tomo.AttrWrap]
hAlign tomo.Align attrAlign attrHierarchy[tomo.AttrAlign]
vAlign tomo.Align attrOverflow attrHierarchy[tomo.AttrOverflow]
text string
selectable bool selectable bool
selecting bool selecting bool
selectStart int selectStart int
dot text.Dot dot text.Dot
dotColor color.Color
drawer typeset.Drawer drawer typeset.Drawer
@ -40,20 +38,12 @@ type textBox struct {
} }
func (this *System) NewTextBox () tomo.TextBox { func (this *System) NewTextBox () tomo.TextBox {
box := &textBox { box := &textBox { }
textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
}
box.box = this.newBox(box) box.box = this.newBox(box)
return box return box
} }
func (this *textBox) SetOverflow (horizontal, vertical bool) { // ----- public methods ----------------------------------------------------- //
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal
this.vOverflow = vertical
this.invalidateLayout()
}
func (this *textBox) ContentBounds () image.Rectangle { func (this *textBox) ContentBounds () image.Rectangle {
return this.contentBounds return this.contentBounds
@ -71,7 +61,7 @@ func (this *textBox) RecommendedHeight (width int) int {
func (this *textBox) RecommendedWidth (height int) int { func (this *textBox) RecommendedWidth (height int) int {
// TODO maybe not the best idea? // TODO maybe not the best idea?
return this.MinimumSize().X return this.minimumSize().X
} }
func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie { func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie {
@ -86,40 +76,11 @@ func (this *textBox) SetText (text string) {
this.invalidateLayout() this.invalidateLayout()
} }
func (this *textBox) SetTextColor (c color.Color) {
if this.textColor == c { return }
this.textColor = c
this.invalidateDraw()
}
func (this *textBox) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.drawer.SetFace(face)
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.drawer.SetWrap(wrap)
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetSelectable (selectable bool) { func (this *textBox) SetSelectable (selectable bool) {
if this.selectable == selectable { return } if this.selectable == selectable { return }
this.selectable = selectable this.selectable = selectable
} }
func (this *textBox) SetDotColor (c color.Color) {
if this.dotColor == c { return }
this.dotColor = c
if !this.dot.Empty() {
this.invalidateDraw()
}
}
func (this *textBox) Select (dot text.Dot) { func (this *textBox) Select (dot text.Dot) {
if !this.selectable { return } if !this.selectable { return }
if this.dot == dot { return } if this.dot == dot { return }
@ -138,20 +99,19 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie {
return this.on.dotChange.Connect(callback) return this.on.dotChange.Connect(callback)
} }
func (this *textBox) SetAlign (x, y tomo.Align) { // ----- private methods ---------------------------------------------------- //
if this.hAlign == x && this.vAlign == y { return }
this.hAlign = x
this.vAlign = y
this.drawer.SetAlign(typeset.Align(x), typeset.Align(y))
this.invalidateDraw()
}
func (this *textBox) Draw (can canvas.Canvas) { func (this *textBox) Draw (can canvas.Canvas) {
if can == nil { return } if can == nil { return }
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
this.drawBorders(can) this.drawBorders(can)
pen := can.Pen() pen := can.Pen()
pen.Fill(this.color) pen.Fill(col)
pen.Texture(this.texture) pen.Texture(texture)
if this.transparent() && this.parent != nil { if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can) this.parent.drawBackgroundPart(can)
@ -162,8 +122,50 @@ func (this *textBox) Draw (can canvas.Canvas) {
this.drawDot(can) this.drawDot(can)
} }
if this.face == nil { return } if this.attrFace.Value().Face != nil {
this.drawer.Draw(can, this.textColor, this.textOffset()) textColor := this.attrTextColor.Value().Color
if textColor == nil { textColor = color.Black }
this.drawer.Draw(can, textColor, this.textOffset())
}
}
func (this *textBox) setAttr (attr tomo.Attr, user bool) {
switch attr := attr.(type) {
case tomo.AttrTextColor:
if this.attrTextColor.Set(attr, user) && !this.dot.Empty() {
this.invalidateDraw()
}
case tomo.AttrDotColor:
if this.attrDotColor.Set(attr, user) && !this.dot.Empty() {
this.invalidateDraw()
}
case tomo.AttrFace:
if this.attrFace.Set(attr, user) {
this.drawer.SetFace(attr.Face)
this.invalidateMinimum()
this.invalidateLayout()
}
case tomo.AttrWrap:
if this.attrWrap.Set(attr, user) {
this.invalidateMinimum()
this.invalidateLayout()
}
case tomo.AttrAlign:
if this.attrAlign.Set(attr, user) {
this.invalidateDraw()
}
case tomo.AttrOverflow:
if this.attrOverflow.Set(attr, user) {
this.invalidateLayout()
}
default: this.box.setAttr(attr, user)
}
} }
func roundPt (point fixed.Point26_6) image.Point { func roundPt (point fixed.Point26_6) image.Point {
@ -175,14 +177,20 @@ func fixPt (point image.Point) fixed.Point26_6 {
} }
func (this *textBox) drawDot (can canvas.Canvas) { func (this *textBox) drawDot (can canvas.Canvas) {
if this.face == nil { return } if this.attrFace.Value().Face == nil { return }
face := this.attrFace.Value().Face
textColor := this.attrTextColor.Value().Color
dotColor := this.attrDotColor.Value().Color
if textColor == nil { textColor = color.Transparent }
if dotColor == nil { dotColor = color.RGBA { G: 255, B: 255, A: 255 } }
pen := can.Pen() pen := can.Pen()
pen.Fill(color.Transparent) pen.Fill(color.Transparent)
pen.Stroke(this.textColor) pen.Stroke(textColor)
bounds := this.InnerBounds() bounds := this.InnerBounds()
metrics := this.face.Metrics() metrics := face.Metrics()
dot := this.dot.Canon() dot := this.dot.Canon()
start := this.drawer.PositionAt(dot.Start).Add(fixPt(this.textOffset())) start := this.drawer.PositionAt(dot.Start).Add(fixPt(this.textOffset()))
end := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset())) end := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset()))
@ -196,7 +204,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent))) pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
case start.Y == end.Y: case start.Y == end.Y:
pen.Fill(this.dotColor) pen.Fill(dotColor)
pen.StrokeWeight(0) pen.StrokeWeight(0)
pen.Rectangle(image.Rectangle { pen.Rectangle(image.Rectangle {
Min: roundPt(start.Add(ascent)), Min: roundPt(start.Add(ascent)),
@ -204,7 +212,7 @@ func (this *textBox) drawDot (can canvas.Canvas) {
}) })
default: default:
pen.Fill(this.dotColor) pen.Fill(dotColor)
pen.StrokeWeight(0) pen.StrokeWeight(0)
rect := image.Rectangle { rect := image.Rectangle {
@ -241,35 +249,37 @@ func (this *textBox) handleFocusLeave () {
this.box.handleFocusLeave() this.box.handleFocusLeave()
} }
func (this *textBox) handleMouseDown (button input.Button) { func (this *textBox) handleMouseDown (button input.Button) bool {
if button == input.ButtonLeft { if button == input.ButtonLeft {
index := this.runeUnderMouse() index := this.runeUnderMouse()
this.selectStart = index this.selectStart = index
this.selecting = true this.selecting = true
this.Select(text.Dot { Start: this.selectStart, End: index }) this.Select(text.Dot { Start: this.selectStart, End: index })
} }
this.box.handleMouseDown(button) return this.box.handleMouseDown(button)
} }
func (this *textBox) handleMouseUp (button input.Button) { func (this *textBox) handleMouseUp (button input.Button) bool {
if button == input.ButtonLeft && this.selecting { if button == input.ButtonLeft && this.selecting {
index := this.runeUnderMouse() index := this.runeUnderMouse()
this.selecting = false this.selecting = false
this.Select(text.Dot { Start: this.selectStart, End: index }) this.Select(text.Dot { Start: this.selectStart, End: index })
} }
this.box.handleMouseUp(button) return this.box.handleMouseUp(button)
} }
func (this *textBox) handleMouseMove () { func (this *textBox) handleMouseMove () bool {
if this.selecting { if this.selecting {
index := this.runeUnderMouse() index := this.runeUnderMouse()
this.Select(text.Dot { Start: this.selectStart, End: index }) this.Select(text.Dot { Start: this.selectStart, End: index })
} }
this.box.handleMouseMove() return this.box.handleMouseMove()
} }
func (this *textBox) runeUnderMouse () int { func (this *textBox) runeUnderMouse () int {
position := this.MousePosition().Sub(this.textOffset()) window := this.Window()
if window == nil { return 0 }
position := window.MousePosition().Sub(this.textOffset())
return this.drawer.AtPosition(fixPt(position)) return this.drawer.AtPosition(fixPt(position))
} }
@ -281,10 +291,10 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
func (this *textBox) contentMinimum () image.Point { func (this *textBox) contentMinimum () image.Point {
minimum := this.drawer.MinimumSize() minimum := this.drawer.MinimumSize()
if this.hOverflow || this.wrap { if this.attrOverflow.Value().X || bool(this.attrWrap.Value()) {
minimum.X = this.drawer.Em().Round() minimum.X = this.drawer.Em().Round()
} }
if this.vOverflow { if this.attrOverflow.Value().Y {
minimum.Y = this.drawer.LineHeight().Round() minimum.Y = this.drawer.LineHeight().Round()
} }

View File

@ -1,5 +1,6 @@
package util package util
import "io"
import "image/color" import "image/color"
// IndexOf returns the index of needle within haystack. If needle does not exist // IndexOf returns the index of needle within haystack. If needle does not exist
@ -99,3 +100,32 @@ func (this *Memo[T]) InvalidateTo (value T) {
this.Invalidate() this.Invalidate()
this.cache = value this.cache = value
} }
// Cycler stores a value and an accompanying io.Closer. When the value is set,
// the closer associated with the previous value is closed.
type Cycler[T any] struct {
value T
closer io.Closer
}
// Value returns the cycler's value.
func (this *Cycler[T]) Value () T {
return this.value
}
// Set sets the value and associated closer, closing the previous one.
func (this *Cycler[T]) Set (value T, closer io.Closer) (err error) {
if this.closer != nil {
err = this.closer.Close()
}
this.value = value
this.closer = closer
return err
}
// Close closes the associated closer early.
func (this *Cycler[T]) Close () error {
err := this.closer.Close()
this.closer = nil
return err
}