backend/internal/system/box.go

643 lines
16 KiB
Go
Raw Normal View History

2024-06-01 14:39:14 -06:00
package system
2024-06-02 11:23:03 -06:00
import "image"
import "image/color"
2024-06-01 14:39:14 -06:00
import "git.tebibyte.media/tomo/tomo"
2024-06-02 11:23:03 -06:00
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/tomo/backend/internal/util"
type box struct {
system *System
parent parent
outer anyBox
2024-07-25 11:01:15 -06:00
tags util.Set[string]
role tomo.Role
lastStyleNonce int
lastIconsNonce int
styleApplicator *styleApplicator
2024-06-03 18:42:54 -06:00
2024-07-25 11:01:15 -06:00
minSize util.Memo[image.Point]
2024-06-02 11:23:03 -06:00
bounds image.Rectangle
innerClippingBounds image.Rectangle
2024-07-25 11:01:15 -06:00
focusQueued *bool
2024-06-02 11:23:03 -06:00
2024-07-25 11:01:15 -06:00
attrColor attrHierarchy[tomo.AttrColor]
attrTexture attrHierarchy[tomo.AttrTexture]
attrTextureMode attrHierarchy[tomo.AttrTextureMode]
attrBorder attrHierarchy[tomo.AttrBorder]
attrMinimumSize attrHierarchy[tomo.AttrMinimumSize]
attrPadding attrHierarchy[tomo.AttrPadding]
2024-06-02 11:23:03 -06:00
dndData data.Data
dndAccept []data.Mime
focusable bool
2024-07-25 11:01:15 -06:00
hovered bool
focused bool
pressed bool
2024-06-02 11:23:03 -06:00
canvas util.Memo[canvas.Canvas]
drawer canvas.Drawer
on struct {
2024-07-25 11:01:15 -06:00
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
2024-06-02 11:23:03 -06:00
}
}
func (this *System) newBox (outer anyBox) *box {
box := &box {
system: this,
outer: outer,
drawer: outer,
2024-07-25 16:17:43 -06:00
tags: make(util.Set[string]),
2024-06-02 11:23:03 -06:00
}
box.canvas = util.NewMemo (func () canvas.Canvas {
if box.parent == nil { return nil }
parentCanvas := box.parent.getCanvas()
if parentCanvas == nil { return nil }
return parentCanvas.SubCanvas(box.bounds)
})
if outer == nil {
box.drawer = box
box.outer = box
}
2024-07-25 11:01:15 -06:00
box.minSize = util.NewMemo(box.calculateMinimumSize)
2024-06-02 11:23:03 -06:00
return box
}
2024-06-01 14:39:14 -06:00
func (this *System) NewBox () tomo.Box {
2024-06-02 11:23:03 -06:00
return this.newBox(nil)
}
2024-07-25 11:01:15 -06:00
// ----- public methods ----------------------------------------------------- //
2024-06-02 11:23:03 -06:00
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 {
2024-07-25 11:01:15 -06:00
return tomo.Inset(this.attrPadding.Value()).Apply(this.innerClippingBounds)
2024-06-02 11:23:03 -06:00
}
2024-06-03 18:42:54 -06:00
func (this *box) Role () tomo.Role {
return this.role
}
2024-07-25 11:01:15 -06:00
func (this *box) SetRole (role tomo.Role) {
if this.role == role { return }
this.role = role
this.lastStyleNonce = -1
this.outer.recursiveReApply()
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
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)
2024-06-02 11:23:03 -06:00
}
}
2024-07-25 11:01:15 -06:00
func (this *box) SetTag (tag string, on bool) {
if on {
this.tags.Add(tag)
} else {
delete(this.tags, tag)
}
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
func (this *box) SetAttr (attr tomo.Attr) {
this.outer.setAttr(attr, true)
2024-06-03 18:42:54 -06:00
}
2024-06-02 11:23:03 -06:00
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()
2024-06-02 11:23:03 -06:00
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)
}
}
2024-07-25 11:01:15 -06:00
func (this *box) Focused () bool {
hierarchy := this.getHierarchy()
if hierarchy == nil { return false }
return hierarchy.isFocused(this.outer)
}
2024-06-02 11:23:03 -06:00
func (this *box) SetFocusable (focusable bool) {
if this.focusable == focusable { return }
this.focusable = focusable
if !focusable {
this.SetFocused(false)
}
}
2024-07-25 11:01:15 -06:00
// ----- 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)
// 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()
}
}
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
func (this *box) setBounds (bounds image.Rectangle) {
if this.bounds == bounds { return }
this.bounds = bounds
this.invalidateLayout()
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
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
2024-06-02 11:23:03 -06:00
}
// ----- 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)
}
2024-07-25 11:01:15 -06:00
func (this *box) OnMouseMove (callback func() bool) event.Cookie {
2024-06-02 11:23:03 -06:00
return this.on.mouseMove.Connect(callback)
}
2024-07-25 11:01:15 -06:00
func (this *box) OnButtonDown (callback func(input.Button) bool) event.Cookie {
return this.on.buttonDown.Connect(callback)
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
func (this *box) OnButtonUp (callback func(input.Button) bool) event.Cookie {
return this.on.buttonUp.Connect(callback)
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
func (this *box) OnScroll (callback func(float64, float64) bool) event.Cookie {
2024-06-02 11:23:03 -06:00
return this.on.scroll.Connect(callback)
}
2024-07-25 11:01:15 -06:00
func (this *box) OnKeyDown (callback func(input.Key, bool) bool) event.Cookie {
2024-06-02 11:23:03 -06:00
return this.on.keyDown.Connect(callback)
}
2024-07-25 11:01:15 -06:00
func (this *box) OnKeyUp (callback func(input.Key, bool) bool) event.Cookie {
2024-06-02 11:23:03 -06:00
return this.on.keyUp.Connect(callback)
}
2024-06-11 16:12:47 -06:00
func (this *box) OnStyleChange (callback func()) event.Cookie {
return this.on.styleChange.Connect(callback)
}
2024-07-25 11:01:15 -06:00
func (this *box) OnIconSetChange (callback func()) event.Cookie {
return this.on.iconSetChange.Connect(callback)
2024-06-11 16:12:47 -06:00
}
2024-06-02 11:23:03 -06:00
func (this *box) handleFocusEnter () {
2024-07-25 11:01:15 -06:00
this.focused = true
this.invalidateStyle()
2024-06-02 11:23:03 -06:00
this.on.focusEnter.Broadcast()
}
func (this *box) handleFocusLeave () {
2024-07-25 11:01:15 -06:00
this.focused = false
this.invalidateStyle()
2024-06-02 11:23:03 -06:00
this.on.focusLeave.Broadcast()
}
func (this *box) handleMouseEnter () {
2024-07-25 11:01:15 -06:00
this.hovered = true
this.invalidateStyle()
2024-06-02 11:23:03 -06:00
this.on.mouseEnter.Broadcast()
}
func (this *box) handleMouseLeave () {
2024-07-25 11:01:15 -06:00
this.hovered = false
this.invalidateStyle()
2024-06-02 11:23:03 -06:00
this.on.mouseLeave.Broadcast()
}
2024-07-25 11:01:15 -06:00
func (this *box) handleMouseMove () (caught bool) {
for _, listener := range this.on.mouseMove.Listeners() {
if listener() { caught = true }
}
return
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
func (this *box) handleMouseDown (button input.Button) (caught bool) {
if button == input.ButtonLeft {
this.pressed = true
this.invalidateStyle()
}
2024-06-02 11:23:03 -06:00
if this.focusable {
this.SetFocused(true)
} else {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.focus(nil)
}
2024-07-25 11:01:15 -06:00
for _, listener := range this.on.buttonDown.Listeners() {
if listener(button) { caught = true }
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
return
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
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 }
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
return
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
func (this *box) handleScroll (x, y float64) (caught bool) {
2024-06-02 11:23:03 -06:00
for _, listener := range this.on.scroll.Listeners() {
2024-07-25 11:01:15 -06:00
if listener(x, y) { caught = true }
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
return
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
func (this *box) handleKeyDown (key input.Key, numberPad bool) (caught bool) {
2024-06-02 11:23:03 -06:00
for _, listener := range this.on.keyDown.Listeners() {
2024-07-25 11:01:15 -06:00
if listener(key, numberPad) { caught = true }
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
return
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
func (this *box) handleKeyUp (key input.Key, numberPad bool) (caught bool) {
2024-06-02 11:23:03 -06:00
for _, listener := range this.on.keyUp.Listeners() {
2024-07-25 11:01:15 -06:00
if listener(key, numberPad) { caught = true }
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
return
2024-06-02 11:23:03 -06:00
}
// -------------------------------------------------------------------------- //
func (this *box) Draw (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
bounds := this.Bounds()
2024-07-25 11:01:15 -06:00
// get values
textureMode := tomo.TextureMode(this.attrTextureMode.Value())
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
2024-06-02 11:23:03 -06:00
// background
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
2024-07-25 11:01:15 -06:00
pen.Fill(col)
if textureMode == tomo.TextureModeTile && texture != nil {
pen.Texture(texture)
}
2024-06-02 11:23:03 -06:00
pen.Rectangle(bounds)
// centered texture
2024-07-25 11:01:15 -06:00
if textureMode == tomo.TextureModeCenter && texture != nil {
textureBounds := texture.Bounds()
2024-06-02 11:23:03 -06:00
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)
2024-07-25 11:01:15 -06:00
pen.Texture(texture)
2024-06-02 11:23:03 -06:00
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)
2024-07-25 11:01:15 -06:00
if area.Empty() { return }
2024-06-02 11:23:03 -06:00
if util.Transparent(c) && this.parent != nil {
this.parent.drawBackgroundPart(can.SubCanvas(area))
}
pen.Fill(c)
pen.Rectangle(area)
}
2024-07-25 11:01:15 -06:00
for _, border := range this.attrBorder.Value() {
2024-06-02 11:23:03 -06:00
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
2024-07-25 11:01:15 -06:00
padding := tomo.Inset(this.attrPadding.Value())
minimum.X += padding.Horizontal()
minimum.Y += padding.Vertical()
2024-06-02 11:23:03 -06:00
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
return minimum
}
2024-07-25 11:01:15 -06:00
func (this *box) calculateMinimumSize () image.Point {
userMinSize := this.attrMinimumSize.Value()
minSize := this.outer.contentMinimum()
if minSize.X < userMinSize.X {
minSize.X = userMinSize.X
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
if minSize.Y < userMinSize.Y {
minSize.Y = userMinSize.Y
2024-06-02 11:23:03 -06:00
}
2024-06-01 14:39:14 -06:00
2024-07-25 11:01:15 -06:00
return minSize
2024-06-02 11:23:03 -06:00
}
// 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)
2024-06-11 16:12:47 -06:00
this.outer.recursiveLoseCanvas()
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
func (this *box) doStyle () {
this.styleApplicator.apply(this)
}
2024-06-02 11:23:03 -06:00
func (this *box) setParent (parent parent) {
if this.parent != parent && this.Focused() {
this.SetFocused(false)
}
this.parent = parent
2024-06-12 00:12:24 -06:00
this.outer.recursiveReApply()
2024-06-02 11:23:03 -06:00
}
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()
}
2024-06-11 16:12:47 -06:00
func (this *box) recursiveLoseCanvas () {
2024-06-02 11:23:03 -06:00
this.canvas.InvalidateTo(nil)
}
2024-07-25 11:01:15 -06:00
func (this *box) invalidateStyle () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
hierarchy.invalidateStyle(this.outer)
}
2024-06-02 11:23:03 -06:00
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 () {
2024-07-25 11:01:15 -06:00
this.minSize.Invalidate()
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
2024-06-02 11:23:03 -06:00
}
2024-06-11 16:12:47 -06:00
func (this *box) recursiveReApply () {
2024-06-11 22:39:00 -06:00
if this.getHierarchy() == nil { return }
2024-06-11 16:12:47 -06:00
// re-apply styling, icons *if needed*
// style
hierarchyStyleNonce := this.getStyleNonce()
if this.lastStyleNonce != hierarchyStyleNonce {
2024-07-25 11:01:15 -06:00
// 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).
2024-06-11 16:12:47 -06:00
this.lastStyleNonce = hierarchyStyleNonce
2024-07-25 11:01:15 -06:00
this.styleApplicator = this.getHierarchy().newStyleApplicator()
this.invalidateStyle()
2024-06-11 16:12:47 -06:00
this.on.styleChange.Broadcast()
}
// icons
hierarchyIconsNonce := this.getIconsNonce()
if this.lastIconsNonce != hierarchyIconsNonce {
this.lastIconsNonce = hierarchyIconsNonce
2024-07-25 11:01:15 -06:00
this.on.iconSetChange.Broadcast()
2024-06-11 16:12:47 -06:00
}
}
2024-06-02 11:23:03 -06:00
func (this *box) canBeFocused () bool {
return this.focusable
}
2024-07-25 11:01:15 -06:00
func (this *box) boxUnder (point image.Point) anyBox {
2024-06-02 11:23:03 -06:00
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
2024-07-25 16:17:43 -06:00
col := this.attrColor.Value().Color
return col == nil || util.Transparent(col) /*&&
2024-06-02 11:23:03 -06:00
(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()
2024-06-01 14:39:14 -06:00
}
2024-06-11 16:12:47 -06:00
func (this *box) getStyleNonce () int {
// should panic if not in the tree
return this.getHierarchy().getStyleNonce()
}
func (this *box) getIconsNonce () int {
// should panic if not in the tree
return this.getHierarchy().getIconsNonce()
}