// Package style provides a data-driven style implementation. package style import "image" import "image/color" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/canvas" // this is CSS's bastard child // Style allows the use of data to define a visual style. type Style 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 // Rules determines which styles get applied to which Objects. Rules []Rule // Colors maps tomo.Color values to color.RGBA values. Colors map[tomo.Color] color.Color } // Rule describes under what circumstances should certain style attributes be // active. type Rule struct { Role tomo.Role Default AttrSet Hovered AttrSet Pressed AttrSet Focused AttrSet } // 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 *Style) execute (object tomo.Object, set AttrSet) { box := object.GetBox() for _, attr := range set.set { switch attr := attr.(type) { case AttrColor: box.SetColor(attr.Color) case AttrTexture: box.SetTextureTile(this.texture(string(attr))) case AttrBorder: box.SetBorder([]tomo.Border(attr)...) case AttrMinimumSize: box.SetMinimumSize(image.Point(attr)) case AttrPadding: box.SetPadding(tomo.Inset(attr)) case AttrGap: if box, ok := box.(tomo.ContainerBox); ok { box.SetGap(image.Point(attr)) } case AttrTextColor: 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/tomo.Theme: unexpected attribute") } } } func (this *Style) texture (name string) canvas.Texture { this.ensureTextureCache() if texture, ok := this.textures[name]; ok { return texture } if this.Textures == nil { if source, ok := this.Textures[name]; ok { texture := tomo.NewTexture(source) this.textures[name] = texture return texture } } return this.missingTexture() } func (this *Style) missingTexture () canvas.Texture { if this.missing == nil { this.missing = tomo.NewTexture(missingTexture(16)) } return this.missing } func (this *Style) 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 *Style) setsFor (role tomo.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 *Style) Apply (object tomo.Object) event.Cookie { pressed := false hovered := false box := object.GetBox() role := box.Role() defaultSet, hoveredSet, pressedSet, focusedSet := this.setsFor(role) updateStyle := func () { if pressed { this.execute(object, pressedSet) } else if hovered { this.execute(object, hoveredSet) } else { this.execute(object, defaultSet) } if box.Focused() && !pressed { this.execute(object, focusedSet) } } updateStyle() return event.MultiCookie ( 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() }), box.OnMouseEnter(func () { hovered = true updateStyle() }), box.OnMouseLeave(func () { hovered = false updateStyle() })) } func (this *Style) RGBA (c tomo.Color) (r, g, b, a uint32) { if this.Colors == nil { return 0xFFFF, 0, 0xFFFF, 0xFFFF } color, ok := this.Colors[c] if !ok { return 0xFFFF, 0, 0xFFFF, 0xFFFF } return color.RGBA() } // Close closes all cached textures this style has open. Do not call this while // the style is in use. func (this *Style) Close () error { this.missing.Close() this.missing = nil for _, texture := range this.textures { texture.Close() } this.textures = nil return nil }