package system import "image" import "image/color" import "git.tebibyte.media/tomo/tomo" 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/sashakoshka/goutil/container" import "git.tebibyte.media/sashakoshka/goutil/image/color" type box struct { system *System parent parent outer anyBox tags ucontainer.Set[string] role tomo.Role lastStyleNonce int lastIconSetNonce int styleApplicator *styleApplicator minSize ucontainer.Memo[image.Point] bounds image.Rectangle innerClippingBounds image.Rectangle focusQueued *bool attrColor attrHierarchy[tomo.AttrColor] attrTexture attrHierarchy[tomo.AttrTexture] attrTextureMode attrHierarchy[tomo.AttrTextureMode] attrBorder attrHierarchy[tomo.AttrBorder] attrMinimumSize attrHierarchy[tomo.AttrMinimumSize] attrPadding attrHierarchy[tomo.AttrPadding] dndData data.Data dndAccept []data.Mime focusable bool hovered bool focused bool pressed bool canvas ucontainer.Memo[canvas.Canvas] drawer canvas.Drawer on struct { 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 } } func (this *System) newBox (outer anyBox) *box { box := &box { system: this, outer: outer, drawer: outer, tags: make(ucontainer.Set[string]), } box.attrColor.SetFallback(tomo.AColor(color.Transparent)) box.canvas = ucontainer.NewMemo (func () canvas.Canvas { if box.parent == nil { return nil } parentCanvas := box.parent.getCanvas() if parentCanvas == nil { return nil } drawableArea := box.bounds.Intersect(box.parent.getInnerClippingBounds()) return parentCanvas.SubCanvas(drawableArea) }) if outer == nil { box.drawer = box box.outer = box } box.minSize = ucontainer.NewMemo(box.calculateMinimumSize) return box } func (this *System) NewBox () tomo.Box { return this.newBox(nil) } // ----- public methods ----------------------------------------------------- // 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 { return tomo.Inset(this.attrPadding.Value()).Apply(this.innerClippingBounds) } func (this *box) Role () tomo.Role { return this.role } func (this *box) SetRole (role tomo.Role) { if this.role == role { return } this.role = role this.lastStyleNonce = -1 this.outer.recursiveReApply() } 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) } } func (this *box) SetTag (tag string, on bool) { wasOn := this.tags.Has(tag) if on { this.tags.Add(tag) } else { delete(this.tags, tag) } if wasOn != on { this.invalidateStyle() } } func (this *box) SetAttr (attr tomo.Attr) { this.outer.setAttr(attr, true) } func (this *box) UnsetAttr (kind tomo.AttrKind) { this.outer.unsetAttr(kind, true) } 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() 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) } } func (this *box) Focused () bool { hierarchy := this.getHierarchy() if hierarchy == nil { return false } return hierarchy.isFocused(this.outer) } func (this *box) SetFocusable (focusable bool) { if this.focusable == focusable { return } this.focusable = focusable if !focusable { this.SetFocused(false) } } // ----- 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) this.handleBorderChange(previousBorderSum, different) case tomo.AttrMinimumSize: if this.attrMinimumSize.Set(attr, user) { this.invalidateMinimum() } case tomo.AttrPadding: if this.attrPadding.Set(attr, user) { this.invalidateLayout() this.invalidateMinimum() } } } func (this *box) unsetAttr (kind tomo.AttrKind, user bool) { switch kind { case tomo.AttrKindColor: if this.attrColor.Unset(user) { this.invalidateDraw() } case tomo.AttrKindTexture: if this.attrTexture.Unset(user) { this.invalidateDraw() } case tomo.AttrKindTextureMode: if this.attrTextureMode.Unset(user) { this.invalidateDraw() } case tomo.AttrKindBorder: previousBorderSum := this.borderSum() different := this.attrBorder.Unset(user) this.handleBorderChange(previousBorderSum, different) case tomo.AttrKindMinimumSize: if this.attrMinimumSize.Unset(user) { this.invalidateMinimum() } case tomo.AttrKindPadding: if this.attrPadding.Unset(user) { this.invalidateLayout() this.invalidateMinimum() } } } func (this *box) unsetAllAttrs (user bool) { // keep this in sync with tomo.AttrKind! this.outer.unsetAttr(tomo.AttrKindColor, user) this.outer.unsetAttr(tomo.AttrKindTexture, user) this.outer.unsetAttr(tomo.AttrKindTextureMode, user) this.outer.unsetAttr(tomo.AttrKindBorder, user) this.outer.unsetAttr(tomo.AttrKindMinimumSize, user) this.outer.unsetAttr(tomo.AttrKindPadding, user) this.outer.unsetAttr(tomo.AttrKindGap, user) this.outer.unsetAttr(tomo.AttrKindTextColor, user) this.outer.unsetAttr(tomo.AttrKindDotColor, user) this.outer.unsetAttr(tomo.AttrKindFace, user) this.outer.unsetAttr(tomo.AttrKindWrap, user) this.outer.unsetAttr(tomo.AttrKindAlign, user) this.outer.unsetAttr(tomo.AttrKindOverflow, user) this.outer.unsetAttr(tomo.AttrKindLayout, user) } func (this *box) setBounds (bounds image.Rectangle) { if this.bounds == bounds { return } this.bounds = bounds this.invalidateLayout() } 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 } // ----- 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) } func (this *box) OnMouseMove (callback func() bool) event.Cookie { return this.on.mouseMove.Connect(callback) } func (this *box) OnButtonDown (callback func(input.Button) bool) event.Cookie { return this.on.buttonDown.Connect(callback) } func (this *box) OnButtonUp (callback func(input.Button) bool) event.Cookie { return this.on.buttonUp.Connect(callback) } func (this *box) OnScroll (callback func(float64, float64) bool) event.Cookie { return this.on.scroll.Connect(callback) } func (this *box) OnKeyDown (callback func(input.Key, bool) bool) event.Cookie { return this.on.keyDown.Connect(callback) } func (this *box) OnKeyUp (callback func(input.Key, bool) bool) event.Cookie { return this.on.keyUp.Connect(callback) } func (this *box) OnStyleChange (callback func()) event.Cookie { return this.on.styleChange.Connect(callback) } func (this *box) OnIconSetChange (callback func()) event.Cookie { return this.on.iconSetChange.Connect(callback) } func (this *box) handleFocusEnter () { this.focused = true this.invalidateStyle() this.on.focusEnter.Broadcast() } func (this *box) handleFocusLeave () { this.focused = false this.invalidateStyle() this.on.focusLeave.Broadcast() } func (this *box) handleMouseEnter () { this.hovered = true this.invalidateStyle() this.on.mouseEnter.Broadcast() } func (this *box) handleMouseLeave () { this.hovered = false this.invalidateStyle() this.on.mouseLeave.Broadcast() } func (this *box) handleMouseMove () (caught bool) { for _, listener := range this.on.mouseMove.Listeners() { if listener() { caught = true } } return } func (this *box) handleMouseDown (button input.Button) (caught bool) { if button == input.ButtonLeft { this.pressed = true this.invalidateStyle() } for _, listener := range this.on.buttonDown.Listeners() { if listener(button) { caught = true } } return } 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 } } return } func (this *box) handleScroll (x, y float64) (caught bool) { for _, listener := range this.on.scroll.Listeners() { if listener(x, y) { caught = true } } return } func (this *box) handleKeyDown (key input.Key, numberPad bool) (caught bool) { for _, listener := range this.on.keyDown.Listeners() { if listener(key, numberPad) { caught = true } } return } func (this *box) handleKeyUp (key input.Key, numberPad bool) (caught bool) { for _, listener := range this.on.keyUp.Listeners() { if listener(key, numberPad) { caught = true } } return } // -------------------------------------------------------------------------- // func (this *box) Draw (can canvas.Canvas) { if can == nil { return } pen := can.Pen() bounds := this.Bounds() // get values textureMode := tomo.TextureMode(this.attrTextureMode.Value()) texture := this.attrTexture.Value().Texture col := this.attrColor.Value().Color if col == nil { col = color.Transparent } // background if this.transparent() && this.parent != nil { this.parent.drawBackgroundPart(can) } pen.Fill(col) if textureMode == tomo.TextureModeTile && texture != nil { pen.Texture(texture) } pen.Rectangle(bounds) // centered texture if textureMode == tomo.TextureModeCenter && texture != nil { this.centeredTexture(can, texture) } } func (this *box) centeredTexture (can canvas.Canvas, texture canvas.Texture) { pen := can.Pen() bounds := this.Bounds() textureBounds := texture.Bounds() 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) pen.Texture(texture) 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) if area.Empty() { return } if ucolor.Transparent(c) && this.parent != nil { this.parent.drawBackgroundPart(can.SubCanvas(area)) } pen.Fill(c) pen.Rectangle(area) } for _, border := range this.attrBorder.Value() { 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 padding := tomo.Inset(this.attrPadding.Value()) minimum.X += padding.Horizontal() minimum.Y += padding.Vertical() borderSum := this.borderSum() minimum.X += borderSum.Horizontal() minimum.Y += borderSum.Vertical() return minimum } func (this *box) calculateMinimumSize () image.Point { userMinSize := this.attrMinimumSize.Value() minSize := this.outer.contentMinimum() if minSize.X < userMinSize.X { minSize.X = userMinSize.X } if minSize.Y < userMinSize.Y { minSize.Y = userMinSize.Y } return minSize } // 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) this.outer.recursiveLoseCanvas() } func (this *box) doStyle () { this.styleApplicator.apply(this.outer) } func (this *box) setParent (parent parent) { if this.parent != parent && this.Focused() { this.SetFocused(false) } this.parent = parent this.outer.recursiveReApply() } 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() } func (this *box) recursiveLoseCanvas () { this.canvas.Invalidate() } func (this *box) handleBorderChange (previousBorderSum tomo.Inset, different bool) { // 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() } } func (this *box) invalidateStyle () { hierarchy := this.getHierarchy() if hierarchy == nil { return } hierarchy.invalidateStyle(this.outer) } 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 () { this.minSize.Invalidate() if this.parent != nil { this.parent.notifyMinimumSizeChange(this) } } func (this *box) recursiveReApply () { hierarchy := this.getHierarchy() if hierarchy == nil { return } // re-apply styling, icons *if needed* // style hierarchyStyleNonce := this.getStyleNonce() if this.lastStyleNonce != hierarchyStyleNonce { // 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). this.unsetAllAttrs(false) this.lastStyleNonce = hierarchyStyleNonce this.styleApplicator = hierarchy.newStyleApplicator() this.invalidateStyle() this.on.styleChange.Broadcast() } // icons hierarchyIconSetNonce := this.getIconSetNonce() if this.lastIconSetNonce != hierarchyIconSetNonce { this.lastIconSetNonce = hierarchyIconSetNonce this.on.iconSetChange.Broadcast() } } func (this *box) canBeFocused () bool { return this.focusable } func (this *box) boxUnder (point image.Point) anyBox { 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 col := this.attrColor.Value().Color return col == nil || ucolor.Transparent(col) /*&& (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() } func (this *box) getStyleNonce () int { // should panic if not in the tree return this.getHierarchy().getStyleNonce() } func (this *box) getIconSetNonce () int { // should panic if not in the tree return this.getHierarchy().getIconSetNonce() }