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/tomo/backend/internal/util" type box struct { system *System parent parent outer anyBox tags util.Set[string] role tomo.Role lastStyleNonce int lastIconSetNonce int styleApplicator *styleApplicator minSize util.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 util.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(util.Set[string]), } box.attrColor.SetFallback(tomo.AColor(color.Transparent)) box.canvas = util.NewMemo (func () canvas.Canvas { if box.parent == nil { return nil } parentCanvas := box.parent.getCanvas() if parentCanvas == nil { return nil } return parentCanvas.SubCanvas(box.bounds) }) if outer == nil { box.drawer = box box.outer = box } box.minSize = util.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) { if on { this.tags.Add(tag) } else { delete(this.tags, tag) } } 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) 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 { 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 util.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 () { if this.getHierarchy() == 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.lastStyleNonce = hierarchyStyleNonce this.styleApplicator = this.getHierarchy().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 || util.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() }