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() }