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/event"
|
|
|
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
|
|
|
import "git.tebibyte.media/tomo/backend/internal/util"
|
|
|
|
|
|
|
|
type containerBox struct {
|
|
|
|
*box
|
|
|
|
|
|
|
|
hOverflow, vOverflow bool
|
|
|
|
hAlign, vAlign tomo.Align
|
|
|
|
contentBounds image.Rectangle
|
|
|
|
scroll image.Point
|
|
|
|
capture [4]bool
|
|
|
|
|
|
|
|
gap image.Point
|
|
|
|
children []tomo.Box
|
|
|
|
layout tomo.Layout
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) SetColor (c color.Color) {
|
|
|
|
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 {
|
|
|
|
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) 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) {
|
|
|
|
box := assertAnyBox(child.GetBox())
|
|
|
|
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return }
|
|
|
|
|
|
|
|
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 := util.IndexOf(this.children, tomo.Box(box))
|
|
|
|
if index < 0 { return }
|
|
|
|
|
|
|
|
box.setParent(nil)
|
|
|
|
this.children = util.Remove(this.children, index)
|
|
|
|
this.invalidateLayout()
|
|
|
|
this.invalidateMinimum()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) Insert (child, before tomo.Object) {
|
|
|
|
box := assertAnyBox(child.GetBox())
|
|
|
|
if util.IndexOf(this.children, tomo.Box(box)) > -1 { return }
|
|
|
|
|
|
|
|
beforeBox := assertAnyBox(before.GetBox())
|
|
|
|
index := util.IndexOf(this.children, tomo.Box(beforeBox))
|
|
|
|
|
|
|
|
if index < 0 {
|
|
|
|
this.children = append(this.children, tomo.Box(box))
|
|
|
|
} else {
|
|
|
|
this.children = util.Insert(this.children, index, tomo.Box(box))
|
|
|
|
}
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) Length () int {
|
|
|
|
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]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) SetLayout (layout tomo.Layout) {
|
|
|
|
this.layout = layout
|
|
|
|
this.invalidateLayout()
|
|
|
|
this.invalidateMinimum()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) Draw (can canvas.Canvas) {
|
|
|
|
if can == nil { return }
|
|
|
|
|
|
|
|
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()
|
|
|
|
pen.Fill(this.color)
|
|
|
|
pen.Texture(this.texture)
|
|
|
|
pen.Rectangle(this.innerClippingBounds)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
|
|
|
|
if can == nil { return }
|
|
|
|
pen := can.Pen()
|
|
|
|
pen.Fill(this.color)
|
|
|
|
pen.Texture(this.texture)
|
|
|
|
|
|
|
|
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) notifyMinimumSizeChange (child anyBox) {
|
|
|
|
this.invalidateMinimum()
|
|
|
|
size := child.MinimumSize()
|
|
|
|
bounds := child.Bounds()
|
|
|
|
if bounds.Dx() < size.X || bounds.Dy() < size.Y {
|
|
|
|
this.invalidateLayout()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) layoutHints () tomo.LayoutHints {
|
|
|
|
return tomo.LayoutHints {
|
|
|
|
OverflowX: this.hOverflow,
|
|
|
|
OverflowY: this.vOverflow,
|
|
|
|
AlignX: this.hAlign,
|
|
|
|
AlignY: this.vAlign,
|
|
|
|
Gap: this.gap,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) contentMinimum () image.Point {
|
|
|
|
minimum := this.box.contentMinimum()
|
|
|
|
if this.layout != nil {
|
|
|
|
layoutMinimum := this.layout.MinimumSize (
|
|
|
|
this.layoutHints(),
|
|
|
|
this.children)
|
|
|
|
if this.hOverflow { layoutMinimum.X = 0 }
|
|
|
|
if this.vOverflow { layoutMinimum.Y = 0 }
|
|
|
|
minimum = minimum.Add(layoutMinimum)
|
|
|
|
}
|
|
|
|
return minimum
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) doLayout () {
|
|
|
|
this.box.doLayout()
|
|
|
|
previousContentBounds := this.contentBounds
|
|
|
|
|
|
|
|
// 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
|
|
|
|
if this.layout != nil {
|
|
|
|
minimum = this.layout.MinimumSize (
|
|
|
|
this.layoutHints(),
|
|
|
|
this.children)
|
|
|
|
}
|
|
|
|
innerBounds := this.InnerBounds()
|
|
|
|
this.contentBounds = innerBounds.Sub(innerBounds.Min)
|
|
|
|
if this.hOverflow { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X }
|
|
|
|
if this.vOverflow { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y }
|
|
|
|
|
|
|
|
// arrange children
|
|
|
|
if this.layout != nil {
|
|
|
|
layoutHints := this.layoutHints()
|
|
|
|
layoutHints.Bounds = this.contentBounds
|
|
|
|
this.layout.Arrange(layoutHints, this.children)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
box.SetBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) recursiveRedo () {
|
|
|
|
this.doLayout()
|
|
|
|
this.doDraw()
|
|
|
|
for _, child := range this.children {
|
|
|
|
child.(anyBox).recursiveRedo()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox {
|
|
|
|
if !point.In(this.bounds) { return nil }
|
|
|
|
|
|
|
|
if !this.capture[category] {
|
|
|
|
for _, box := range this.children {
|
|
|
|
candidate := box.(anyBox).boxUnder(point, category)
|
|
|
|
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-06-02 11:23:03 -06:00
|
|
|
func (this *containerBox) captures (category eventCategory) bool {
|
|
|
|
return this.capture[category]
|
2024-06-01 14:39:14 -06:00
|
|
|
}
|