package tss import "fmt" import "image" import "errors" import "image/color" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/event" // 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) (*tomo.Style, event.Cookie, error) { err := sheet.Flatten() if err != nil { return nil, nil, err } getColor := func (name string) color.Color { if list, ok := 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 } } cookies := []event.Cookie { } style := &tomo.Style { Rules: make([]tomo.Rule, len(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"), }, } for index, rule := range sheet.Rules { styleRule := tomo.Rule { Role: tomo.Role { Package: rule.Selector.Package, Object: rule.Selector.Object, }, Tags: rule.Selector.Tags, Set: make(tomo.AttrSet), } for name, attr := range rule.Attrs { styleAttr, cookie, err := buildAttr(name, attr) if err != nil { return nil, nil, err } styleRule.Set.Add(styleAttr) if cookie != nil { cookies = append(cookies, cookie) } } style.Rules[index] = styleRule } return style, event.MultiCookie(cookies...), nil } func buildAttr (name string, attr []ValueList) (tomo.Attr, event.Cookie, error) { 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 errors.New(fmt.Sprintf ( "wrong type for %s attribute", name)) } 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, errors.New(fmt.Sprintf ( "wrong type for %s attribute", name)) } 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, errors.New(fmt.Sprintf ( "wrong type for %s attribute", name)) } 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, errors.New(fmt.Sprintf ( "wrong type for %s attribute", name)) case "Texture": // TODO load image from file 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, errors.New(fmt.Sprintf ( "wrong type for %s attribute", name)) 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, errors.New(fmt.Sprintf ( "wrong type for %s attribute", name)) 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, errors.New(fmt.Sprintf ( "wrong type for %s attribute", name)) case "Face": // TODO: load font from file with some parameters 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, errors.New(fmt.Sprintf ( "wrong type for %s attribute", name)) 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 case "middle": aligns[index] = tomo.AlignMiddle case "end": aligns[index] = tomo.AlignEnd case "even": aligns[index] = tomo.AlignEven default: return nil, nil, errors.New(fmt.Sprintf ( "unknown texture mode: %s", keyword)) } } return nil, nil, errors.New(fmt.Sprintf ( "wrong type for %s attribute", name)) } 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 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 } }