From 196afbc2f3befe825d6e1ae59d98efac0c492cda Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 25 Jul 2024 13:01:15 -0400 Subject: [PATCH] Update code for internal system --- internal/system/attribute.go | 38 +++ internal/system/box.go | 441 ++++++++++++++++-------------- internal/system/boxquerier.go | 63 +++++ internal/system/canvasbox.go | 7 +- internal/system/containerbox.go | 247 +++++++++-------- internal/system/event.go | 101 ++++--- internal/system/hierarchy.go | 85 ++++-- internal/system/internal-iface.go | 54 ++-- internal/system/style.go | 46 ++++ internal/system/system.go | 10 +- internal/system/textbox.go | 158 ++++++----- internal/util/util.go | 30 ++ 12 files changed, 785 insertions(+), 495 deletions(-) create mode 100644 internal/system/attribute.go create mode 100644 internal/system/boxquerier.go create mode 100644 internal/system/style.go diff --git a/internal/system/attribute.go b/internal/system/attribute.go new file mode 100644 index 0000000..c760010 --- /dev/null +++ b/internal/system/attribute.go @@ -0,0 +1,38 @@ +package system + +import "git.tebibyte.media/tomo/tomo" + +type attrHierarchy [T tomo.Attr] struct { + style T + user T + userExists bool +} + +func (this *attrHierarchy[T]) SetStyle (style T) (different bool) { + styleEquals := this.style.Equals(style) + this.style = style + return !styleEquals && !this.userExists +} + +func (this *attrHierarchy[T]) SetUser (user T) (different bool) { + userEquals := this.user.Equals(user) + this.user = user + this.userExists = true + return !userEquals +} + +func (this *attrHierarchy[T]) Set (attr T, user bool) (different bool) { + if user { + return this.SetUser(attr) + } else { + return this.SetStyle(attr) + } +} + +func (this *attrHierarchy[T]) Value () T { + if this.userExists { + return this.user + } else { + return this.style + } +} diff --git a/internal/system/box.go b/internal/system/box.go index 5d6ccac..d01d0c5 100644 --- a/internal/system/box.go +++ b/internal/system/box.go @@ -9,66 +9,64 @@ 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 + tags util.Set[string] + role tomo.Role + lastStyleNonce int + lastIconsNonce int + styleApplicator *styleApplicator + minSize util.Memo[image.Point] bounds image.Rectangle - minSize image.Point - userMinSize image.Point innerClippingBounds image.Rectangle - minSizeQueued bool - focusQueued *bool + focusQueued *bool - padding tomo.Inset - border []tomo.Border - color color.Color - texture canvas.Texture - textureMode textureMode + 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 - focused bool 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.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 + 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, - color: color.Transparent, outer: outer, drawer: outer, } @@ -82,15 +80,16 @@ func (this *System) newBox (outer anyBox) *box { box.drawer = box box.outer = box } - box.invalidateMinimum() + 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 } @@ -106,102 +105,13 @@ func (this *box) Bounds () image.Rectangle { } func (this *box) InnerBounds () image.Rectangle { - return this.padding.Apply(this.innerClippingBounds) -} - -func (this *box) MinimumSize () image.Point { - return this.minSize + return tomo.Inset(this.attrPadding.Value()).Apply(this.innerClippingBounds) } 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) { if this.role == role { return } this.role = role @@ -209,6 +119,27 @@ func (this *box) SetRole (role tomo.Role) { 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) SetDNDData (dat data.Data) { this.dndData = dat } @@ -233,6 +164,12 @@ func (this *box) SetFocused (focused bool) { } } +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 @@ -241,22 +178,83 @@ func (this *box) SetFocusable (focusable bool) { } } -func (this *box) Focused () bool { - hierarchy := this.getHierarchy() - if hierarchy == nil { return false } - return hierarchy.isFocused(this.outer) +// ----- 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) + + // 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() + } + + case tomo.AttrMinimumSize: + if this.attrMinimumSize.Set(attr, user) { + this.invalidateMinimum() + } + + case tomo.AttrPadding: + if this.attrPadding.Set(attr, true) { + this.invalidateLayout() + this.invalidateMinimum() + } + } } -func (this *box) Modifiers () input.Modifiers { - hierarchy := this.getHierarchy() - if hierarchy == nil { return input.Modifiers { } } - return hierarchy.getModifiers() +func (this *box) setBounds (bounds image.Rectangle) { + if this.bounds == bounds { return } + this.bounds = bounds + this.invalidateLayout() } -func (this *box) MousePosition () image.Point { - hierarchy := this.getHierarchy() - if hierarchy == nil { return image.Point { } } - return hierarchy.getMousePosition() +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 ---------------------------------------------- // @@ -281,46 +279,62 @@ func (this *box) OnMouseEnter (callback func()) event.Cookie { func (this *box) OnMouseLeave (callback func()) event.Cookie { return this.on.mouseLeave.Connect(callback) } -func (this *box) OnMouseMove (callback func()) event.Cookie { +func (this *box) OnMouseMove (callback func() bool) 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) OnButtonDown (callback func(input.Button) bool) event.Cookie { + return this.on.buttonDown.Connect(callback) } -func (this *box) OnMouseUp (callback func(input.Button)) event.Cookie { - return this.on.mouseUp.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(deltaX, deltaY float64)) event.Cookie { +func (this *box) OnScroll (callback func(float64, float64) bool) event.Cookie { return this.on.scroll.Connect(callback) } -func (this *box) OnKeyDown (callback func(key input.Key, numberPad bool)) event.Cookie { +func (this *box) OnKeyDown (callback func(input.Key, bool) bool) event.Cookie { return this.on.keyDown.Connect(callback) } -func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Cookie { +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) OnIconsChange (callback func()) event.Cookie { - return this.on.iconsChange.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 () { - this.on.mouseMove.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) { +func (this *box) handleMouseDown (button input.Button) (caught bool) { + if button == input.ButtonLeft { + this.pressed = true + this.invalidateStyle() + } + if this.focusable { this.SetFocused(true) } else { @@ -328,29 +342,39 @@ func (this *box) handleMouseDown (button input.Button) { if hierarchy == nil { return } hierarchy.focus(nil) } - for _, listener := range this.on.mouseDown.Listeners() { - listener(button) + for _, listener := range this.on.buttonDown.Listeners() { + if listener(button) { caught = true } } + return } -func (this *box) handleMouseUp (button input.Button) { - for _, listener := range this.on.mouseUp.Listeners() { - listener(button) +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) { +func (this *box) handleScroll (x, y float64) (caught bool) { for _, listener := range this.on.scroll.Listeners() { - listener(x, y) + if listener(x, y) { caught = true } } + return } -func (this *box) handleKeyDown (key input.Key, numberPad bool) { +func (this *box) handleKeyDown (key input.Key, numberPad bool) (caught bool) { for _, listener := range this.on.keyDown.Listeners() { - listener(key, numberPad) + if listener(key, numberPad) { caught = true } } + return } -func (this *box) handleKeyUp (key input.Key, numberPad bool) { +func (this *box) handleKeyUp (key input.Key, numberPad bool) (caught bool) { for _, listener := range this.on.keyUp.Listeners() { - listener(key, numberPad) + if listener(key, numberPad) { caught = true } } + return } // -------------------------------------------------------------------------- // @@ -359,19 +383,25 @@ func (this *box) Draw (can canvas.Canvas) { 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 - pen.Fill(this.color) - if this.textureMode == textureModeTile { - pen.Texture(this.texture) - } 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 this.textureMode == textureModeCenter && this.texture != nil { - textureBounds := this.texture.Bounds() + if textureMode == tomo.TextureModeCenter && texture != nil { + textureBounds := texture.Bounds() textureOrigin := bounds.Min. Add(image.Pt ( @@ -382,7 +412,7 @@ func (this *box) Draw (can canvas.Canvas) { textureBounds.Dy() / 2)) pen.Fill(color.Transparent) - pen.Texture(this.texture) + pen.Texture(texture) pen.Rectangle(textureBounds.Sub(textureBounds.Min).Add(textureOrigin)) } } @@ -394,6 +424,7 @@ func (this *box) drawBorders (can canvas.Canvas) { 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)) } @@ -401,7 +432,7 @@ func (this *box) drawBorders (can canvas.Canvas) { pen.Rectangle(area) } - for _, border := range this.border { + for _, border := range this.attrBorder.Value() { rectangle ( bounds.Min.X, bounds.Min.Y, @@ -433,26 +464,29 @@ func (this *box) drawBorders (can canvas.Canvas) { func (this *box) contentMinimum () image.Point { var minimum image.Point - minimum.X += this.padding.Horizontal() - minimum.Y += this.padding.Vertical() + 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) doMinimumSize () { - this.minSize = this.outer.contentMinimum() - if this.minSize.X < this.userMinSize.X { - this.minSize.X = this.userMinSize.X +func (this *box) calculateMinimumSize () image.Point { + userMinSize := this.attrMinimumSize.Value() + minSize := this.outer.contentMinimum() + if minSize.X < userMinSize.X { + minSize.X = userMinSize.X } - if this.minSize.Y < this.userMinSize.Y { - this.minSize.Y = this.userMinSize.Y + if minSize.Y < userMinSize.Y { + minSize.Y = userMinSize.Y } if this.parent != nil { this.parent.notifyMinimumSizeChange(this) } + return minSize } // var drawcnt int @@ -477,6 +511,10 @@ func (this *box) doLayout () { this.outer.recursiveLoseCanvas() } +func (this *box) doStyle () { + this.styleApplicator.apply(this) +} + func (this *box) setParent (parent parent) { if this.parent != parent && this.Focused() { this.SetFocused(false) @@ -491,10 +529,6 @@ func (this *box) getParent () parent { func (this *box) flushActionQueue () { if this.getHierarchy() == nil { return } - - if this.minSizeQueued { - this.invalidateMinimum() - } if this.focusQueued != nil { this.SetFocused(*this.focusQueued) } @@ -509,6 +543,12 @@ func (this *box) recursiveLoseCanvas () { this.canvas.InvalidateTo(nil) } +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 } @@ -522,12 +562,7 @@ func (this *box) invalidateDraw () { } func (this *box) invalidateMinimum () { - hierarchy := this.getHierarchy() - if hierarchy == nil { - this.minSizeQueued = true - } else { - hierarchy.invalidateMinimum(this.outer) - } + this.minSize.Invalidate() } func (this *box) recursiveReApply () { @@ -538,17 +573,13 @@ func (this *box) recursiveReApply () { // style hierarchyStyleNonce := this.getStyleNonce() if this.lastStyleNonce != hierarchyStyleNonce { - // remove old style + // 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 - 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.styleApplicator = this.getHierarchy().newStyleApplicator() + this.invalidateStyle() this.on.styleChange.Broadcast() } @@ -556,7 +587,7 @@ func (this *box) recursiveReApply () { hierarchyIconsNonce := this.getIconsNonce() if this.lastIconsNonce != hierarchyIconsNonce { this.lastIconsNonce = hierarchyIconsNonce - this.on.iconsChange.Broadcast() + this.on.iconSetChange.Broadcast() } } @@ -564,7 +595,7 @@ func (this *box) canBeFocused () bool { return this.focusable } -func (this *box) boxUnder (point image.Point, category eventCategory) anyBox { +func (this *box) boxUnder (point image.Point) anyBox { if point.In(this.bounds) { return this.outer } else { @@ -583,7 +614,7 @@ func (this *box) propagateAlt (callback func (anyBox) bool) bool { func (this *box) transparent () bool { // TODO uncomment once we have // a way to detect texture transparency - return util.Transparent(this.color) /*&& + return util.Transparent(this.attrColor.Value()) /*&& (this.texture == nil || !this.texture.Opaque())*/ } @@ -593,12 +624,6 @@ func (this *box) getWindow () tomo.Window { 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() diff --git a/internal/system/boxquerier.go b/internal/system/boxquerier.go new file mode 100644 index 0000000..21f1432 --- /dev/null +++ b/internal/system/boxquerier.go @@ -0,0 +1,63 @@ +package system + +import "image" + +type boxQuerier []anyBox + +func (querier boxQuerier) Len () int { + return len(querier) +} + +func (querier boxQuerier) MinimumSize (index int) image.Point { + if box, ok := querier.box(index); ok { + return box.minimumSize() + } + return image.Point { } +} + +func (querier boxQuerier) RecommendedWidth (index int, height int) int { + if box, ok := querier.box(index); ok { + if box, ok := box.(anyContentBox); ok { + return box.recommendedWidth(height) + } + } + return 0 +} + +func (querier boxQuerier) RecommendedHeight (index int, width int) int { + if box, ok := querier.box(index); ok { + if box, ok := box.(anyContentBox); ok { + return box.recommendedHeight(width) + } + } + return 0 +} + +func (querier boxQuerier) box (index int) (anyBox, bool) { + if index < 0 || index >= len(querier) { return nil, false } + return querier[index], true +} + +type boxArranger []anyBox + +func (arranger boxArranger) Len () int { + return boxQuerier(arranger).Len() +} + +func (arranger boxArranger) MinimumSize (index int) image.Point { + return boxQuerier(arranger).MinimumSize(index) +} + +func (arranger boxArranger) RecommendedWidth (index int, height int) int { + return boxQuerier(arranger).RecommendedWidth(index, height) +} + +func (arranger boxArranger) RecommendedHeight (index int, width int) int { + return boxQuerier(arranger).RecommendedHeight(index, width) +} + +func (arranger boxArranger) SetBounds (index int, bounds image.Rectangle) { + if box, ok := boxQuerier(arranger).box(index); ok { + box.setBounds(bounds) + } +} diff --git a/internal/system/canvasbox.go b/internal/system/canvasbox.go index d63de6c..cf080f7 100644 --- a/internal/system/canvasbox.go +++ b/internal/system/canvasbox.go @@ -15,10 +15,6 @@ func (this *System) NewCanvasBox () tomo.CanvasBox { return box } -func (this *canvasBox) Box () tomo.Box { - return this -} - func (this *canvasBox) SetDrawer (drawer canvas.Drawer) { this.userDrawer = drawer this.invalidateDraw() @@ -32,7 +28,8 @@ func (this *canvasBox) Draw (can canvas.Canvas) { if can == nil { return } this.box.Draw(can) if this.userDrawer != nil { + padding := tomo.Inset(this.attrPadding.Value()) this.userDrawer.Draw ( - can.SubCanvas(this.padding.Apply(this.innerClippingBounds))) + can.SubCanvas(padding.Apply(this.innerClippingBounds))) } } diff --git a/internal/system/containerbox.go b/internal/system/containerbox.go index 56d410a..aa84b44 100644 --- a/internal/system/containerbox.go +++ b/internal/system/containerbox.go @@ -10,14 +10,16 @@ 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 + contentBounds image.Rectangle + scroll image.Point + mask bool - gap image.Point - children []tomo.Box + attrGap attrHierarchy[tomo.AttrGap] + attrAlign attrHierarchy[tomo.AttrAlign] + attrOverflow attrHierarchy[tomo.AttrOverflow] + attrLayout attrHierarchy[tomo.AttrLayout] + + children []anyBox layout tomo.Layout on struct { @@ -31,37 +33,7 @@ func (this *System) NewContainerBox () tomo.ContainerBox { 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() -} +// ----- public methods ----------------------------------------------------- // func (this *containerBox) ContentBounds () image.Rectangle { return this.contentBounds @@ -73,54 +45,13 @@ func (this *containerBox) ScrollTo (point image.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 } + if util.IndexOf(this.children, box) > -1 { return } box.setParent(this) box.flushActionQueue() @@ -131,7 +62,7 @@ func (this *containerBox) Add (child tomo.Object) { func (this *containerBox) Remove (child tomo.Object) { box := assertAnyBox(child.GetBox()) - index := util.IndexOf(this.children, tomo.Box(box)) + index := util.IndexOf(this.children, box) if index < 0 { return } box.setParent(nil) @@ -142,15 +73,15 @@ func (this *containerBox) Remove (child tomo.Object) { func (this *containerBox) Insert (child, before tomo.Object) { box := assertAnyBox(child.GetBox()) - if util.IndexOf(this.children, tomo.Box(box)) > -1 { return } + if util.IndexOf(this.children, box) > -1 { return } beforeBox := assertAnyBox(before.GetBox()) - index := util.IndexOf(this.children, tomo.Box(beforeBox)) + index := util.IndexOf(this.children, beforeBox) if index < 0 { - this.children = append(this.children, tomo.Box(box)) + this.children = append(this.children, box) } else { - this.children = util.Insert(this.children, index, tomo.Box(box)) + this.children = util.Insert(this.children, index, box) } box.setParent(this) @@ -167,7 +98,7 @@ func (this *containerBox) Clear () { this.invalidateMinimum() } -func (this *containerBox) Length () int { +func (this *containerBox) Len () int { return len(this.children) } @@ -178,14 +109,19 @@ func (this *containerBox) At (index int) tomo.Object { return this.children[index] } -func (this *containerBox) SetLayout (layout tomo.Layout) { - this.layout = layout - this.invalidateLayout() - this.invalidateMinimum() +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 { @@ -198,17 +134,87 @@ func (this *containerBox) Draw (can canvas.Canvas) { } if clipped == nil { continue } pen := clipped.Pen() - pen.Fill(this.color) - pen.Texture(this.texture) + 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() + } + + 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 { + if this.layout == nil || this.attrOverflow.Value().Y { + return this.minSize.Value().Y + } else { + return this.layout.RecommendedHeight(this.layoutHints(), this.boxQuerier(), width) + + this.borderAndPaddingSum().Vertical() + } +} + +func (this *containerBox) recommendedWidth (height int) int { + if this.layout == nil || this.attrOverflow.Value().X { + return this.minSize.Value().X + } else { + return this.layout.RecommendedWidth(this.layoutHints(), this.boxQuerier(), height) + + this.borderAndPaddingSum().Horizontal() + } +} + func (this *containerBox) drawBackgroundPart (can canvas.Canvas) { if can == nil { return } pen := can.Pen() - pen.Fill(this.color) - pen.Texture(this.texture) + + // 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) @@ -245,7 +251,7 @@ func (this *containerBox) getCanvas () canvas.Canvas { func (this *containerBox) notifyMinimumSizeChange (child anyBox) { this.invalidateMinimum() - size := child.MinimumSize() + size := child.minimumSize() bounds := child.Bounds() if bounds.Dx() < size.X || bounds.Dy() < size.Y { this.invalidateLayout() @@ -253,23 +259,27 @@ func (this *containerBox) notifyMinimumSizeChange (child anyBox) { } func (this *containerBox) layoutHints () tomo.LayoutHints { + overflow := this.attrOverflow.Value() + align := this.attrAlign.Value() + gap := image.Point(this.attrGap.Value()) return tomo.LayoutHints { - OverflowX: this.hOverflow, - OverflowY: this.vOverflow, - AlignX: this.hAlign, - AlignY: this.vAlign, - Gap: this.gap, + OverflowX: overflow.X, + OverflowY: overflow.Y, + AlignX: align.X, + AlignY: align.Y, + Gap: gap, } } func (this *containerBox) contentMinimum () image.Point { - minimum := this.box.contentMinimum() + overflow := this.attrOverflow.Value() + 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 } + this.boxQuerier()) + if overflow.X { layoutMinimum.X = 0 } + if overflow.Y { layoutMinimum.Y = 0 } minimum = minimum.Add(layoutMinimum) } return minimum @@ -285,18 +295,19 @@ func (this *containerBox) doLayout () { if this.layout != nil { minimum = this.layout.MinimumSize ( this.layoutHints(), - this.children) + this.boxQuerier()) } innerBounds := this.InnerBounds() + overflow := this.attrOverflow.Value() 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 } + 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 this.layout != nil { layoutHints := this.layoutHints() layoutHints.Bounds = this.contentBounds - this.layout.Arrange(layoutHints, this.children) + this.layout.Arrange(layoutHints, this.boxArranger()) } // build an accurate contentBounds by unioning the bounds of all child @@ -312,7 +323,7 @@ func (this *containerBox) doLayout () { // offset children and contentBounds by scroll for _, box := range this.children { - box.SetBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min)) + assertAnyBox(box).setBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min)) } this.contentBounds = this.contentBounds.Add(this.scroll) @@ -345,6 +356,14 @@ func (this *containerBox) constrainScroll () { } } +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() @@ -367,12 +386,12 @@ func (this *containerBox) recursiveReApply () { } } -func (this *containerBox) boxUnder (point image.Point, category eventCategory) anyBox { +func (this *containerBox) boxUnder (point image.Point) anyBox { if !point.In(this.bounds) { return nil } - if !this.capture[category] { + if !this.mask { for _, box := range this.children { - candidate := box.(anyBox).boxUnder(point, category) + candidate := box.(anyBox).boxUnder(point) if candidate != nil { return candidate } } } @@ -400,6 +419,6 @@ func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool { return true } -func (this *containerBox) captures (category eventCategory) bool { - return this.capture[category] +func (this *containerBox) masks () bool { + return this.mask } diff --git a/internal/system/event.go b/internal/system/event.go index 91509e9..62d70fa 100644 --- a/internal/system/event.go +++ b/internal/system/event.go @@ -3,6 +3,10 @@ package system import "image" import "git.tebibyte.media/tomo/tomo/input" +// TODO: redo all of this because there are new event propogation rules +// TODO: once go v1.23 comes out, replace the explicit iterator calls here with +// range loops + // HandleFocusChange sets whether or not the window containing this Hierarchy // has input focus. func (this *Hierarchy) HandleFocusChange (focused bool) { @@ -20,14 +24,22 @@ func (this *Hierarchy) HandleModifiers (modifiers input.Modifiers) { // event which triggers this comes with modifier key information, // HandleModifiers must be called *before* HandleKeyDown. func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) { + caught := false + this.keyboardTargets(func (target anyBox) bool { + if target.handleKeyDown(key, numberPad) { + caught = true + return false + } + return true + }) + if caught { return } + if key == input.KeyTab && this.modifiers.Alt { if this.modifiers.Shift { this.focusPrevious() } else { this.focusNext() } - } else if target := this.keyboardTarget(); target != nil { - target.handleKeyDown(key, numberPad) } } @@ -35,63 +47,72 @@ func (this *Hierarchy) HandleKeyDown (key input.Key, numberPad bool) { // which triggers this comes with modifier key information, HandleModifiers must // be called *before* HandleKeyUp. func (this *Hierarchy) HandleKeyUp (key input.Key, numberPad bool) { - if target := this.keyboardTarget(); target != nil { - target.handleKeyUp(key, numberPad) - } + this.keyboardTargets(func (target anyBox) bool { + if target.handleKeyUp(key, numberPad) { + return false + } + return true + }) } -// HandleMouseDown sends a mouse down event to the Box positioned underneath the -// mouse cursor and marks it as being "dragged" by that mouse button. If the -// event which triggers this comes with mouse position information, -// HandleMouseMove must be called *before* HandleMouseDown. +// HandleMouseDown sends a mouse down event to the Boxes positioned underneath +// the mouse cursor and marks them as being "dragged" by that mouse button, +// starting at the first Box to mask events and ending at the first box to +// catch the event. If the event which triggers this comes with mouse position +// information, HandleMouseMove must be called *before* HandleMouseDown. func (this *Hierarchy) HandleMouseDown (button input.Button) { - underneath := this.boxUnder(this.mousePosition, eventCategoryMouse) - this.drags[button] = underneath - if underneath != nil { - underneath.handleMouseDown(button) - } + boxes := []anyBox { } + this.boxesUnder(this.mousePosition)(func (box anyBox) bool { + boxes = append(boxes, box) + return !box.handleMouseDown(button) + }) + this.drags[button] = boxes } -// HandleMouseUp sends a mouse up event to the Box currently being "dragged" by -// the specified mouse button, and marks it as being "not dragged" by that mouse -// button. If the event which triggers this comes with mouse position +// HandleMouseUp sends a mouse up event to the Boxes currently being "dragged" +// by the specified mouse button, and marks them as being "not dragged" by that +// mouse button. If the event which triggers this comes with mouse position // information, HandleMouseMove must be caleld *before* HandleMouseUp func (this *Hierarchy) HandleMouseUp (button input.Button) { - dragging := this.drags[button] - this.drags[button] = nil - if dragging != nil { - dragging.handleMouseUp(button) + for _, box := range this.drags[button] { + box.handleMouseUp(button) } + this.drags[button] = nil } // HandleMouseMove sends a mouse move event to any Boxes currently being -// "dragged" by a mouse button. If none are, it sends the event to the Box which -// is underneath the mouse pointer. +// "dragged" by a mouse button. If none are, it sends the event to the Boxes +// which are underneath the mouse pointer, starting at the first Box to mask +// events and ending at the first box to catch the event. func (this *Hierarchy) HandleMouseMove (position image.Point) { if this.mousePosition == position { return } this.mousePosition = position - handled := false - for _, child := range this.drags { - if child == nil { continue } - child.handleMouseMove() - handled = true - } - - underneath := this.boxUnder(position, eventCategoryMouse) - if underneath != nil { - this.hover(underneath) - if !handled { - underneath.handleMouseMove() + dragged := false + for _, dragSet := range this.drags { + for _, box := range dragSet { + if box.handleMouseMove() { break } } + dragged = true + } + if dragged { return } + + // TODO we can hover over multiple boxes at once. however, any way of + // detecting this involves several slice allocations every time we + // process a MouseMove event. perhaps we just ought to suck it up and do + // it. + box := this.boxUnder(position) + if box != nil { + this.hover(box) + box.handleMouseMove() } } // HandleScroll sends a scroll event to the Box currently underneath the mouse -// cursor. +// cursor. If the event which triggers this comes with mouse position +// information, HandleMouseMove must be called *before* HandleScroll. func (this *Hierarchy) HandleScroll (x, y float64) { - underneath := this.boxUnder(this.mousePosition, eventCategoryScroll) - if underneath != nil { - underneath.handleScroll(x, y) - } + this.boxesUnder(this.mousePosition)(func (box anyBox) bool { + return !box.handleScroll(x, y) + }) } diff --git a/internal/system/hierarchy.go b/internal/system/hierarchy.go index 73a5337..a1ffb24 100644 --- a/internal/system/hierarchy.go +++ b/internal/system/hierarchy.go @@ -21,10 +21,10 @@ type Hierarchy struct { modifiers input.Modifiers mousePosition image.Point - drags [10]anyBox + drags [10][]anyBox minimumSize image.Point - - needMinimum util.Set[anyBox] + + needStyle util.Set[anyBox] needLayout util.Set[anyBox] needDraw util.Set[anyBox] needRedo bool @@ -52,7 +52,7 @@ func (this *System) NewHierarchy (link WindowLink) *Hierarchy { hierarchy := &Hierarchy { system: this, link: link, - needMinimum: make(util.Set[anyBox]), + needStyle: make(util.Set[anyBox]), needLayout: make(util.Set[anyBox]), needDraw: make(util.Set[anyBox]), } @@ -95,6 +95,16 @@ func (this *Hierarchy) MinimumSize () image.Point { return this.minimumSize } +// Modifiers returns the current modifier keys being held. +func (this *Hierarchy) Modifiers () input.Modifiers { + return this.modifiers +} + +// MousePosition returns the current mouse position. +func (this *Hierarchy) MousePosition () image.Point { + return this.mousePosition +} + // AfterEvent should be called at the end of every event cycle. func (this *Hierarchy) AfterEvent () { if this.canvas == nil { return } @@ -104,7 +114,7 @@ func (this *Hierarchy) AfterEvent () { childBounds := this.canvas.Bounds() childBounds = childBounds.Sub(childBounds.Min) if this.root != nil { - this.root.SetBounds(childBounds) + this.root.setBounds(childBounds) } // full relayout/redraw @@ -116,8 +126,8 @@ func (this *Hierarchy) AfterEvent () { return } - for len(this.needMinimum) > 0 { - this.needMinimum.Pop().doMinimumSize() + for len(this.needStyle) > 0 { + this.needStyle.Pop().doStyle() } if !this.minimumClean { this.doMinimumSize() @@ -146,7 +156,7 @@ func (this *Hierarchy) setStyle () { if this.root != nil { this.root.recursiveReApply() } } -func (this *Hierarchy) setIcons () { +func (this *Hierarchy) setIconSet () { if this.root != nil { this.root.recursiveReApply() } } @@ -158,7 +168,7 @@ func (this *Hierarchy) getWindow () tomo.Window { return this.link.GetWindow() } -func (this *Hierarchy) getStyle () tomo.Style { +func (this *Hierarchy) getStyle () *tomo.Style { return this.system.style } @@ -186,8 +196,8 @@ func (this *Hierarchy) notifyMinimumSizeChange (anyBox) { this.minimumClean = false } -func (this *Hierarchy) invalidateMinimum (box anyBox) { - this.needMinimum.Add(box) +func (this *Hierarchy) invalidateStyle (box anyBox) { + this.needStyle.Add(box) } func (this *Hierarchy) invalidateDraw (box anyBox) { @@ -235,35 +245,52 @@ func (this *Hierarchy) anyFocused () bool { return this.focused != nil } -func (this *Hierarchy) boxUnder (point image.Point, category eventCategory) anyBox { - if this.root == nil { return nil } - return this.root.boxUnder(point, category) -} - -func (this *Hierarchy) captures (eventCategory) bool { +func (this *Hierarchy) masks () bool { return false } -func (this *Hierarchy) keyboardTarget () anyBox { +func (this *Hierarchy) boxUnder (point image.Point) anyBox { + if this.root == nil { return nil } + return this.root.boxUnder(point) +} + +func (this *Hierarchy) parents (box anyBox) func (func (anyBox) bool) { + return func (yield func (anyBox) bool) { + for box != nil && yield(box) { + parent, ok := box.getParent().(anyBox) + if !ok { break } + box = parent + } + } +} + +func (this *Hierarchy) boxesUnder (point image.Point) func (func (anyBox) bool) { + return this.parents(this.boxUnder(point)) +} + +func (this *Hierarchy) keyboardTargets (yield func (anyBox) bool) { focused := this.focused - if focused == nil { return nil } - parent := focused.getParent() + if focused == nil { return } + this.parents(this.considerMaskingParents(focused))(yield) +} + +func (this *Hierarchy) considerMaskingParents (box anyBox) anyBox { + parent := box.getParent() for { parentBox, ok := parent.(anyBox) if !ok { break } - if parent.captures(eventCategoryKeyboard) { + if parent.masks() { return parentBox } parent = parentBox.getParent() } - - return focused + return box } func (this *Hierarchy) focusNext () { found := !this.anyFocused() focused := false - this.propagateAlt (func (box anyBox) bool { + this.propagateAlt(func (box anyBox) bool { if found { // looking for the next box to select if box.canBeFocused() { @@ -287,7 +314,7 @@ func (this *Hierarchy) focusNext () { func (this *Hierarchy) focusPrevious () { var behind anyBox - this.propagate (func (box anyBox) bool { + this.propagate(func (box anyBox) bool { if box == this.focused { return false } @@ -319,8 +346,14 @@ func (this *Hierarchy) doMinimumSize () { this.minimumSize = image.Point { } if this.root != nil { - this.minimumSize = this.root.MinimumSize() + this.minimumSize = this.root.minimumSize() } this.link.NotifyMinimumSizeChange() } + +func (this *Hierarchy) newStyleApplicator () *styleApplicator { + return &styleApplicator { + style: this.getStyle(), + } +} diff --git a/internal/system/internal-iface.go b/internal/system/internal-iface.go index c3edf55..80e7009 100644 --- a/internal/system/internal-iface.go +++ b/internal/system/internal-iface.go @@ -5,14 +5,6 @@ import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/canvas" -// eventCategory lists kinds of Tomo events. -type eventCategory int; const ( - eventCategoryDND eventCategory = iota - eventCategoryMouse - eventCategoryScroll - eventCategoryKeyboard -) - // parent is any hierarchical type which contains other boxes. This can be a // Hierarchy, containerBox, etc. type parent interface { @@ -27,9 +19,8 @@ type parent interface { // given Canvas, filling the Canvas's entire bounds. The origin (0, 0) // of the given Canvas is assumed to be the same as the parent's canvas. drawBackgroundPart (canvas.Canvas) - // captures returns whether or not this parent captures the given event - // category. - captures (eventCategory) bool + // catches returns whether or not this parent masks events. + masks () bool } // anyBox is any tomo.Box type that is implemented by this package. @@ -44,10 +35,10 @@ type anyBox interface { // doDraw re-paints the anyBox onto its currently held Canvas non-recursively // doLayout re-calculates the layout of the anyBox non-recursively - // doMinimumSize re-calculates the minimum size of the anyBox non-recursively - doDraw () - doLayout () - doMinimumSize () + // doStyle re-applies the box's style non-recursively + doDraw () + doLayout () + doStyle () // flushActionQueue performs any queued actions, like invalidating the // minimum size or grabbing input focus. @@ -62,7 +53,9 @@ type anyBox interface { // to check whether they have an outdated style or icon set, and if so, // update it and trigger the appropriate event broadcasters. recursiveReApply () - + + // minimumSize returns the box's minimum size + minimumSize () image.Point // contentMinimum returns the minimum dimensions of this box's content contentMinimum () image.Point // canBeFocused returns whether or not this anyBox is capable of holding @@ -71,12 +64,18 @@ type anyBox interface { // boxUnder returns the anyBox under the mouse pointer. It can be this // anyBox, one of its children (if applicable). It must return nil if // the mouse pointer is outside of this anyBox's bounds. - boxUnder (image.Point, eventCategory) anyBox + boxUnder (image.Point) anyBox // transparent returns whether or not this anyBox contains transparent // pixels or not, and thus needs its parent's backround to be painted // underneath it. transparent () bool + // setBounds sets the box's bounds. + setBounds (image.Rectangle) + // setAttr sets an attribute at the user or style level depending + // on the value of user. + setAttr (attr tomo.Attr, user bool) + // propagate recursively calls a function on this anyBox, and all of its // children (if applicable) The normal propagate behavior calls the // callback on all children before calling it on this anyBox, and @@ -92,12 +91,21 @@ type anyBox interface { // handleDndDrop (data.Data) handleMouseEnter () handleMouseLeave () - handleMouseMove () - handleMouseDown (input.Button) - handleMouseUp (input.Button) - handleScroll (float64, float64) - handleKeyDown (input.Key, bool) - handleKeyUp (input.Key, bool) + handleMouseMove () bool + handleMouseDown (input.Button) bool + handleMouseUp (input.Button) bool + handleScroll (float64, float64) bool + handleKeyDown (input.Key, bool) bool + handleKeyUp (input.Key, bool) bool +} + +type anyContentBox interface { + anyBox + + // recommendedWidth returns the recommended width for a given height. + recommendedWidth (int) int + // recommendedHeight returns the recommended height for a given height. + recommendedHeight (int) int } func assertAnyBox (unknown tomo.Box) anyBox { diff --git a/internal/system/style.go b/internal/system/style.go new file mode 100644 index 0000000..df7e46a --- /dev/null +++ b/internal/system/style.go @@ -0,0 +1,46 @@ +package system + +import "git.tebibyte.media/tomo/tomo" + +type styleApplicator struct { + style *tomo.Style + role tomo.Role + rules []*tomo.Rule +} + +func (this *styleApplicator) apply (box anyBox) { + if box.Role() != this.role { + // the role has changed, so re-cache the list of rules + this.rules = make([]*tomo.Rule, 4) + for _, rule := range this.style.Rules { + role := box.Role() + // blank fields match anything + if rule.Role.Package == "" { role.Package = "" } + if rule.Role.Object == "" { role.Object = "" } + if rule.Role == role { + this.rules = append(this.rules, &rule) + } + } + } + + // compile list of attributes by searching through the cached ruleset + attrs := make(tomo.AttrSet) + for _, rule := range this.rules { + satisifed := true + for _, tag := range rule.Tags { + if !box.Tag(tag) { + satisifed = false + break + } + } + + if satisifed { + attrs.MergeOver(rule.Set) + } + } + + // apply that list of attributes + for _, attr := range attrs { + box.setAttr(attr, false) + } +} diff --git a/internal/system/system.go b/internal/system/system.go index ce6bb4d..93f55ae 100644 --- a/internal/system/system.go +++ b/internal/system/system.go @@ -11,7 +11,7 @@ import "git.tebibyte.media/tomo/backend/internal/util" type System struct { link BackendLink - style tomo.Style + style *tomo.Style styleNonce int iconsNonce int @@ -45,7 +45,7 @@ func New (link BackendLink) *System { // SetStyle sets the tomo.Style that is applied to objects, and notifies them // that the style has changed. -func (this *System) SetStyle (style tomo.Style) { +func (this *System) SetStyle (style *tomo.Style) { this.style = style this.styleNonce ++ for hierarchy := range this.hierarchies { @@ -53,11 +53,11 @@ func (this *System) SetStyle (style tomo.Style) { } } -// SetIcons notifies objects that the icons have changed. -func (this *System) SetIcons (icons tomo.Icons) { +// SetIconSet notifies objects that the icons have changed. +func (this *System) SetIconSet (iconSet tomo.IconSet) { this.iconsNonce ++ for hierarchy := range this.hierarchies { - hierarchy.setIcons() + hierarchy.setIconSet() } } diff --git a/internal/system/textbox.go b/internal/system/textbox.go index ffbadca..4986db3 100644 --- a/internal/system/textbox.go +++ b/internal/system/textbox.go @@ -2,7 +2,6 @@ package system import "image" import "image/color" -import "golang.org/x/image/font" import "git.tebibyte.media/tomo/tomo" import "golang.org/x/image/math/fixed" import "git.tebibyte.media/tomo/typeset" @@ -14,22 +13,21 @@ import "git.tebibyte.media/tomo/tomo/canvas" type textBox struct { *box - hOverflow, vOverflow bool contentBounds image.Rectangle scroll image.Point - text string - textColor color.Color - face font.Face - wrap bool - hAlign tomo.Align - vAlign tomo.Align + attrTextColor attrHierarchy[tomo.AttrTextColor] + attrDotColor attrHierarchy[tomo.AttrDotColor] + attrFace attrHierarchy[tomo.AttrFace] + attrWrap attrHierarchy[tomo.AttrWrap] + attrAlign attrHierarchy[tomo.AttrAlign] + attrOverflow attrHierarchy[tomo.AttrOverflow] + text string selectable bool selecting bool selectStart int dot text.Dot - dotColor color.Color drawer typeset.Drawer @@ -40,20 +38,12 @@ type textBox struct { } func (this *System) NewTextBox () tomo.TextBox { - box := &textBox { - textColor: color.Black, - dotColor: color.RGBA { B: 255, G: 255, A: 255 }, - } + box := &textBox { } box.box = this.newBox(box) return box } -func (this *textBox) SetOverflow (horizontal, vertical bool) { - if this.hOverflow == horizontal && this.vOverflow == vertical { return } - this.hOverflow = horizontal - this.vOverflow = vertical - this.invalidateLayout() -} +// ----- public methods ----------------------------------------------------- // func (this *textBox) ContentBounds () image.Rectangle { return this.contentBounds @@ -71,7 +61,7 @@ func (this *textBox) RecommendedHeight (width int) int { func (this *textBox) RecommendedWidth (height int) int { // TODO maybe not the best idea? - return this.MinimumSize().X + return this.minimumSize().X } func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie { @@ -86,40 +76,11 @@ func (this *textBox) SetText (text string) { this.invalidateLayout() } -func (this *textBox) SetTextColor (c color.Color) { - if this.textColor == c { return } - this.textColor = c - this.invalidateDraw() -} - -func (this *textBox) SetFace (face font.Face) { - if this.face == face { return } - this.face = face - this.drawer.SetFace(face) - this.invalidateMinimum() - this.invalidateLayout() -} - -func (this *textBox) SetWrap (wrap bool) { - if this.wrap == wrap { return } - this.drawer.SetWrap(wrap) - this.invalidateMinimum() - this.invalidateLayout() -} - func (this *textBox) SetSelectable (selectable bool) { if this.selectable == selectable { return } this.selectable = selectable } -func (this *textBox) SetDotColor (c color.Color) { - if this.dotColor == c { return } - this.dotColor = c - if !this.dot.Empty() { - this.invalidateDraw() - } -} - func (this *textBox) Select (dot text.Dot) { if !this.selectable { return } if this.dot == dot { return } @@ -138,20 +99,19 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie { return this.on.dotChange.Connect(callback) } -func (this *textBox) SetAlign (x, y tomo.Align) { - if this.hAlign == x && this.vAlign == y { return } - this.hAlign = x - this.vAlign = y - this.drawer.SetAlign(typeset.Align(x), typeset.Align(y)) - this.invalidateDraw() -} +// ----- private methods ---------------------------------------------------- // func (this *textBox) Draw (can canvas.Canvas) { if can == nil { return } + + texture := this.attrTexture.Value().Texture + col := this.attrColor.Value().Color + if col == nil { col = color.Transparent } + this.drawBorders(can) pen := can.Pen() - pen.Fill(this.color) - pen.Texture(this.texture) + pen.Fill(col) + pen.Texture(texture) if this.transparent() && this.parent != nil { this.parent.drawBackgroundPart(can) @@ -162,8 +122,50 @@ func (this *textBox) Draw (can canvas.Canvas) { this.drawDot(can) } - if this.face == nil { return } - this.drawer.Draw(can, this.textColor, this.textOffset()) + if this.attrFace.Value().Face != nil { + textColor := this.attrTextColor.Value().Color + if textColor == nil { textColor = color.Black } + this.drawer.Draw(can, textColor, this.textOffset()) + } +} + +func (this *textBox) setAttr (attr tomo.Attr, user bool) { + switch attr := attr.(type) { + case tomo.AttrTextColor: + if this.attrTextColor.Set(attr, user) && !this.dot.Empty() { + this.invalidateDraw() + } + + case tomo.AttrDotColor: + if this.attrDotColor.Set(attr, user) && !this.dot.Empty() { + this.invalidateDraw() + } + + case tomo.AttrFace: + if this.attrFace.Set(attr, user) { + this.drawer.SetFace(attr.Face) + this.invalidateMinimum() + this.invalidateLayout() + } + + case tomo.AttrWrap: + if this.attrWrap.Set(attr, user) { + this.invalidateMinimum() + this.invalidateLayout() + } + + case tomo.AttrAlign: + if this.attrAlign.Set(attr, user) { + this.invalidateDraw() + } + + case tomo.AttrOverflow: + if this.attrOverflow.Set(attr, user) { + this.invalidateLayout() + } + + default: this.box.setAttr(attr, user) + } } func roundPt (point fixed.Point26_6) image.Point { @@ -175,14 +177,20 @@ func fixPt (point image.Point) fixed.Point26_6 { } func (this *textBox) drawDot (can canvas.Canvas) { - if this.face == nil { return } + if this.attrFace.Value().Face == nil { return } + face := this.attrFace.Value().Face + textColor := this.attrTextColor.Value().Color + dotColor := this.attrDotColor.Value().Color + if textColor == nil { textColor = color.Transparent } + if dotColor == nil { dotColor = color.RGBA { G: 255, B: 255, A: 255 } } + pen := can.Pen() pen.Fill(color.Transparent) - pen.Stroke(this.textColor) + pen.Stroke(textColor) bounds := this.InnerBounds() - metrics := this.face.Metrics() + metrics := face.Metrics() dot := this.dot.Canon() start := this.drawer.PositionAt(dot.Start).Add(fixPt(this.textOffset())) end := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset())) @@ -196,7 +204,7 @@ func (this *textBox) drawDot (can canvas.Canvas) { pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent))) case start.Y == end.Y: - pen.Fill(this.dotColor) + pen.Fill(dotColor) pen.StrokeWeight(0) pen.Rectangle(image.Rectangle { Min: roundPt(start.Add(ascent)), @@ -204,7 +212,7 @@ func (this *textBox) drawDot (can canvas.Canvas) { }) default: - pen.Fill(this.dotColor) + pen.Fill(dotColor) pen.StrokeWeight(0) rect := image.Rectangle { @@ -241,35 +249,37 @@ func (this *textBox) handleFocusLeave () { this.box.handleFocusLeave() } -func (this *textBox) handleMouseDown (button input.Button) { +func (this *textBox) handleMouseDown (button input.Button) bool { if button == input.ButtonLeft { index := this.runeUnderMouse() this.selectStart = index this.selecting = true this.Select(text.Dot { Start: this.selectStart, End: index }) } - this.box.handleMouseDown(button) + return this.box.handleMouseDown(button) } -func (this *textBox) handleMouseUp (button input.Button) { +func (this *textBox) handleMouseUp (button input.Button) bool { if button == input.ButtonLeft && this.selecting { index := this.runeUnderMouse() this.selecting = false this.Select(text.Dot { Start: this.selectStart, End: index }) } - this.box.handleMouseUp(button) + return this.box.handleMouseUp(button) } -func (this *textBox) handleMouseMove () { +func (this *textBox) handleMouseMove () bool { if this.selecting { index := this.runeUnderMouse() this.Select(text.Dot { Start: this.selectStart, End: index }) } - this.box.handleMouseMove() + return this.box.handleMouseMove() } func (this *textBox) runeUnderMouse () int { - position := this.MousePosition().Sub(this.textOffset()) + window := this.Window() + if window == nil { return 0 } + position := window.MousePosition().Sub(this.textOffset()) return this.drawer.AtPosition(fixPt(position)) } @@ -281,10 +291,10 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle { func (this *textBox) contentMinimum () image.Point { minimum := this.drawer.MinimumSize() - if this.hOverflow || this.wrap { + if this.attrOverflow.Value().X || bool(this.attrWrap.Value()) { minimum.X = this.drawer.Em().Round() } - if this.vOverflow { + if this.attrOverflow.Value().Y { minimum.Y = this.drawer.LineHeight().Round() } diff --git a/internal/util/util.go b/internal/util/util.go index 3f490dc..58caf97 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,5 +1,6 @@ package util +import "io" import "image/color" // IndexOf returns the index of needle within haystack. If needle does not exist @@ -99,3 +100,32 @@ func (this *Memo[T]) InvalidateTo (value T) { this.Invalidate() this.cache = value } + +// Cycler stores a value and an accompanying io.Closer. When the value is set, +// the closer associated with the previous value is closed. +type Cycler[T any] struct { + value T + closer io.Closer +} + +// Value returns the cycler's value. +func (this *Cycler[T]) Value () T { + return this.value +} + +// Set sets the value and associated closer, closing the previous one. +func (this *Cycler[T]) Set (value T, closer io.Closer) (err error) { + if this.closer != nil { + err = this.closer.Close() + } + this.value = value + this.closer = closer + return err +} + +// Close closes the associated closer early. +func (this *Cycler[T]) Close () error { + err := this.closer.Close() + this.closer = nil + return err +}