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 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 on struct { contentBoundsChange event.FuncBroadcaster } } func (this *System) NewContainerBox () tomo.ContainerBox { box := &containerBox { } box.box = this.newBox(box) return box } // ----- public methods ----------------------------------------------------- // 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 util.IndexOf(this.children, 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, 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, box) > -1 { return } beforeBox := assertAnyBox(before.GetBox()) index := util.IndexOf(this.children, beforeBox) if index < 0 { this.children = append(this.children, box) } else { this.children = util.Insert(this.children, index, 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) Len () 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) SetInputMask (mask bool) { this.mask = mask } // ----- private methods ---------------------------------------------------- // func (this *containerBox) Draw (can canvas.Canvas) { if can == nil { return } // textureMode := tomo.TextureMode(this.attrTextureMode.Value()) texture := this.attrTexture.Value().Texture col := this.attrColor.Value().Color if col == nil { col = color.Transparent } 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(col) pen.Texture(texture) pen.Rectangle(this.innerClippingBounds) } } 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() } case tomo.AttrLayout: if this.attrLayout.Set(attr, user) { this.invalidateLayout() this.invalidateMinimum() } default: this.box.setAttr(attr, user) } } func (this *containerBox) recommendedHeight (width int) int { layout := this.attrLayout.Value().Layout if layout == nil || !this.attrOverflow.Value().Y { return this.minSize.Value().Y } else { return layout.RecommendedHeight(this.layoutHints(), this.boxQuerier(), width) + this.borderAndPaddingSum().Vertical() } } func (this *containerBox) recommendedWidth (height int) int { layout := this.attrLayout.Value().Layout if layout == nil || !this.attrOverflow.Value().X { return this.minSize.Value().X } else { return layout.RecommendedWidth(this.layoutHints(), this.boxQuerier(), height) + this.borderAndPaddingSum().Horizontal() } } func (this *containerBox) drawBackgroundPart (can canvas.Canvas) { if can == nil { return } pen := can.Pen() // 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) 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 { overflow := this.attrOverflow.Value() align := this.attrAlign.Value() gap := image.Point(this.attrGap.Value()) return tomo.LayoutHints { OverflowX: overflow.X, OverflowY: overflow.Y, AlignX: align.X, AlignY: align.Y, Gap: gap, } } func (this *containerBox) contentMinimum () image.Point { overflow := this.attrOverflow.Value() minimum := this.box.contentMinimum() layout := this.attrLayout.Value().Layout if layout != nil { layoutMinimum := layout.MinimumSize ( this.layoutHints(), this.boxQuerier()) if overflow.X { layoutMinimum.X = 0 } if overflow.Y { layoutMinimum.Y = 0 } minimum = minimum.Add(layoutMinimum) } return minimum } func (this *containerBox) doLayout () { this.box.doLayout() previousContentBounds := this.contentBounds layout := this.attrLayout.Value().Layout // 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 layout != nil { minimum = layout.MinimumSize ( this.layoutHints(), this.boxQuerier()) } innerBounds := this.InnerBounds() overflow := this.attrOverflow.Value() this.contentBounds = innerBounds.Sub(innerBounds.Min) 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 } // arrange children if layout != nil { layoutHints := this.layoutHints() layoutHints.Bounds = this.contentBounds layout.Arrange(layoutHints, this.boxArranger()) } // 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) boxQuerier () boxQuerier { return boxQuerier(this.children) } func (this *containerBox) boxArranger () boxArranger { return boxArranger(this.children) } 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) anyBox { if !point.In(this.bounds) { return nil } if !this.mask { for _, box := range this.children { candidate := box.(anyBox).boxUnder(point) 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) masks () bool { return this.mask }