backend/internal/system/box.go

710 lines
18 KiB
Go

package system
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/sashakoshka/goutil/container"
import "git.tebibyte.media/sashakoshka/goutil/image/color"
type box struct {
system *System
parent parent
outer anyBox
tags ucontainer.Set[string]
role tomo.Role
lastStyleNonce int
lastIconSetNonce int
styleApplicator *styleApplicator
minSize ucontainer.Memo[image.Point]
bounds image.Rectangle
innerClippingBounds image.Rectangle
focusQueued *bool
attrColor attrHierarchy[tomo.AttrColor]
attrTexture attrHierarchy[tomo.AttrTexture]
attrTextureMode attrHierarchy[tomo.AttrTextureMode]
attrBorder attrHierarchy[tomo.AttrBorder]
attrMinimumSize attrHierarchy[tomo.AttrMinimumSize]
attrPadding attrHierarchy[tomo.AttrPadding]
dndData data.Data
dndAccept []data.Mime
focusable bool
hovered bool
focused bool
pressed bool
canvas ucontainer.Memo[canvas.Canvas]
drawer canvas.Drawer
on struct {
focusEnter event.FuncBroadcaster
focusLeave event.FuncBroadcaster
dndEnter event.FuncBroadcaster
dndLeave event.FuncBroadcaster
dndDrop event.Broadcaster[func (data.Data)]
mouseEnter event.FuncBroadcaster
mouseLeave event.FuncBroadcaster
mouseMove event.Broadcaster[func () bool]
buttonDown event.Broadcaster[func (input.Button) bool]
buttonUp event.Broadcaster[func (input.Button) bool]
scroll event.Broadcaster[func (float64, float64) bool]
keyDown event.Broadcaster[func (input.Key, bool) bool]
keyUp event.Broadcaster[func (input.Key, bool) bool]
styleChange event.FuncBroadcaster
iconSetChange event.FuncBroadcaster
}
}
func (this *System) newBox (outer anyBox) *box {
box := &box {
system: this,
outer: outer,
drawer: outer,
tags: make(ucontainer.Set[string]),
}
box.attrColor.SetFallback(tomo.AColor(color.Transparent))
box.canvas = ucontainer.NewMemo (func () canvas.Canvas {
if box.parent == nil { return nil }
parentCanvas := box.parent.getCanvas()
if parentCanvas == nil { return nil }
drawableArea := box.bounds.Intersect(box.parent.getInnerClippingBounds())
return parentCanvas.SubCanvas(drawableArea)
})
if outer == nil {
box.drawer = box
box.outer = box
}
box.minSize = ucontainer.NewMemo(box.calculateMinimumSize)
return box
}
func (this *System) NewBox () tomo.Box {
return this.newBox(nil)
}
// ----- public methods ----------------------------------------------------- //
func (this *box) GetBox () tomo.Box {
return this.outer
}
func (this *box) Window () tomo.Window {
hierarchy := this.getHierarchy()
if hierarchy == nil { return nil }
return hierarchy.getWindow()
}
func (this *box) Bounds () image.Rectangle {
return this.bounds
}
func (this *box) InnerBounds () image.Rectangle {
return tomo.Inset(this.attrPadding.Value()).Apply(this.innerClippingBounds)
}
func (this *box) Role () tomo.Role {
return this.role
}
func (this *box) SetRole (role tomo.Role) {
if this.role == role { return }
this.role = role
this.lastStyleNonce = -1
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) {
wasOn := this.tags.Has(tag)
if on {
this.tags.Add(tag)
} else {
delete(this.tags, tag)
}
if wasOn != on {
this.invalidateStyle()
}
}
func (this *box) SetAttr (attr tomo.Attr) {
this.outer.setAttr(attr, true)
}
func (this *box) UnsetAttr (kind tomo.AttrKind) {
this.outer.unsetAttr(kind, true)
}
func (this *box) SetDNDData (dat data.Data) {
this.dndData = dat
}
func (this *box) SetDNDAccept (types ...data.Mime) {
this.dndAccept = types
}
func (this *box) SetFocused (focused bool) {
hierarchy := this.getHierarchy()
if hierarchy == nil {
focusedCopy := focused
this.focusQueued = &focusedCopy
return
}
if !this.focusable { return }
if this.Focused () && !focused {
hierarchy.focus(nil)
} else if !this.Focused() && focused {
hierarchy.focus(this.outer)
}
}
func (this *box) Focused () bool {
hierarchy := this.getHierarchy()
if hierarchy == nil { return false }
return hierarchy.isFocused(this.outer)
}
func (this *box) SetFocusable (focusable bool) {
if this.focusable == focusable { return }
this.focusable = focusable
if !focusable {
this.SetFocused(false)
}
}
// ----- private methods ---------------------------------------------------- //
func (this *box) setAttr (attr tomo.Attr, user bool) {
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)
this.handleBorderChange(previousBorderSum, different)
case tomo.AttrMinimumSize:
if this.attrMinimumSize.Set(attr, user) {
this.invalidateMinimum()
}
case tomo.AttrPadding:
if this.attrPadding.Set(attr, user) {
this.invalidateLayout()
this.invalidateMinimum()
}
}
}
func (this *box) unsetAttr (kind tomo.AttrKind, user bool) {
switch kind {
case tomo.AttrKindColor:
if this.attrColor.Unset(user) {
this.invalidateDraw()
}
case tomo.AttrKindTexture:
if this.attrTexture.Unset(user) {
this.invalidateDraw()
}
case tomo.AttrKindTextureMode:
if this.attrTextureMode.Unset(user) {
this.invalidateDraw()
}
case tomo.AttrKindBorder:
previousBorderSum := this.borderSum()
different := this.attrBorder.Unset(user)
this.handleBorderChange(previousBorderSum, different)
case tomo.AttrKindMinimumSize:
if this.attrMinimumSize.Unset(user) {
this.invalidateMinimum()
}
case tomo.AttrKindPadding:
if this.attrPadding.Unset(user) {
this.invalidateLayout()
this.invalidateMinimum()
}
}
}
func (this *box) unsetAllAttrs (user bool) {
// keep this in sync with tomo.AttrKind!
this.outer.unsetAttr(tomo.AttrKindColor, user)
this.outer.unsetAttr(tomo.AttrKindTexture, user)
this.outer.unsetAttr(tomo.AttrKindTextureMode, user)
this.outer.unsetAttr(tomo.AttrKindBorder, user)
this.outer.unsetAttr(tomo.AttrKindMinimumSize, user)
this.outer.unsetAttr(tomo.AttrKindPadding, user)
this.outer.unsetAttr(tomo.AttrKindGap, user)
this.outer.unsetAttr(tomo.AttrKindTextColor, user)
this.outer.unsetAttr(tomo.AttrKindDotColor, user)
this.outer.unsetAttr(tomo.AttrKindFace, user)
this.outer.unsetAttr(tomo.AttrKindWrap, user)
this.outer.unsetAttr(tomo.AttrKindAlign, user)
this.outer.unsetAttr(tomo.AttrKindOverflow, user)
this.outer.unsetAttr(tomo.AttrKindLayout, user)
}
func (this *box) setBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
this.invalidateLayout()
}
func (this *box) minimumSize () image.Point {
return this.minSize.Value()
}
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 ---------------------------------------------- //
func (this *box) OnFocusEnter (callback func()) event.Cookie {
return this.on.focusEnter.Connect(callback)
}
func (this *box) OnFocusLeave (callback func()) event.Cookie {
return this.on.focusLeave.Connect(callback)
}
func (this *box) OnDNDEnter (callback func()) event.Cookie {
return this.on.dndEnter.Connect(callback)
}
func (this *box) OnDNDLeave (callback func()) event.Cookie {
return this.on.dndLeave.Connect(callback)
}
func (this *box) OnDNDDrop (callback func(data.Data)) event.Cookie {
return this.on.dndDrop.Connect(callback)
}
func (this *box) OnMouseEnter (callback func()) event.Cookie {
return this.on.mouseEnter.Connect(callback)
}
func (this *box) OnMouseLeave (callback func()) event.Cookie {
return this.on.mouseLeave.Connect(callback)
}
func (this *box) OnMouseMove (callback func() bool) event.Cookie {
return this.on.mouseMove.Connect(callback)
}
func (this *box) OnButtonDown (callback func(input.Button) bool) event.Cookie {
return this.on.buttonDown.Connect(callback)
}
func (this *box) OnButtonUp (callback func(input.Button) bool) event.Cookie {
return this.on.buttonUp.Connect(callback)
}
func (this *box) OnScroll (callback func(float64, float64) bool) event.Cookie {
return this.on.scroll.Connect(callback)
}
func (this *box) OnKeyDown (callback func(input.Key, bool) bool) event.Cookie {
return this.on.keyDown.Connect(callback)
}
func (this *box) OnKeyUp (callback func(input.Key, bool) bool) event.Cookie {
return this.on.keyUp.Connect(callback)
}
func (this *box) OnStyleChange (callback func()) event.Cookie {
return this.on.styleChange.Connect(callback)
}
func (this *box) OnIconSetChange (callback func()) event.Cookie {
return this.on.iconSetChange.Connect(callback)
}
func (this *box) handleFocusEnter () {
this.focused = true
this.invalidateStyle()
this.on.focusEnter.Broadcast()
}
func (this *box) handleFocusLeave () {
this.focused = false
this.invalidateStyle()
this.on.focusLeave.Broadcast()
}
func (this *box) handleMouseEnter () {
this.hovered = true
this.invalidateStyle()
this.on.mouseEnter.Broadcast()
}
func (this *box) handleMouseLeave () {
this.hovered = false
this.invalidateStyle()
this.on.mouseLeave.Broadcast()
}
func (this *box) handleMouseMove () (caught bool) {
for _, listener := range this.on.mouseMove.Listeners() {
if listener() { caught = true }
}
return
}
func (this *box) handleMouseDown (button input.Button) (caught bool) {
if button == input.ButtonLeft {
this.pressed = true
this.invalidateStyle()
}
for _, listener := range this.on.buttonDown.Listeners() {
if listener(button) { caught = true }
}
return
}
func (this *box) handleMouseUp (button input.Button) (caught bool) {
if button == input.ButtonLeft {
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) (caught bool) {
for _, listener := range this.on.scroll.Listeners() {
if listener(x, y) { caught = true }
}
return
}
func (this *box) handleKeyDown (key input.Key, numberPad bool) (caught bool) {
for _, listener := range this.on.keyDown.Listeners() {
if listener(key, numberPad) { caught = true }
}
return
}
func (this *box) handleKeyUp (key input.Key, numberPad bool) (caught bool) {
for _, listener := range this.on.keyUp.Listeners() {
if listener(key, numberPad) { caught = true }
}
return
}
// -------------------------------------------------------------------------- //
func (this *box) Draw (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
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
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Fill(col)
if textureMode == tomo.TextureModeTile && texture != nil {
pen.Texture(texture)
}
pen.Rectangle(bounds)
// centered texture
if textureMode == tomo.TextureModeCenter && texture != nil {
this.centeredTexture(can, texture)
}
}
func (this *box) centeredTexture (can canvas.Canvas, texture canvas.Texture) {
pen := can.Pen()
bounds := this.Bounds()
textureBounds := texture.Bounds()
textureOrigin :=
bounds.Min.
Add(image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)).
Sub(image.Pt (
textureBounds.Dx() / 2,
textureBounds.Dy() / 2))
pen.Fill(color.Transparent)
pen.Texture(texture)
pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin))
}
func (this *box) drawBorders (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
bounds := this.bounds
rectangle := func (x0, y0, x1, y1 int, c color.Color) {
area := image.Rect(x0, y0, x1, y1)
if area.Empty() { return }
if ucolor.Transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.SubCanvas(area))
}
pen.Fill(c)
pen.Rectangle(area)
}
for _, border := range this.attrBorder.Value() {
rectangle (
bounds.Min.X,
bounds.Min.Y,
bounds.Max.X,
bounds.Min.Y + border.Width[tomo.SideTop],
border.Color[tomo.SideTop])
rectangle (
bounds.Min.X,
bounds.Max.Y - border.Width[tomo.SideBottom],
bounds.Max.X,
bounds.Max.Y,
border.Color[tomo.SideBottom])
rectangle (
bounds.Min.X,
bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Min.X + border.Width[tomo.SideLeft],
bounds.Max.Y - border.Width[tomo.SideBottom],
border.Color[tomo.SideLeft])
rectangle (
bounds.Max.X - border.Width[tomo.SideRight],
bounds.Min.Y + border.Width[tomo.SideTop],
bounds.Max.X,
bounds.Max.Y - border.Width[tomo.SideBottom],
border.Color[tomo.SideRight])
bounds = border.Width.Apply(bounds)
}
}
func (this *box) contentMinimum () image.Point {
var minimum image.Point
padding := tomo.Inset(this.attrPadding.Value())
minimum.X += padding.Horizontal()
minimum.Y += padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
return minimum
}
func (this *box) calculateMinimumSize () image.Point {
userMinSize := this.attrMinimumSize.Value()
minSize := this.outer.contentMinimum()
if minSize.X < userMinSize.X {
minSize.X = userMinSize.X
}
if minSize.Y < userMinSize.Y {
minSize.Y = userMinSize.Y
}
return minSize
}
// var drawcnt int
func (this *box) doDraw () {
// println("DRAW", drawcnt)
// drawcnt ++
canvas := this.canvas.Value()
if canvas == nil { return }
if this.drawer != nil {
this.drawBorders(canvas)
this.drawer.Draw(canvas.SubCanvas(this.innerClippingBounds))
}
}
// var laycnt int
func (this *box) doLayout () {
// println("LAYOUT", laycnt)
// laycnt ++
this.innerClippingBounds = this.borderSum().Apply(this.bounds)
this.outer.recursiveLoseCanvas()
}
func (this *box) doStyle () {
this.styleApplicator.apply(this.outer)
}
func (this *box) setParent (parent parent) {
if this.parent != parent && this.Focused() {
this.SetFocused(false)
}
this.parent = parent
this.outer.recursiveReApply()
}
func (this *box) getParent () parent {
return this.parent
}
func (this *box) flushActionQueue () {
if this.getHierarchy() == nil { return }
if this.focusQueued != nil {
this.SetFocused(*this.focusQueued)
}
}
func (this *box) recursiveRedo () {
this.doLayout()
this.doDraw()
}
func (this *box) recursiveLoseCanvas () {
this.canvas.Invalidate()
}
func (this *box) handleBorderChange (previousBorderSum tomo.Inset, different bool) {
// 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()
}
}
func (this *box) invalidateStyle () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.invalidateStyle(this.outer)
}
func (this *box) invalidateLayout () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.invalidateLayout(this.outer)
}
func (this *box) invalidateDraw () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.invalidateDraw(this.outer)
}
func (this *box) invalidateMinimum () {
this.minSize.Invalidate()
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
}
func (this *box) recursiveReApply () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
// re-apply styling, icons *if needed*
// style
hierarchyStyleNonce := this.getStyleNonce()
if this.lastStyleNonce != hierarchyStyleNonce {
// 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.unsetAllAttrs(false)
this.lastStyleNonce = hierarchyStyleNonce
this.styleApplicator = hierarchy.newStyleApplicator()
this.invalidateStyle()
this.on.styleChange.Broadcast()
}
// icons
hierarchyIconSetNonce := this.getIconSetNonce()
if this.lastIconSetNonce != hierarchyIconSetNonce {
this.lastIconSetNonce = hierarchyIconSetNonce
this.on.iconSetChange.Broadcast()
}
}
func (this *box) canBeFocused () bool {
return this.focusable
}
func (this *box) boxUnder (point image.Point) anyBox {
if point.In(this.bounds) {
return this.outer
} else {
return nil
}
}
func (this *box) propagate (callback func (anyBox) bool) bool {
return callback(this.outer)
}
func (this *box) propagateAlt (callback func (anyBox) bool) bool {
return callback(this.outer)
}
func (this *box) transparent () bool {
// TODO uncomment once we have
// a way to detect texture transparency
col := this.attrColor.Value().Color
return col == nil || ucolor.Transparent(col) /*&&
(this.texture == nil || !this.texture.Opaque())*/
}
func (this *box) getWindow () tomo.Window {
hierarchy := this.getHierarchy()
if hierarchy == nil { return nil }
return hierarchy.getWindow()
}
func (this *box) getHierarchy () *Hierarchy {
if this.parent == nil { return nil }
return this.parent.getHierarchy()
}
func (this *box) getStyleNonce () int {
// should panic if not in the tree
return this.getHierarchy().getStyleNonce()
}
func (this *box) getIconSetNonce () int {
// should panic if not in the tree
return this.getHierarchy().getIconSetNonce()
}