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 textureMode int; const ( textureModeTile textureMode = iota textureModeCenter ) type box struct { system *System parent parent outer anyBox role tomo.Role styleCookie event.Cookie lastStyleNonce int lastIconsNonce int bounds image.Rectangle minSize image.Point userMinSize image.Point innerClippingBounds image.Rectangle minSizeQueued bool focusQueued *bool padding tomo.Inset border []tomo.Border color color.Color texture canvas.Texture textureMode textureMode dndData data.Data dndAccept []data.Mime focused bool focusable 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.FuncBroadcaster mouseDown event.Broadcaster[func (input.Button)] mouseUp event.Broadcaster[func (input.Button)] scroll event.Broadcaster[func (float64, float64)] keyDown event.Broadcaster[func (input.Key, bool)] keyUp event.Broadcaster[func (input.Key, bool)] styleChange event.FuncBroadcaster iconsChange event.FuncBroadcaster } } func (this *System) newBox (outer anyBox) *box { box := &box { system: this, color: color.Transparent, outer: outer, drawer: outer, } 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.invalidateMinimum() return box } func (this *System) NewBox () tomo.Box { return this.newBox(nil) } 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 this.padding.Apply(this.innerClippingBounds) } func (this *box) MinimumSize () image.Point { return this.minSize } func (this *box) Role () tomo.Role { return this.role } func (this *box) borderSum () tomo.Inset { sum := tomo.Inset { } for _, border := range this.border { 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() sum[0] += this.padding[0] sum[1] += this.padding[1] sum[2] += this.padding[2] sum[3] += this.padding[3] return sum } func (this *box) SetBounds (bounds image.Rectangle) { if this.bounds == bounds { return } this.bounds = bounds this.invalidateLayout() } func (this *box) SetColor (c color.Color) { if c == nil { c = color.Transparent } if this.color == c { return } this.color = c this.invalidateDraw() } func (this *box) SetTextureTile (texture canvas.Texture) { if this.texture == texture && this.textureMode == textureModeTile { return } this.textureMode = textureModeTile this.texture = texture this.invalidateDraw() } func (this *box) SetTextureCenter (texture canvas.Texture) { if this.texture == texture && this.textureMode == textureModeCenter { return } this.texture = texture this.textureMode = textureModeCenter this.invalidateDraw() } func (this *box) SetBorder (borders ...tomo.Border) { previousBorderSum := this.borderSum() previousBorders := this.border this.border = borders // only invalidate the layout if the border is sized differently if this.borderSum() != previousBorderSum { this.invalidateLayout() this.invalidateMinimum() return } // if the border takes up the same amount of space, only invalidate the // drawing if it looks different for index, newBorder := range this.border { different := index >= len(previousBorders) || newBorder != previousBorders[index] if different { this.invalidateDraw() return } } } func (this *box) SetMinimumSize (size image.Point) { if this.userMinSize == size { return } this.userMinSize = size this.invalidateMinimum() } func (this *box) SetPadding (padding tomo.Inset) { if this.padding == padding { return } this.padding = padding this.invalidateLayout() this.invalidateMinimum() } func (this *box) SetRole (role tomo.Role) { this.role = role } 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) SetFocusable (focusable bool) { if this.focusable == focusable { return } this.focusable = focusable if !focusable { this.SetFocused(false) } } func (this *box) Focused () bool { hierarchy := this.getHierarchy() if hierarchy == nil { return false } return hierarchy.isFocused(this.outer) } func (this *box) Modifiers () input.Modifiers { hierarchy := this.getHierarchy() if hierarchy == nil { return input.Modifiers { } } return hierarchy.getModifiers() } func (this *box) MousePosition () image.Point { hierarchy := this.getHierarchy() if hierarchy == nil { return image.Point { } } return hierarchy.getMousePosition() } // ----- 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()) event.Cookie { return this.on.mouseMove.Connect(callback) } func (this *box) OnMouseDown (callback func(input.Button)) event.Cookie { return this.on.mouseDown.Connect(callback) } func (this *box) OnMouseUp (callback func(input.Button)) event.Cookie { return this.on.mouseUp.Connect(callback) } func (this *box) OnScroll (callback func(deltaX, deltaY float64)) event.Cookie { return this.on.scroll.Connect(callback) } func (this *box) OnKeyDown (callback func(key input.Key, numberPad bool)) event.Cookie { return this.on.keyDown.Connect(callback) } func (this *box) OnKeyUp (callback func(key input.Key, numberPad 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) OnIconsChange (callback func()) event.Cookie { return this.on.iconsChange.Connect(callback) } func (this *box) handleFocusEnter () { this.on.focusEnter.Broadcast() } func (this *box) handleFocusLeave () { this.on.focusLeave.Broadcast() } func (this *box) handleMouseEnter () { this.on.mouseEnter.Broadcast() } func (this *box) handleMouseLeave () { this.on.mouseLeave.Broadcast() } func (this *box) handleMouseMove () { this.on.mouseMove.Broadcast() } func (this *box) handleMouseDown (button input.Button) { if this.focusable { this.SetFocused(true) } else { hierarchy := this.getHierarchy() if hierarchy == nil { return } hierarchy.focus(nil) } for _, listener := range this.on.mouseDown.Listeners() { listener(button) } } func (this *box) handleMouseUp (button input.Button) { for _, listener := range this.on.mouseUp.Listeners() { listener(button) } } func (this *box) handleScroll (x, y float64) { for _, listener := range this.on.scroll.Listeners() { listener(x, y) } } func (this *box) handleKeyDown (key input.Key, numberPad bool) { for _, listener := range this.on.keyDown.Listeners() { listener(key, numberPad) } } func (this *box) handleKeyUp (key input.Key, numberPad bool) { for _, listener := range this.on.keyUp.Listeners() { listener(key, numberPad) } } // -------------------------------------------------------------------------- // func (this *box) Draw (can canvas.Canvas) { if can == nil { return } pen := can.Pen() bounds := this.Bounds() // background pen.Fill(this.color) if this.textureMode == textureModeTile { pen.Texture(this.texture) } if this.transparent() && this.parent != nil { this.parent.drawBackgroundPart(can) } pen.Rectangle(bounds) // centered texture if this.textureMode == textureModeCenter && this.texture != nil { textureBounds := this.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(this.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 util.Transparent(c) && this.parent != nil { this.parent.drawBackgroundPart(can.SubCanvas(area)) } pen.Fill(c) pen.Rectangle(area) } for _, border := range this.border { 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 minimum.X += this.padding.Horizontal() minimum.Y += this.padding.Vertical() borderSum := this.borderSum() minimum.X += borderSum.Horizontal() minimum.Y += borderSum.Vertical() return minimum } func (this *box) doMinimumSize () { this.minSize = this.outer.contentMinimum() if this.minSize.X < this.userMinSize.X { this.minSize.X = this.userMinSize.X } if this.minSize.Y < this.userMinSize.Y { this.minSize.Y = this.userMinSize.Y } if this.parent != nil { this.parent.notifyMinimumSizeChange(this) } } // 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) 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.minSizeQueued { this.invalidateMinimum() } if this.focusQueued != nil { this.SetFocused(*this.focusQueued) } } func (this *box) recursiveRedo () { this.doLayout() this.doDraw() } func (this *box) recursiveLoseCanvas () { this.canvas.InvalidateTo(nil) } 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 () { hierarchy := this.getHierarchy() if hierarchy == nil { this.minSizeQueued = true } else { hierarchy.invalidateMinimum(this.outer) } } func (this *box) recursiveReApply () { if this.getHierarchy() == nil { return } // re-apply styling, icons *if needed* // style hierarchyStyleNonce := this.getStyleNonce() if this.lastStyleNonce != hierarchyStyleNonce { // remove old style this.lastStyleNonce = hierarchyStyleNonce if this.styleCookie != nil { this.styleCookie.Close() this.styleCookie = nil } // apply new one if style := this.getStyle(); style != nil { this.styleCookie = style.Apply(this.outer) } this.on.styleChange.Broadcast() } // icons hierarchyIconsNonce := this.getIconsNonce() if this.lastIconsNonce != hierarchyIconsNonce { this.lastIconsNonce = hierarchyIconsNonce this.on.iconsChange.Broadcast() } } func (this *box) canBeFocused () bool { return this.focusable } func (this *box) boxUnder (point image.Point, category eventCategory) 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 return util.Transparent(this.color) /*&& (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) getStyle () tomo.Style { hierarchy := this.getHierarchy() if hierarchy == nil { return nil } return hierarchy.getStyle() } 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) getIconsNonce () int { // should panic if not in the tree return this.getHierarchy().getIconsNonce() }