backend/internal/system/containerbox.go

480 lines
12 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 "slices"
2024-06-02 11:23:03 -06:00
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/event"
import "git.tebibyte.media/tomo/tomo/canvas"
type containerBox struct {
*box
2024-07-25 11:01:15 -06:00
contentBounds image.Rectangle
scroll image.Point
mask bool
attrGap attrHierarchy[tomo.AttrGap]
attrAlign attrHierarchy[tomo.AttrAlign]
attrOverflow attrHierarchy[tomo.AttrOverflow]
attrLayout attrHierarchy[tomo.AttrLayout]
children []anyBox
2024-06-02 11:23:03 -06:00
on struct {
contentBoundsChange event.FuncBroadcaster
}
}
2024-06-01 14:39:14 -06:00
func (this *System) NewContainerBox () tomo.ContainerBox {
2024-06-02 11:23:03 -06:00
box := &containerBox { }
box.box = this.newBox(box)
return box
}
2024-07-25 11:01:15 -06:00
// ----- public methods ----------------------------------------------------- //
2024-06-02 11:23:03 -06:00
func (this *containerBox) ContentBounds () image.Rectangle {
return this.contentBounds
}
func (this *containerBox) ScrollTo (point image.Point) {
if this.scroll == point { return }
this.scroll = point
this.invalidateLayout()
}
func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback)
}
func (this *containerBox) Add (child tomo.Object) {
box := assertAnyBox(child.GetBox())
if slices.Index(this.children, box) > -1 { return }
2024-06-02 11:23:03 -06:00
box.setParent(this)
box.flushActionQueue()
this.children = append(this.children, box)
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Remove (child tomo.Object) {
box := assertAnyBox(child.GetBox())
index := slices.Index(this.children, box)
2024-06-02 11:23:03 -06:00
if index < 0 { return }
box.setParent(nil)
this.children = slices.Delete(this.children, index, index + 1)
2024-06-02 11:23:03 -06:00
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Insert (child, before tomo.Object) {
box := assertAnyBox(child.GetBox())
if slices.Index(this.children, box) > -1 { return }
2024-06-02 11:23:03 -06:00
beforeBox := assertAnyBox(before.GetBox())
index := slices.Index(this.children, beforeBox)
2024-06-02 11:23:03 -06:00
if index < 0 {
2024-07-25 11:01:15 -06:00
this.children = append(this.children, box)
2024-06-02 11:23:03 -06:00
} else {
this.children = slices.Insert(this.children, index, box)
2024-06-02 11:23:03 -06:00
}
box.setParent(this)
this.invalidateLayout()
this.invalidateMinimum()
}
func (this *containerBox) Clear () {
for _, box := range this.children {
box.(anyBox).setParent(nil)
}
this.children = nil
this.invalidateLayout()
this.invalidateMinimum()
}
2024-07-25 11:01:15 -06:00
func (this *containerBox) Len () int {
2024-06-02 11:23:03 -06:00
return len(this.children)
}
func (this *containerBox) At (index int) tomo.Object {
if index < 0 || index >= len(this.children) {
return nil
}
return this.children[index]
}
2024-07-25 11:01:15 -06:00
func (this *containerBox) SetInputMask (mask bool) {
this.mask = mask
2024-06-02 11:23:03 -06:00
}
2024-07-25 11:01:15 -06:00
// ----- private methods ---------------------------------------------------- //
2024-06-02 11:23:03 -06:00
func (this *containerBox) Draw (can canvas.Canvas) {
if can == nil { return }
2024-07-25 11:01:15 -06:00
// 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
rocks := make([]image.Rectangle, len(this.children))
for index, box := range this.children {
rocks[index] = box.Bounds()
}
for _, tile := range canvas.Shatter(this.bounds, rocks...) {
clipped := can.SubCanvas(tile)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(clipped)
}
if clipped == nil { continue }
pen := clipped.Pen()
2024-07-25 11:01:15 -06:00
pen.Fill(col)
pen.Texture(texture)
2024-06-02 11:23:03 -06:00
pen.Rectangle(this.innerClippingBounds)
}
}
2024-07-25 11:01:15 -06:00
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()
this.invalidateMinimum()
2024-07-25 11:01:15 -06:00
}
case tomo.AttrLayout:
if this.attrLayout.Set(attr, user) {
this.invalidateLayout()
this.invalidateMinimum()
}
default: this.box.setAttr(attr, user)
}
}
2024-08-10 18:24:25 -06:00
func (this *containerBox) unsetAttr (kind tomo.AttrKind, user bool) {
switch kind {
case tomo.AttrKindColor:
if this.attrColor.Unset(user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrKindTexture:
if this.attrTexture.Unset(user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrKindTextureMode:
if this.attrTextureMode.Unset(user) {
this.invalidateTransparentChildren()
this.invalidateDraw()
}
case tomo.AttrKindGap:
if this.attrGap.Unset(user) {
this.invalidateLayout()
this.invalidateMinimum()
}
case tomo.AttrKindAlign:
if this.attrAlign.Unset(user) {
this.invalidateLayout()
}
case tomo.AttrKindOverflow:
if this.attrOverflow.Unset(user) {
this.invalidateLayout()
this.invalidateMinimum()
}
case tomo.AttrKindLayout:
if this.attrLayout.Unset(user) {
this.invalidateLayout()
this.invalidateMinimum()
}
default: this.box.unsetAttr(kind, user)
}
}
2024-07-25 11:01:15 -06:00
func (this *containerBox) recommendedHeight (width int) int {
2024-07-25 19:05:03 -06:00
layout := this.attrLayout.Value().Layout
if layout == nil || !this.attrOverflow.Value().Y {
2024-07-25 11:01:15 -06:00
return this.minSize.Value().Y
} else {
2024-07-25 19:05:03 -06:00
return layout.RecommendedHeight(this.layoutHints(), this.boxQuerier(), width) +
2024-07-25 11:01:15 -06:00
this.borderAndPaddingSum().Vertical()
}
}
func (this *containerBox) recommendedWidth (height int) int {
2024-07-25 19:05:03 -06:00
layout := this.attrLayout.Value().Layout
if layout == nil || !this.attrOverflow.Value().X {
2024-07-25 11:01:15 -06:00
return this.minSize.Value().X
} else {
2024-07-25 19:05:03 -06:00
return layout.RecommendedWidth(this.layoutHints(), this.boxQuerier(), height) +
2024-07-25 11:01:15 -06:00
this.borderAndPaddingSum().Horizontal()
}
}
2024-06-02 11:23:03 -06:00
func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
if can == nil { return }
pen := can.Pen()
2024-07-25 11:01:15 -06:00
// 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)
2024-06-02 11:23:03 -06:00
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(this.innerClippingBounds)
}
func (this *containerBox) invalidateTransparentChildren () {
hierarchy := this.getHierarchy()
if hierarchy == nil { return }
for _, box := range this.children {
box := assertAnyBox(box)
if box.transparent() {
hierarchy.invalidateDraw(box)
}
}
}
func (this *containerBox) flushActionQueue () {
for _, box := range this.children {
box.(anyBox).flushActionQueue()
}
this.box.flushActionQueue()
}
func (this *containerBox) getHierarchy () *Hierarchy {
if this.parent == nil { return nil }
return this.parent.getHierarchy()
}
func (this *containerBox) getCanvas () canvas.Canvas {
return this.canvas.Value()
}
func (this *containerBox) getInnerClippingBounds () image.Rectangle {
return this.innerClippingBounds
}
2024-06-02 11:23:03 -06:00
func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.invalidateMinimum()
2024-07-25 11:01:15 -06:00
size := child.minimumSize()
2024-06-02 11:23:03 -06:00
bounds := child.Bounds()
if bounds.Dx() < size.X || bounds.Dy() < size.Y {
this.invalidateLayout()
}
}
func (this *containerBox) layoutHints () tomo.LayoutHints {
2024-07-25 11:01:15 -06:00
overflow := this.attrOverflow.Value()
align := this.attrAlign.Value()
gap := image.Point(this.attrGap.Value())
2024-06-02 11:23:03 -06:00
return tomo.LayoutHints {
2024-07-25 11:01:15 -06:00
OverflowX: overflow.X,
OverflowY: overflow.Y,
AlignX: align.X,
AlignY: align.Y,
Gap: gap,
2024-06-02 11:23:03 -06:00
}
}
func (this *containerBox) contentMinimum () image.Point {
2024-07-25 11:01:15 -06:00
overflow := this.attrOverflow.Value()
minimum := this.box.contentMinimum()
2024-07-25 19:05:03 -06:00
layout := this.attrLayout.Value().Layout
if layout != nil {
layoutMinimum := layout.MinimumSize (
2024-06-02 11:23:03 -06:00
this.layoutHints(),
2024-07-25 11:01:15 -06:00
this.boxQuerier())
if overflow.X { layoutMinimum.X = 0 }
if overflow.Y { layoutMinimum.Y = 0 }
2024-06-02 11:23:03 -06:00
minimum = minimum.Add(layoutMinimum)
}
return minimum
}
func (this *containerBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
2024-07-25 19:05:03 -06:00
layout := this.attrLayout.Value().Layout
2024-06-02 11:23:03 -06:00
// by default, use innerBounds (translated to 0, 0) for contentBounds.
// if a direction overflows, use the layout's minimum size for it.
var minimum image.Point
2024-07-25 19:05:03 -06:00
if layout != nil {
minimum = layout.MinimumSize (
2024-06-02 11:23:03 -06:00
this.layoutHints(),
2024-07-25 11:01:15 -06:00
this.boxQuerier())
2024-06-02 11:23:03 -06:00
}
innerBounds := this.InnerBounds()
2024-07-25 11:01:15 -06:00
overflow := this.attrOverflow.Value()
2024-06-02 11:23:03 -06:00
this.contentBounds = innerBounds.Sub(innerBounds.Min)
2024-07-25 11:01:15 -06:00
if overflow.X { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X }
if overflow.Y { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y }
2024-06-02 11:23:03 -06:00
// arrange children
2024-07-25 19:05:03 -06:00
if layout != nil {
2024-06-02 11:23:03 -06:00
layoutHints := this.layoutHints()
layoutHints.Bounds = this.contentBounds
2024-07-25 19:05:03 -06:00
layout.Arrange(layoutHints, this.boxArranger())
2024-06-02 11:23:03 -06:00
}
// build an accurate contentBounds by unioning the bounds of all child
// boxes
this.contentBounds = image.Rectangle { }
for _, box := range this.children {
bounds := box.Bounds()
this.contentBounds = this.contentBounds.Union(bounds)
}
// constrain the scroll
this.constrainScroll()
// offset children and contentBounds by scroll
for _, box := range this.children {
2024-07-25 19:05:03 -06:00
box.setBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
2024-06-02 11:23:03 -06:00
}
this.contentBounds = this.contentBounds.Add(this.scroll)
if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast()
}
}
func (this *containerBox) constrainScroll () {
innerBounds := this.InnerBounds()
width := this.contentBounds.Dx()
height := this.contentBounds.Dy()
// X
if width <= innerBounds.Dx() {
this.scroll.X = 0
} else if this.scroll.X > 0 {
this.scroll.X = 0
} else if this.scroll.X < innerBounds.Dx() - width {
this.scroll.X = innerBounds.Dx() - width
}
// Y
if height <= innerBounds.Dy() {
this.scroll.Y = 0
} else if this.scroll.Y > 0 {
this.scroll.Y = 0
} else if this.scroll.Y < innerBounds.Dy() - height {
this.scroll.Y = innerBounds.Dy() - height
}
}
2024-07-25 11:01:15 -06:00
func (this *containerBox) boxQuerier () boxQuerier {
return boxQuerier(this.children)
}
func (this *containerBox) boxArranger () boxArranger {
return boxArranger(this.children)
}
2024-06-02 11:23:03 -06:00
func (this *containerBox) recursiveRedo () {
this.doLayout()
this.doDraw()
for _, child := range this.children {
child.(anyBox).recursiveRedo()
}
}
2024-06-11 16:12:47 -06:00
func (this *containerBox) recursiveLoseCanvas () {
this.box.recursiveLoseCanvas()
for _, child := range this.children {
child.(anyBox).recursiveLoseCanvas()
}
}
func (this *containerBox) recursiveReApply () {
this.box.recursiveReApply()
for _, child := range this.children {
child.(anyBox).recursiveReApply()
}
}
2024-07-25 11:01:15 -06:00
func (this *containerBox) boxUnder (point image.Point) anyBox {
2024-06-02 11:23:03 -06:00
if !point.In(this.bounds) { return nil }
2024-07-25 11:01:15 -06:00
if !this.mask {
2024-06-02 11:23:03 -06:00
for _, box := range this.children {
2024-07-25 11:01:15 -06:00
candidate := box.(anyBox).boxUnder(point)
2024-06-02 11:23:03 -06:00
if candidate != nil { return candidate }
}
}
return this
}
func (this *containerBox) propagate (callback func (anyBox) bool) bool {
for _, box := range this.children {
box := box.(anyBox)
if !box.propagate(callback) { return false }
}
return callback(this)
}
func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
if !callback(this) { return false}
for _, box := range this.children {
box := box.(anyBox)
if !box.propagateAlt(callback) { return false }
}
return true
}
2024-06-01 14:39:14 -06:00
2024-07-25 11:01:15 -06:00
func (this *containerBox) masks () bool {
return this.mask
2024-06-01 14:39:14 -06:00
}