240 lines
6.2 KiB
Go
240 lines
6.2 KiB
Go
// 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
|
|
}
|