nasin/internal/styles/tss/build.go

408 lines
11 KiB
Go
Raw Normal View History

2024-07-29 13:13:02 -06:00
package tss
2024-08-12 18:16:29 -06:00
import "os"
import "io"
2024-07-29 13:13:02 -06:00
import "fmt"
import "image"
import "errors"
import "image/color"
2024-08-12 18:16:29 -06:00
import _ "image/png"
import "path/filepath"
2024-07-29 13:13:02 -06:00
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
2024-08-12 18:16:29 -06:00
import "git.tebibyte.media/tomo/tomo/canvas"
2024-08-10 20:20:34 -06:00
import "git.tebibyte.media/tomo/backend/style"
2024-07-29 13:13:02 -06:00
2024-07-30 11:01:24 -06:00
type styleBuilder struct {
2024-08-12 18:16:29 -06:00
sheet Sheet
workingDir string
textures map[string] canvas.TextureCloser
2024-07-30 11:01:24 -06:00
}
2024-07-29 13:13:02 -06:00
// BuildStyle builds a Tomo style from the specified sheet. Resources associated
// with it (such as textures) can be freed by closing the returned cookie.
2024-08-10 20:20:34 -06:00
func BuildStyle (sheet Sheet) (*style.Style, event.Cookie, error) {
2024-07-30 11:01:24 -06:00
builder := &styleBuilder {
2024-08-12 18:16:29 -06:00
sheet: sheet,
workingDir: filepath.Dir(sheet.Path),
2024-07-30 11:01:24 -06:00
}
return builder.build()
}
2024-08-10 20:20:34 -06:00
func (this *styleBuilder) build () (*style.Style, event.Cookie, error) {
2024-08-12 18:16:29 -06:00
// ensure the sheet is flattened (all vars are resolved)
2024-07-30 11:01:24 -06:00
err := this.sheet.Flatten()
2024-07-29 13:13:02 -06:00
if err != nil { return nil, nil, err }
getColor := func (name string) color.Color {
2024-07-30 11:01:24 -06:00
if list, ok := this.sheet.Variables[name]; ok {
2024-07-29 13:13:02 -06:00
if len(list) > 0 {
if col, ok := list[0].(ValueColor); ok {
return col
}
}
}
return color.RGBA { R: 255, B: 255, A: 255 }
}
2024-08-12 18:16:29 -06:00
// construct style and get colors
2024-07-29 13:13:02 -06:00
cookies := []event.Cookie { }
2024-08-10 20:20:34 -06:00
sty := &style.Style {
Rules: make([]style.Rule, len(this.sheet.Rules)),
2024-07-29 13:13:02 -06:00
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"),
},
}
2024-08-12 18:16:29 -06:00
// build style rules from the sheet's rule slice
2024-07-30 11:01:24 -06:00
for index, rule := range this.sheet.Rules {
2024-08-10 20:20:34 -06:00
styleRule := style.Rule {
2024-07-29 13:13:02 -06:00
Role: tomo.Role {
Package: rule.Selector.Package,
Object: rule.Selector.Object,
},
Tags: rule.Selector.Tags,
2024-08-10 20:20:34 -06:00
Set: make(style.AttrSet),
2024-07-29 13:13:02 -06:00
}
for name, attr := range rule.Attrs {
2024-07-30 11:01:24 -06:00
styleAttr, cookie, err := this.buildAttr(name, attr)
2024-07-29 13:13:02 -06:00
if err != nil { return nil, nil, err }
styleRule.Set.Add(styleAttr)
if cookie != nil {
cookies = append(cookies, cookie)
}
}
2024-08-10 20:20:34 -06:00
sty.Rules[index] = styleRule
2024-07-29 13:13:02 -06:00
}
2024-08-12 18:16:29 -06:00
// add each texture to the cookies list
for _, texture := range this.textures {
cookies = append(cookies, closerCookie { Closer: texture })
}
2024-07-30 11:01:24 -06:00
2024-08-10 20:20:34 -06:00
return sty, event.MultiCookie(cookies...), nil
2024-07-29 13:13:02 -06:00
}
2024-07-30 11:01:24 -06:00
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))
}
2024-07-29 13:13:02 -06:00
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 }
2024-07-30 11:01:24 -06:00
return errWrongType()
2024-07-29 13:13:02 -06:00
}
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
}
2024-07-30 11:01:24 -06:00
return nil, errWrongType()
2024-07-29 13:13:02 -06:00
}
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
}
}
2024-07-30 11:01:24 -06:00
return nil, errWrongType()
2024-07-29 13:13:02 -06:00
}
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
}
2024-07-30 11:01:24 -06:00
return nil, nil, errWrongType()
2024-07-29 13:13:02 -06:00
case "Texture":
2024-08-12 18:16:29 -06:00
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()
2024-07-29 13:13:02 -06:00
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))
}
2024-07-30 11:01:24 -06:00
return nil, nil, errWrongType()
2024-07-29 13:13:02 -06:00
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
}
2024-07-30 11:01:24 -06:00
return nil, nil, errWrongType()
2024-07-29 13:13:02 -06:00
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
}
2024-07-30 11:01:24 -06:00
return nil, nil, errWrongType()
2024-07-29 13:13:02 -06:00
case "Face":
2024-08-10 20:20:34 -06:00
// TODO support weight, italic, slant
2024-07-30 11:01:24 -06:00
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() }
2024-08-10 20:20:34 -06:00
return tomo.AFace(tomo.Face {
Font: string(name),
Size: float64(size),
}), nil, nil
2024-07-29 13:13:02 -06:00
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
}
}
2024-07-30 11:01:24 -06:00
return nil, nil, errWrongType()
2024-07-29 13:13:02 -06:00
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 {
2024-07-30 11:01:24 -06:00
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
2024-07-29 13:13:02 -06:00
default: return nil, nil, errors.New(fmt.Sprintf (
"unknown texture mode: %s",
keyword))
}
}
2024-07-30 11:01:24 -06:00
return nil, nil, errWrongType()
2024-07-29 13:13:02 -06:00
}
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))
}
2024-08-12 18:16:29 -06:00
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
}
2024-07-29 13:13:02 -06:00
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
}
}
2024-07-30 11:01:24 -06:00
2024-08-12 18:16:29 -06:00
type closerCookie struct {
io.Closer
}
func (cookie closerCookie) Close () {
cookie.Closer.Close()
}