diff --git a/internal/theme/attribute.go b/internal/theme/attribute.go index 3fd4481..6d2738f 100644 --- a/internal/theme/attribute.go +++ b/internal/theme/attribute.go @@ -6,29 +6,36 @@ import "golang.org/x/image/font" import "git.tebibyte.media/tomo/tomo" // Attr modifies one thing about an Objects's style. -type Attr interface { attr () } +type Attr interface { attr () int } // AttrColor sets the background color of an Objects. type AttrColor struct { color.Color } -func (AttrColor) attr () { } // AttrTexture sets the texture of an Objects to a named texture. type AttrTexture string -func (AttrTexture) attr () { } // AttrBorder sets the border of an Objects. type AttrBorder []tomo.Border -func (AttrBorder) attr () { } // AttrMinimumSize sets the minimum size of an Objects. type AttrMinimumSize image.Point -func (AttrMinimumSize) attr () { } // AttrPadding sets the inner padding of an Objects. type AttrPadding tomo.Inset -func (AttrPadding) attr () { } // AttrGap sets the gap between child Objects, if the Object is a ContainerBox. type AttrGap image.Point -func (AttrGap) attr () { } // AttrTextColor sets the text color, if the Object is a TextBox. type AttrTextColor struct { color.Color } -func (AttrTextColor) attr () { } +// AttrDotColor sets the text selection color, if the Object is a TextBox. +type AttrDotColor struct { color.Color } // AttrFace sets the font face, if the Object is a TextBox. type AttrFace struct { font.Face } -func (AttrFace) attr () { } +// AttrAlign sets the alignment, if the Object is a ContentBox. +type AttrAlign struct { X, Y tomo.Align } + +func (AttrColor) attr () int { return 0 } +func (AttrTexture) attr () int { return 1 } +func (AttrBorder) attr () int { return 2 } +func (AttrMinimumSize) attr () int { return 3 } +func (AttrPadding) attr () int { return 4 } +func (AttrGap) attr () int { return 5 } +func (AttrTextColor) attr () int { return 6 } +func (AttrDotColor) attr () int { return 7 } +func (AttrFace) attr () int { return 8 } +func (AttrAlign) attr () int { return 9 } diff --git a/internal/theme/theme.go b/internal/theme/theme.go index f22d833..99d59f6 100644 --- a/internal/theme/theme.go +++ b/internal/theme/theme.go @@ -10,19 +10,17 @@ import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/theme" import "git.tebibyte.media/tomo/tomo/canvas" +// this is CSS's bastard child + // Theme allows the use of data to define a visual style. type Theme struct { // Textures maps texture names to image textures. Textures map[string] image.Image textures map[string] canvas.TextureCloser // private texture cache missing canvas.TextureCloser // cache for "missing" texture - - // Default lists style attributes that apply to all objects, which are - // overridden by attributes in the Rules map. - Default Rule - + // Rules determines which styles get applied to which Objects. - Rules map[theme.Role] Rule + Rules []Rule // Colors maps theme.Color values to color.RGBA values. Colors map[theme.Color] color.Color @@ -43,15 +41,66 @@ type IconTheme interface { // Rule describes under what circumstances should certain style attributes be // active. type Rule struct { - Default []Attr - Hovered []Attr - Pressed []Attr + Role theme.Role + Default AttrSet + Hovered AttrSet + Pressed AttrSet + Focused AttrSet } -func (this *Theme) execute (object tomo.Object, attrs ...Attr) { +// AttrSet is a set of attributes wherein only one/zero of each attribute type +// can exist. I deserve to be imprisoned for the way I made this work (look in +// attribute.go). Its zero value can be used safely, and you can copy it if you +// want, but it will point to the same set of attributes. +type AttrSet struct { + set map[int] Attr +} + +// AS builds an AttrSet out of a vararg list of Attr values. +func AS (attrs ...Attr) AttrSet { + set := AttrSet { } + set.Add(attrs...) + return set +} + +// Add adds attributes to the set. +func (this *AttrSet) Add (attrs ...Attr) { + this.ensure() + for _, attr := range attrs { + this.set[attr.attr()] = attr + } +} + +// MergeUnder takes attributes from another set and adds them if they don't +// already exist in this one. +func (this *AttrSet) MergeUnder (other AttrSet) { + this.ensure() + if other.set == nil { return } + for _, attr := range other.set { + if _, exists := this.set[attr.attr()]; !exists { + this.Add(attr) + } + } +} + +// MergeOver takes attributes from another set and adds them, overriding this +// one. +func (this *AttrSet) MergeOver (other AttrSet) { + this.ensure() + if other.set == nil { return } + for _, attr := range other.set { + this.Add(attr) + } +} + +func (this *AttrSet) ensure () { + if this.set == nil { this.set = make(map[int] Attr) } +} + +func (this *Theme) execute (object tomo.Object, set AttrSet) { box := object.GetBox() - for _, attr := range attrs { + for _, attr := range set.set { switch attr := attr.(type) { case AttrColor: box.SetColor(attr.Color) @@ -71,10 +120,20 @@ func (this *Theme) execute (object tomo.Object, attrs ...Attr) { if box, ok := box.(tomo.TextBox); ok { box.SetTextColor(attr.Color) } + case AttrDotColor: + if box, ok := box.(tomo.TextBox); ok { + box.SetDotColor(attr.Color) + } case AttrFace: if box, ok := box.(tomo.TextBox); ok { box.SetFace(attr) } + case AttrAlign: + if box, ok := box.(tomo.ContentBox); ok { + box.SetAlign(attr.X, attr.Y) + } + default: + panic("bug: nasin/internal/theme.Theme: unexpected attribute") } } } @@ -105,37 +164,46 @@ func (this *Theme) ensureTextureCache () { if this.textures == nil { this.textures = make(map[string] canvas.TextureCloser) } } +// setsFor builds flattened attr sets for a specific role based on the rules list +func (this *Theme) setsFor (role theme.Role) (defaul, hovered, pressed, focused AttrSet) { + for _, current := range this.Rules { + // check for a match + packageMatch := current.Role.Package == role.Package || current.Role.Package == "" + objectMatch := current.Role.Object == role.Object || current.Role.Object == "" + variantMatch := current.Role.Variant == role.Variant || current.Role.Variant == "" + if packageMatch && objectMatch && variantMatch { + // if found, merge and override + defaul.MergeOver(current.Default) + hovered.MergeOver(current.Hovered) + pressed.MergeOver(current.Pressed) + focused.MergeOver(current.Focused) + } + } + + // hovered and pressed are mutually exclusive states, so we compress + // them with the default state. + hovered.MergeUnder(defaul) + pressed.MergeUnder(defaul) + return defaul, hovered, pressed, focused +} + func (this *Theme) Apply (object tomo.Object, role theme.Role) event.Cookie { pressed := false hovered := false box := object.GetBox() - var rule Rule - if this.Rules != nil { - rule, _ = this.Rules[role] - } + defaultSet, hoveredSet, pressedSet, focusedSet := this.setsFor(role) updateStyle := func () { - // hover styles override default styles, pressed styles - // override hovered styles, and specific styles override default - // styles. - - // default - this.execute(object, this.Default.Default...) - if hovered { - this.execute(object, this.Default.Hovered...) - } if pressed { - this.execute(object, this.Default.Pressed...) + this.execute(object, pressedSet) + } else if hovered { + this.execute(object, hoveredSet) + } else { + this.execute(object, defaultSet) } - - // specific - this.execute(object, rule.Default...) - if hovered { - this.execute(object, rule.Hovered...) - } - if pressed { - this.execute(object, rule.Pressed...) + if box.Focused() && !pressed { + this.execute(object, focusedSet) } } updateStyle() @@ -144,10 +212,12 @@ func (this *Theme) Apply (object tomo.Object, role theme.Role) event.Cookie { box.OnFocusEnter(updateStyle), box.OnFocusLeave(updateStyle), box.OnMouseDown(func (button input.Button) { + if button != input.ButtonLeft { return } pressed = true updateStyle() }), box.OnMouseUp(func (button input.Button) { + if button != input.ButtonLeft { return } pressed = false updateStyle() }),