408 lines
11 KiB
Go
408 lines
11 KiB
Go
package tss
|
|
|
|
import "os"
|
|
import "io"
|
|
import "fmt"
|
|
import "image"
|
|
import "errors"
|
|
import "image/color"
|
|
import _ "image/png"
|
|
import "path/filepath"
|
|
import "git.tebibyte.media/tomo/tomo"
|
|
import "git.tebibyte.media/tomo/tomo/event"
|
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
|
import "git.tebibyte.media/tomo/backend/style"
|
|
|
|
type styleBuilder struct {
|
|
sheet Sheet
|
|
workingDir string
|
|
textures map[string] canvas.TextureCloser
|
|
}
|
|
|
|
// BuildStyle builds a Tomo style from the specified sheet. Resources associated
|
|
// with it (such as textures) can be freed by closing the returned cookie.
|
|
func BuildStyle (sheet Sheet) (*style.Style, event.Cookie, error) {
|
|
builder := &styleBuilder {
|
|
sheet: sheet,
|
|
workingDir: filepath.Dir(sheet.Path),
|
|
}
|
|
return builder.build()
|
|
}
|
|
|
|
func (this *styleBuilder) build () (*style.Style, event.Cookie, error) {
|
|
// ensure the sheet is flattened (all vars are resolved)
|
|
err := this.sheet.Flatten()
|
|
if err != nil { return nil, nil, err }
|
|
|
|
getColor := func (name string) color.Color {
|
|
if list, ok := this.sheet.Variables[name]; ok {
|
|
if len(list) > 0 {
|
|
if col, ok := list[0].(ValueColor); ok {
|
|
return col
|
|
}
|
|
}
|
|
}
|
|
return color.RGBA { R: 255, B: 255, A: 255 }
|
|
}
|
|
|
|
// construct style and get colors
|
|
cookies := []event.Cookie { }
|
|
sty := &style.Style {
|
|
Rules: make([]style.Rule, len(this.sheet.Rules)),
|
|
Colors: map[tomo.Color] color.Color {
|
|
tomo.ColorBackground: getColor("ColorBackground"),
|
|
tomo.ColorForeground: getColor("ColorForeground"),
|
|
tomo.ColorRaised: getColor("ColorRaised"),
|
|
tomo.ColorSunken: getColor("ColorSunken"),
|
|
tomo.ColorAccent: getColor("ColorAccent"),
|
|
},
|
|
}
|
|
|
|
// build style rules from the sheet's rule slice
|
|
for index, rule := range this.sheet.Rules {
|
|
styleRule := style.Rule {
|
|
Role: tomo.Role {
|
|
Package: rule.Selector.Package,
|
|
Object: rule.Selector.Object,
|
|
},
|
|
Tags: rule.Selector.Tags,
|
|
Set: make(style.AttrSet),
|
|
}
|
|
for name, attr := range rule.Attrs {
|
|
styleAttr, cookie, err := this.buildAttr(name, attr)
|
|
if err != nil { return nil, nil, err }
|
|
styleRule.Set.Add(styleAttr)
|
|
if cookie != nil {
|
|
cookies = append(cookies, cookie)
|
|
}
|
|
}
|
|
sty.Rules[index] = styleRule
|
|
}
|
|
|
|
// add each texture to the cookies list
|
|
for _, texture := range this.textures {
|
|
cookies = append(cookies, closerCookie { Closer: texture })
|
|
}
|
|
|
|
return sty, event.MultiCookie(cookies...), nil
|
|
}
|
|
|
|
func (this *styleBuilder) buildAttr (name string, attr []ValueList) (tomo.Attr, event.Cookie, error) {
|
|
errWrongType := func () error {
|
|
return errors.New(fmt.Sprintf("wrong type for %s attribute", name))
|
|
}
|
|
expectSingle := func () error {
|
|
if len(attr) != 1 {
|
|
return errors.New(fmt.Sprintf (
|
|
"%s attribute requires exactly one value list",
|
|
name))
|
|
}
|
|
return nil
|
|
}
|
|
expectSingleSingle := func () error {
|
|
err := expectSingle()
|
|
if err != nil { return err }
|
|
if len(attr[0]) != 1 {
|
|
return errors.New(fmt.Sprintf (
|
|
"%s attribute requires exactly one value",
|
|
name))
|
|
}
|
|
return nil
|
|
}
|
|
expectNumbers := func (list ValueList) error {
|
|
for _, value := range list {
|
|
if _, ok := value.(ValueNumber); ok { continue }
|
|
return errWrongType()
|
|
}
|
|
return nil
|
|
}
|
|
numbers := func (list ValueList) ([]int, error) {
|
|
nums := make([]int, len(list))
|
|
for index, value := range list {
|
|
if value, ok := value.(ValueNumber); ok {
|
|
nums[index] = int(value)
|
|
continue
|
|
}
|
|
return nil, errWrongType()
|
|
}
|
|
return nums, nil
|
|
}
|
|
bools := func (list ValueList) ([]bool, error) {
|
|
bools := make([]bool, len(list))
|
|
for index, value := range list {
|
|
if value, ok := value.(ValueKeyword); ok {
|
|
switch value {
|
|
case "true":
|
|
bools[index] = true
|
|
continue
|
|
case "false":
|
|
bools[index] = false
|
|
continue
|
|
}
|
|
}
|
|
return nil, errWrongType()
|
|
}
|
|
return bools, nil
|
|
}
|
|
point := func (list ValueList) (image.Point, error) {
|
|
err := expectNumbers(list)
|
|
if err != nil { return image.Point { }, err }
|
|
|
|
vector := image.Point { }
|
|
switch len(attr[0]) {
|
|
case 1:
|
|
vector.X = int(list[0].(ValueNumber))
|
|
vector.Y = int(list[0].(ValueNumber))
|
|
case 2:
|
|
vector.X = int(list[0].(ValueNumber))
|
|
vector.Y = int(list[1].(ValueNumber))
|
|
default:
|
|
return image.Point { }, errors.New(fmt.Sprintf (
|
|
"%s attribute requires exactly one or two values",
|
|
name))
|
|
}
|
|
return vector, nil
|
|
}
|
|
|
|
switch name {
|
|
case "Color":
|
|
err := expectSingleSingle()
|
|
if err != nil { return nil, nil, err }
|
|
if col, ok := attr[0][0].(ValueColor); ok {
|
|
return tomo.AColor(col), nil, nil
|
|
}
|
|
return nil, nil, errWrongType()
|
|
|
|
case "Texture":
|
|
err := expectSingleSingle()
|
|
if err != nil { return nil, nil, err }
|
|
if name, ok := attr[0][0].(ValueString); ok {
|
|
texture, err := this.texture(string(name))
|
|
if err != nil { return nil, nil, err }
|
|
return tomo.ATexture(texture), nil, nil
|
|
}
|
|
return nil, nil, errWrongType()
|
|
|
|
case "TextureMode":
|
|
err := expectSingleSingle()
|
|
if err != nil { return nil, nil, err }
|
|
if keyword, ok := attr[0][0].(ValueKeyword); ok {
|
|
switch keyword {
|
|
case "tile": return tomo.ATextureMode(tomo.TextureModeCenter), nil, nil
|
|
case "center": return tomo.ATextureMode(tomo.TextureModeCenter), nil, nil
|
|
}
|
|
return nil, nil, errors.New(fmt.Sprintf (
|
|
"unknown texture mode: %s",
|
|
keyword))
|
|
}
|
|
return nil, nil, errWrongType()
|
|
|
|
case "Border":
|
|
attrBorder, err := buildAttrBorder(attr)
|
|
if err != nil { return nil, nil, err }
|
|
return attrBorder, nil, nil
|
|
|
|
case "MinimumSize":
|
|
err := expectSingle()
|
|
if err != nil { return nil, nil, err }
|
|
vector, err := point(attr[0])
|
|
if err != nil { return nil, nil, err }
|
|
return tomo.AttrMinimumSize(vector), nil, nil
|
|
|
|
case "Padding":
|
|
err := expectSingle()
|
|
if err != nil { return nil, nil, err }
|
|
numbers, err := numbers(attr[0])
|
|
if err != nil { return nil, nil, err }
|
|
inset := tomo.Inset { }
|
|
if !copyBorderValue(inset[:], numbers) {
|
|
return nil, nil, errors.New(fmt.Sprintf (
|
|
"%s attribute requires exactly one, two, or four values",
|
|
name))
|
|
}
|
|
return tomo.AttrPadding(inset), nil, nil
|
|
|
|
case "Gap":
|
|
err := expectSingle()
|
|
if err != nil { return nil, nil, err }
|
|
vector, err := point(attr[0])
|
|
if err != nil { return nil, nil, err }
|
|
return tomo.AttrGap(vector), nil, nil
|
|
|
|
case "TextColor":
|
|
err := expectSingleSingle()
|
|
if err != nil { return nil, nil, err }
|
|
if col, ok := attr[0][0].(ValueColor); ok {
|
|
return tomo.ATextColor(col), nil, nil
|
|
}
|
|
return nil, nil, errWrongType()
|
|
|
|
case "DotColor":
|
|
err := expectSingleSingle()
|
|
if err != nil { return nil, nil, err }
|
|
if col, ok := attr[0][0].(ValueColor); ok {
|
|
return tomo.ADotColor(col), nil, nil
|
|
}
|
|
return nil, nil, errWrongType()
|
|
|
|
case "Face":
|
|
// TODO support weight, italic, slant
|
|
err := expectSingle()
|
|
if err != nil { return nil, nil, err }
|
|
list := attr[0]
|
|
if len(list) != 2 {
|
|
return nil, nil, errors.New(fmt.Sprintf (
|
|
"%s attribute requires exactly two values",
|
|
name))
|
|
}
|
|
name, ok := list[0].(ValueString)
|
|
if !ok { return nil, nil, errWrongType() }
|
|
size, ok := list[1].(ValueNumber)
|
|
if !ok { return nil, nil, errWrongType() }
|
|
return tomo.AFace(tomo.Face {
|
|
Font: string(name),
|
|
Size: float64(size),
|
|
}), nil, nil
|
|
|
|
case "Wrap":
|
|
err := expectSingleSingle()
|
|
if err != nil { return nil, nil, err }
|
|
if value, ok := attr[0][0].(ValueKeyword); ok {
|
|
switch value {
|
|
case "true": return tomo.AWrap(true), nil, nil
|
|
case "false": return tomo.AWrap(false), nil, nil
|
|
}
|
|
}
|
|
return nil, nil, errWrongType()
|
|
|
|
case "Align":
|
|
err := expectSingle()
|
|
if err != nil { return nil, nil, err }
|
|
list := attr[0]
|
|
if len(list) != 2 {
|
|
return nil, nil, errors.New(fmt.Sprintf (
|
|
"%s attribute requires exactly two values",
|
|
name))
|
|
}
|
|
aligns := [2]tomo.Align { }
|
|
for index, value := range list {
|
|
if keyword, ok := value.(ValueKeyword); ok {
|
|
switch keyword {
|
|
case "start": aligns[index] = tomo.AlignStart; continue
|
|
case "middle": aligns[index] = tomo.AlignMiddle; continue
|
|
case "end": aligns[index] = tomo.AlignEnd; continue
|
|
case "even": aligns[index] = tomo.AlignEven; continue
|
|
default: return nil, nil, errors.New(fmt.Sprintf (
|
|
"unknown texture mode: %s",
|
|
keyword))
|
|
}
|
|
}
|
|
return nil, nil, errWrongType()
|
|
}
|
|
return tomo.AAlign(aligns[0], aligns[1]), nil, nil
|
|
|
|
case "Overflow":
|
|
err := expectSingle()
|
|
if err != nil { return nil, nil, err }
|
|
bools, err := bools(attr[0])
|
|
if err != nil { return nil, nil, err }
|
|
if len(bools) != 2 {
|
|
return nil, nil, errors.New(fmt.Sprintf (
|
|
"%s attribute requires exactly two values",
|
|
name))
|
|
}
|
|
return tomo.AOverflow(bools[0], bools[1]), nil, nil
|
|
|
|
case "Layout":
|
|
// TODO allow use of some layouts in the objects package
|
|
|
|
default: return nil, nil, errors.New(fmt.Sprintf("unknown attribute name %s", name))
|
|
}
|
|
return nil, nil, errors.New(fmt.Sprintf("unimplemented attribute name %s", name))
|
|
}
|
|
|
|
func (this *styleBuilder) texture (path string) (canvas.TextureCloser, error) {
|
|
path = filepath.Join(this.workingDir, path)
|
|
if texture, ok := this.textures[path]; ok {
|
|
return texture, nil
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil { return nil, err }
|
|
defer file.Close()
|
|
rawImage, _, err := image.Decode(file)
|
|
if err != nil { return nil, err }
|
|
return tomo.NewTexture(rawImage), nil
|
|
}
|
|
|
|
func buildAttrBorder (attr []ValueList) (tomo.Attr, error) {
|
|
borders := make([]tomo.Border, len(attr))
|
|
for index, list := range attr {
|
|
colors := make([]color.Color, 0, len(list))
|
|
sizes := make([]int, 0, len(list))
|
|
|
|
capturingSize := false
|
|
for _, value := range list {
|
|
if capturingSize {
|
|
if value, ok := value.(ValueNumber); ok {
|
|
sizes = append(sizes, int(value))
|
|
continue
|
|
}
|
|
} else {
|
|
if _, ok := value.(ValueCut); ok {
|
|
capturingSize = true
|
|
continue
|
|
}
|
|
if value, ok := value.(ValueColor); ok {
|
|
colors = append(colors, value)
|
|
continue
|
|
}
|
|
}
|
|
return nil, errors.New("malformed Border attribute value list")
|
|
}
|
|
|
|
border := tomo.Border { }
|
|
if !copyBorderValue(border.Width[:], sizes) {
|
|
return nil, errors.New("malformed Border attribute width list")
|
|
}
|
|
if !copyBorderValue(border.Color[:], colors) {
|
|
return nil, errors.New("malformed Border attribute color list")
|
|
}
|
|
borders[index] = border
|
|
}
|
|
|
|
return tomo.ABorder(borders...), nil
|
|
}
|
|
|
|
func copyBorderValue[T any, U ~[]T] (destination, source U) bool {
|
|
if len(source) > len(destination) { return false }
|
|
switch len(source) {
|
|
case 1:
|
|
destination[0] = source[0]
|
|
destination[1] = source[0]
|
|
destination[2] = source[0]
|
|
destination[3] = source[0]
|
|
return true
|
|
case 2:
|
|
destination[0] = source[0]
|
|
destination[1] = source[1]
|
|
destination[2] = source[0]
|
|
destination[3] = source[1]
|
|
return true
|
|
case 4:
|
|
destination[0] = source[0]
|
|
destination[1] = source[1]
|
|
destination[2] = source[2]
|
|
destination[3] = source[3]
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type closerCookie struct {
|
|
io.Closer
|
|
}
|
|
func (cookie closerCookie) Close () {
|
|
cookie.Closer.Close()
|
|
}
|