diff --git a/internal/system/attribute.go b/internal/system/attribute.go index c760010..aacab29 100644 --- a/internal/system/attribute.go +++ b/internal/system/attribute.go @@ -1,26 +1,48 @@ package system import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/backend/internal/util" type attrHierarchy [T tomo.Attr] struct { - style T - user T - userExists bool + fallback T + style util.Optional[T] + user util.Optional[T] +} + +func (this *attrHierarchy[T]) SetFallback (fallback T) { + this.fallback = fallback } func (this *attrHierarchy[T]) SetStyle (style T) (different bool) { - styleEquals := this.style.Equals(style) - this.style = style - return !styleEquals && !this.userExists + styleEquals := false + if previous, ok := this.style.Value(); ok { + styleEquals = previous.Equals(style) + } + this.style.Set(style) + return !styleEquals && !this.user.Exists() +} + +func (this *attrHierarchy[T]) UnsetStyle () (different bool) { + different = this.style.Exists() + this.style.Unset() + return different } func (this *attrHierarchy[T]) SetUser (user T) (different bool) { - userEquals := this.user.Equals(user) - this.user = user - this.userExists = true + userEquals := false + if previous, ok := this.user.Value(); ok { + userEquals = previous.Equals(user) + } + this.user.Set(user) return !userEquals } +func (this *attrHierarchy[T]) UnsetUser () (different bool) { + different = this.user.Exists() + this.user.Unset() + return different +} + func (this *attrHierarchy[T]) Set (attr T, user bool) (different bool) { if user { return this.SetUser(attr) @@ -29,10 +51,20 @@ func (this *attrHierarchy[T]) Set (attr T, user bool) (different bool) { } } -func (this *attrHierarchy[T]) Value () T { - if this.userExists { - return this.user +func (this *attrHierarchy[T]) Unset (user bool) (different bool) { + if user { + return this.UnsetUser() } else { - return this.style + return this.UnsetStyle() + } +} + +func (this *attrHierarchy[T]) Value () T { + if user, ok := this.user.Value(); ok { + return user + } else if style, ok := this.style.Value(); ok{ + return style + } else { + return this.fallback } } diff --git a/internal/system/box.go b/internal/system/box.go index e67ffe5..6b9dce2 100644 --- a/internal/system/box.go +++ b/internal/system/box.go @@ -14,11 +14,11 @@ type box struct { parent parent outer anyBox - tags util.Set[string] - role tomo.Role - lastStyleNonce int - lastIconsNonce int - styleApplicator *styleApplicator + tags util.Set[string] + role tomo.Role + lastStyleNonce int + lastIconSetNonce int + styleApplicator *styleApplicator minSize util.Memo[image.Point] bounds image.Rectangle @@ -41,7 +41,6 @@ type box struct { focused bool pressed bool - canvas util.Memo[canvas.Canvas] drawer canvas.Drawer @@ -71,6 +70,7 @@ func (this *System) newBox (outer anyBox) *box { 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() @@ -141,6 +141,10 @@ 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 } @@ -201,18 +205,7 @@ func (this *box) setAttr (attr tomo.Attr, user bool) { 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() - } + this.handleBorderChange(previousBorderSum, different) case tomo.AttrMinimumSize: if this.attrMinimumSize.Set(attr, user) { @@ -220,7 +213,42 @@ func (this *box) setAttr (attr tomo.Attr, user bool) { } case tomo.AttrPadding: - if this.attrPadding.Set(attr, true) { + 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() } @@ -531,7 +559,21 @@ func (this *box) recursiveRedo () { } func (this *box) recursiveLoseCanvas () { - this.canvas.InvalidateTo(nil) + 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 () { @@ -578,9 +620,9 @@ func (this *box) recursiveReApply () { } // icons - hierarchyIconsNonce := this.getIconsNonce() - if this.lastIconsNonce != hierarchyIconsNonce { - this.lastIconsNonce = hierarchyIconsNonce + hierarchyIconSetNonce := this.getIconSetNonce() + if this.lastIconSetNonce != hierarchyIconSetNonce { + this.lastIconSetNonce = hierarchyIconSetNonce this.on.iconSetChange.Broadcast() } } @@ -629,7 +671,7 @@ func (this *box) getStyleNonce () int { return this.getHierarchy().getStyleNonce() } -func (this *box) getIconsNonce () int { +func (this *box) getIconSetNonce () int { // should panic if not in the tree - return this.getHierarchy().getIconsNonce() + return this.getHierarchy().getIconSetNonce() } diff --git a/internal/system/containerbox.go b/internal/system/containerbox.go index f7103a0..3c454db 100644 --- a/internal/system/containerbox.go +++ b/internal/system/containerbox.go @@ -186,6 +186,53 @@ func (this *containerBox) setAttr (attr tomo.Attr, user bool) { } } +func (this *containerBox) unsetAttr (kind tomo.AttrKind, user bool) { + switch kind { + case tomo.AttrKindColor: + if this.attrColor.Unset(user) { + this.invalidateTransparentChildren() + this.invalidateDraw() + } + + case tomo.AttrKindTexture: + if this.attrTexture.Unset(user) { + this.invalidateTransparentChildren() + this.invalidateDraw() + } + + case tomo.AttrKindTextureMode: + if this.attrTextureMode.Unset(user) { + this.invalidateTransparentChildren() + this.invalidateDraw() + } + + case tomo.AttrKindGap: + if this.attrGap.Unset(user) { + this.invalidateLayout() + this.invalidateMinimum() + } + + case tomo.AttrKindAlign: + if this.attrAlign.Unset(user) { + this.invalidateLayout() + } + + case tomo.AttrKindOverflow: + if this.attrOverflow.Unset(user) { + this.invalidateLayout() + this.invalidateMinimum() + } + + case tomo.AttrKindLayout: + if this.attrLayout.Unset(user) { + this.invalidateLayout() + this.invalidateMinimum() + } + + default: this.box.unsetAttr(kind, user) + } +} + func (this *containerBox) recommendedHeight (width int) int { layout := this.attrLayout.Value().Layout if layout == nil || !this.attrOverflow.Value().Y { diff --git a/internal/system/hierarchy.go b/internal/system/hierarchy.go index f949eb1..b4181df 100644 --- a/internal/system/hierarchy.go +++ b/internal/system/hierarchy.go @@ -4,6 +4,7 @@ import "image" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/canvas" +import "git.tebibyte.media/tomo/backend/style" import "git.tebibyte.media/tomo/backend/internal/util" // Hierarchy is coupled to a tomo.Window implementation, and manages a tree of @@ -168,16 +169,24 @@ func (this *Hierarchy) getWindow () tomo.Window { return this.link.GetWindow() } -func (this *Hierarchy) getStyle () *tomo.Style { +func (this *Hierarchy) getStyle () *style.Style { return this.system.style } +func (this *Hierarchy) getIconSet () style.IconSet { + return this.system.iconSet +} + +func (this *Hierarchy) getFaceSet () style.FaceSet { + return this.system.faceSet +} + func (this *Hierarchy) getStyleNonce () int { return this.system.styleNonce } -func (this *Hierarchy) getIconsNonce () int { - return this.system.iconsNonce +func (this *Hierarchy) getIconSetNonce () int { + return this.system.iconSetNonce } func (this *Hierarchy) getCanvas () canvas.Canvas { diff --git a/internal/system/internal-iface.go b/internal/system/internal-iface.go index 80e7009..986f207 100644 --- a/internal/system/internal-iface.go +++ b/internal/system/internal-iface.go @@ -75,6 +75,9 @@ type anyBox interface { // setAttr sets an attribute at the user or style level depending // on the value of user. setAttr (attr tomo.Attr, user bool) + // unsetAttr unsets an attribute at the user or style level depending + // on the value of user. + unsetAttr (kind tomo.AttrKind, user bool) // propagate recursively calls a function on this anyBox, and all of its // children (if applicable) The normal propagate behavior calls the diff --git a/internal/system/style.go b/internal/system/style.go index 4617eeb..bf42e43 100644 --- a/internal/system/style.go +++ b/internal/system/style.go @@ -1,11 +1,12 @@ package system import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/backend/style" type styleApplicator struct { - style *tomo.Style + style *style.Style role tomo.Role - rules []tomo.Rule + rules []style.Rule } func (this *styleApplicator) apply (box anyBox) { @@ -25,7 +26,7 @@ func (this *styleApplicator) apply (box anyBox) { } // compile list of attributes by searching through the cached ruleset - attrs := make(tomo.AttrSet) + attrs := make(style.AttrSet) for _, rule := range this.rules { satisifed := true for _, tag := range rule.Tags { diff --git a/internal/system/system.go b/internal/system/system.go index 93f55ae..0ea9bb9 100644 --- a/internal/system/system.go +++ b/internal/system/system.go @@ -2,8 +2,8 @@ package system import "io" import "image" -import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/canvas" +import "git.tebibyte.media/tomo/backend/style" import "git.tebibyte.media/tomo/backend/internal/util" // System is coupled to a tomo.Backend implementation, and manages Hierarchies @@ -11,9 +11,11 @@ import "git.tebibyte.media/tomo/backend/internal/util" type System struct { link BackendLink - style *tomo.Style - styleNonce int - iconsNonce int + style *style.Style + iconSet style.IconSet + faceSet style.FaceSet + styleNonce int + iconSetNonce int hierarchies util.Set[*Hierarchy] } @@ -43,9 +45,9 @@ func New (link BackendLink) *System { } } -// SetStyle sets the tomo.Style that is applied to objects, and notifies them +// SetStyle sets the 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 *style.Style) { this.style = style this.styleNonce ++ for hierarchy := range this.hierarchies { @@ -53,14 +55,21 @@ func (this *System) SetStyle (style *tomo.Style) { } } -// SetIconSet notifies objects that the icons have changed. -func (this *System) SetIconSet (iconSet tomo.IconSet) { - this.iconsNonce ++ +// SetIconSet sets the icon set that provides icon textures, and notifies +// objects that the icons have changed. +func (this *System) SetIconSet (iconSet style.IconSet) { + this.iconSet = iconSet + this.iconSetNonce ++ for hierarchy := range this.hierarchies { hierarchy.setIconSet() } } +// SetFaceSet sets the face set that provides font faces. +func (this *System) SetFaceSet (faceSet style.FaceSet) { + this.faceSet = faceSet +} + func (this *System) removeHierarchy (hierarchy *Hierarchy) { delete(this.hierarchies, hierarchy) } diff --git a/internal/system/textbox.go b/internal/system/textbox.go index 8764672..9c5d8f7 100644 --- a/internal/system/textbox.go +++ b/internal/system/textbox.go @@ -2,6 +2,7 @@ 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" @@ -9,6 +10,7 @@ import "git.tebibyte.media/tomo/tomo/text" 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 textBox struct { *box @@ -31,6 +33,7 @@ type textBox struct { dot text.Dot drawer typeset.Drawer + face util.Cycler[font.Face] on struct { contentBoundsChange event.FuncBroadcaster @@ -41,6 +44,8 @@ type textBox struct { func (this *System) NewTextBox () tomo.TextBox { box := &textBox { } box.box = this.newBox(box) + box.attrTextColor.SetFallback(tomo.ATextColor(color.Black)) + box.attrDotColor.SetFallback(tomo.ADotColor(color.RGBA { G: 255, B: 255, A: 255})) return box } @@ -106,9 +111,8 @@ func (this *textBox) OnDotChange (callback func ()) event.Cookie { 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 } + texture := this.attrTexture.Value().Texture + col := this.attrColor.Value().Color this.drawBorders(can) @@ -124,9 +128,8 @@ func (this *textBox) Draw (can canvas.Canvas) { this.drawDot(can) } - if this.attrFace.Value().Face != nil { + if this.face.Value() != nil { textColor := this.attrTextColor.Value().Color - if textColor == nil { textColor = color.Black } this.drawer.Draw(can, textColor, this.textOffset()) } } @@ -145,23 +148,22 @@ func (this *textBox) setAttr (attr tomo.Attr, user bool) { case tomo.AttrFace: if this.attrFace.Set(attr, user) { - this.drawer.SetFace(attr.Face) - this.invalidateMinimum() - this.invalidateLayout() + this.handleFaceChange() } case tomo.AttrWrap: if this.attrWrap.Set(attr, user) { - this.drawer.SetWrap(bool(attr)) + this.drawer.SetWrap(bool(this.attrWrap.Value())) this.invalidateMinimum() this.invalidateLayout() } case tomo.AttrAlign: if this.attrAlign.Set(attr, user) { + align := this.attrAlign.Value() this.drawer.SetAlign ( - typeset.Align(attr.X), - typeset.Align(attr.Y)) + typeset.Align(align.X), + typeset.Align(align.Y)) this.invalidateDraw() } @@ -175,6 +177,49 @@ func (this *textBox) setAttr (attr tomo.Attr, user bool) { } } +func (this *textBox) unsetAttr (kind tomo.AttrKind, user bool) { + switch kind { + case tomo.AttrKindTextColor: + if this.attrTextColor.Unset(user) && !this.dot.Empty() { + this.invalidateDraw() + } + + case tomo.AttrKindDotColor: + if this.attrDotColor.Unset(user) && !this.dot.Empty() { + this.invalidateDraw() + } + + case tomo.AttrKindFace: + if this.attrFace.Unset(user) { + this.handleFaceChange() + } + + case tomo.AttrKindWrap: + if this.attrWrap.Unset(user) { + this.drawer.SetWrap(bool(this.attrWrap.Value())) + this.invalidateMinimum() + this.invalidateLayout() + } + + case tomo.AttrKindAlign: + if this.attrAlign.Unset(user) { + align := this.attrAlign.Value() + this.drawer.SetAlign ( + typeset.Align(align.X), + typeset.Align(align.Y)) + this.invalidateDraw() + } + + case tomo.AttrKindOverflow: + if this.attrOverflow.Unset(user) { + this.invalidateMinimum() + this.invalidateLayout() + } + + default: this.box.unsetAttr(kind, user) + } +} + func roundPt (point fixed.Point26_6) image.Point { return image.Pt(point.X.Round(), point.Y.Round()) } @@ -184,13 +229,11 @@ func fixPt (point image.Point) fixed.Point26_6 { } func (this *textBox) drawDot (can canvas.Canvas) { - if this.attrFace.Value().Face == nil { return } + face := this.face.Value() + if face == nil { return } - face := this.attrFace.Value().Face textColor := this.attrTextColor.Value().Color dotColor := this.attrDotColor.Value().Color - if textColor == nil { textColor = color.Black } - if dotColor == nil { dotColor = color.RGBA { G: 255, B: 255, A: 255 } } pen := can.Pen() @@ -447,3 +490,16 @@ func (this *textBox) scrollToDot () { this.ScrollTo(scroll) } + +func (this *textBox) handleFaceChange () { + hierarchy := this.getHierarchy() + if hierarchy != nil { return } + faceSet := hierarchy.getFaceSet() + if faceSet != nil { return } + + face := faceSet.Face(tomo.Face(this.attrFace.Value())) + this.face.Set(face, face) + this.drawer.SetFace(face) + this.invalidateMinimum() + this.invalidateLayout() +}