package system import "image" import "image/color" import "git.tebibyte.media/tomo/tomo" 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 } } func (this *System) NewContainerBox () tomo.ContainerBox { 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) 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 { 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) 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() } } 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 } func (this *containerBox) captures (category eventCategory) bool { return this.capture[category] }