diff --git a/application.go b/application.go index 3c62c81..bcfad91 100644 --- a/application.go +++ b/application.go @@ -65,6 +65,15 @@ type ApplicationDescription struct { Role ApplicationRole } +// GlobalApplicationDescription returns the global application description which +// points to cache, data, config, etc. used by Nasin itself. +func GlobalApplicationDescription () ApplicationDescription { + return ApplicationDescription { + Name: "Nasin", + ID: "xyz.holanet.Nasin", + } +} + // String satisfies the fmt.Stringer interface. func (application ApplicationDescription) String () string { if application.Name == "" { @@ -133,8 +142,9 @@ func RunApplication (application Application) { if err != nil { log.Fatalln("nasin: could not register backend:", err) } err = tomo.Run(func () { err := registrar.SetTheme() - err := registrar.SetIconSet() if err != nil { log.Fatalln("nasin: could not set theme:", err) } + err = registrar.SetIconSet() + if err != nil { log.Fatalln("nasin: could not set icon set:", err) } err = application.Init() if err != nil { log.Fatalln("nasin: could not run application:", err) } diff --git a/internal/registrar/registrar_unix.go b/internal/registrar/registrar_unix.go index be01bfd..e46d282 100644 --- a/internal/registrar/registrar_unix.go +++ b/internal/registrar/registrar_unix.go @@ -5,6 +5,7 @@ import "os" import "log" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/backend/x" +import "git.tebibyte.media/sashakoshka/goparse" import "git.tebibyte.media/tomo/nasin/internal/icons/xdg" import "git.tebibyte.media/tomo/nasin/internal/style/tss" import "git.tebibyte.media/tomo/nasin/internal/icons/fallback" @@ -23,8 +24,11 @@ func SetTheme () error { styl, _, err := tss.LoadFile(styleSheetName) if err == nil { tomo.SetStyle(styl) + return nil } else { - log.Printf("nasin: could not load style sheet '%s': %v", styleSheetName, err) + log.Printf ( + "nasin: could not load style sheet '%s'\n%v", + styleSheetName, parse.Format(err)) } } diff --git a/internal/style/styleminimal.tss b/internal/style/styleminimal.tss new file mode 100644 index 0000000..1d8739f --- /dev/null +++ b/internal/style/styleminimal.tss @@ -0,0 +1,14 @@ +$ColorBackground = #FFF; +$ColorForeground = #000; +$ColorRaised = #AAA; +$ColorSunken = #888; +$ColorAccent = #0FF; + +*.* { + Color: $ColorBackground; + Border: $ColorForeground / 1; + TextColor: $ColorForeground; + DotColor: $ColorAccent; + Padding: 2; +} + diff --git a/internal/style/tss/build.go b/internal/style/tss/build.go new file mode 100644 index 0000000..954ee61 --- /dev/null +++ b/internal/style/tss/build.go @@ -0,0 +1,351 @@ +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 + } +} diff --git a/internal/style/tss/flatten.go b/internal/style/tss/flatten.go new file mode 100644 index 0000000..9b4ed68 --- /dev/null +++ b/internal/style/tss/flatten.go @@ -0,0 +1,53 @@ +package tss + +import "fmt" +import "errors" + +// Flatten evaluates all variables recursively, thereby eliminating all +// instances of ValueVariable. +func (this *Sheet) Flatten () error { + if this.flat { return nil } + this.flat = true + + for name, variable := range this.Variables { + variable, err := this.eval(variable) + if err != nil { return err } + this.Variables[name] = variable + } + + for index, rule := range this.Rules { + for name, attr := range rule.Attrs { + for index, list := range attr { + list, err := this.eval(list) + if err != nil { return err } + attr[index] = list + } + rule.Attrs[name] = attr + } + this.Rules[index] = rule + } + + return nil +} + +func (this *Sheet) eval (source ValueList) (ValueList, error) { + destination := make(ValueList, 0, len(source)) + for _, value := range source { + if name, ok := value.(ValueVariable); ok { + variable, ok := this.Variables[string(name)] + if !ok { + return nil, errors.New(fmt.Sprintf( + "variable $%s does not exist", + value)) + } + variable, err := this.eval(variable) + if err != nil { return nil, err } + destination = append(destination, variable...) + continue + } else { + destination = append(destination, value) + } + } + + return destination, nil +} diff --git a/internal/style/tss/lex.go b/internal/style/tss/lex.go index bb2ca64..5dc2125 100644 --- a/internal/style/tss/lex.go +++ b/internal/style/tss/lex.go @@ -19,6 +19,7 @@ const ( Star Dot Dollar + Slash Color Ident Number @@ -39,6 +40,7 @@ var tokenNames = map[parse.TokenKind] string { Star: "Star", Dot: "Dot", Dollar: "Dollar", + Slash: "Slash", Color: "Color", Ident: "Ident", Number: "Number", @@ -115,6 +117,8 @@ func (this *lexer) next () (token parse.Token, err error) { skipRune() if err != nil { return } } + } else { + token.Kind = Slash } if this.eof { err = nil; return } diff --git a/internal/style/tss/parse.go b/internal/style/tss/parse.go index 988cb8f..eac9904 100644 --- a/internal/style/tss/parse.go +++ b/internal/style/tss/parse.go @@ -4,43 +4,6 @@ import "io" import "strconv" import "git.tebibyte.media/sashakoshka/goparse" -type Sheet struct { - Variables map[string] ValueList - Rules []Rule -} - -type Rule struct { - Selector Selector - Attrs map[string] []ValueList -} - -type Selector struct { - Package string - Object string - Tags []string -} - -type ValueList []Value - -type Value interface { - value () -} - -func (ValueNumber) value () { } -type ValueNumber int - -func (ValueColor) value () { } -type ValueColor uint32 - -func (ValueString) value () { } -type ValueString string - -func (ValueKeyword) value () { } -type ValueKeyword string - -func (ValueVariable) value () { } -type ValueVariable string - type parser struct { parse.Parser sheet Sheet @@ -105,9 +68,9 @@ func (this *parser) parseVariable () (string, ValueList, error) { if err != nil { return "", nil, err } err = this.ExpectNext(Ident) if err != nil { return "", nil, err } + name := this.Value() err = this.ExpectNext(Equals) if err != nil { return "", nil, err } - name := this.Value() this.Next() values, err := this.parseValueList() if err != nil { return "", nil, err } @@ -124,15 +87,18 @@ func (this *parser) parseRule () (Rule, error) { selector, err := this.parseSelector() if err != nil { return Rule { }, err } rule.Selector = selector - err = this.Expect(LBracket) + err = this.Expect(LBrace) if err != nil { return Rule { }, err } for { + this.Next() + if this.Is(RBrace) { break } pos := this.Pos() name, attr, err := this.parseAttr() - if err != nil { break } - _, exists := rule.Attrs[name] - if !exists { + if err != nil { return Rule { }, err } + err = this.Expect(Semicolon) + if err != nil { return Rule { }, err } + if _, exists := rule.Attrs[name]; exists { return Rule { }, parse.Errorf ( pos, "attribute %s already declared in this rule", @@ -141,8 +107,6 @@ func (this *parser) parseRule () (Rule, error) { rule.Attrs[name] = attr } - err = this.Expect(LBracket) - if err != nil { return Rule { }, err } return rule, this.Next() } @@ -167,7 +131,7 @@ func (this *parser) parseSelector () (Selector, error) { } // tags - err = this.ExpectNext(LBrace) + err = this.ExpectNext(LBracket) if err == nil { this.Next() for { @@ -175,12 +139,12 @@ func (this *parser) parseSelector () (Selector, error) { if err != nil { return Selector { }, err } if this.Is(RBrace) { break } selector.Tags = append(selector.Tags, this.Value()) - err = this.ExpectNext(Comma, RBrace) + err = this.ExpectNext(Comma, RBracket) if err != nil { return Selector { }, err } } } - return selector, this.Next() + return selector, nil } func (this *parser) parseAttr () (string, []ValueList, error) { @@ -195,18 +159,19 @@ func (this *parser) parseAttr () (string, []ValueList, error) { this.Next() for { err := this.ExpectDesc ( - "value, comma, or semicolon", - Number, Color, String, Ident, Dollar, Comma, Semicolon) + "value, Comma, or Semicolon", + Number, Color, String, Ident, Dollar, Slash, + Comma, Semicolon) if err != nil { return "", nil, err } if this.Is(Semicolon) { break } valueList, err := this.parseValueList() if err != nil { return "", nil, err } attr = append(attr, valueList) - err = this.ExpectNext(Comma, Semicolon) + err = this.Expect(Comma, Semicolon) if err != nil { return "", nil, err } } - return name, attr, this.Next() + return name, attr, nil } func (this *parser) parseValueList () (ValueList, error) { @@ -214,7 +179,7 @@ func (this *parser) parseValueList () (ValueList, error) { for { err := this.ExpectDesc ( "value", - Number, Color, String, Ident, Dollar) + Number, Color, String, Ident, Dollar, Slash) if err != nil { break } switch this.Kind() { case Number: @@ -237,9 +202,12 @@ func (this *parser) parseValueList () (ValueList, error) { err := this.ExpectNext(Ident) if err != nil { return nil, err } list = append(list, ValueVariable(this.Value())) + case Slash: + list = append(list, ValueCut { }) } + this.Next() } - return list, this.Next() + return list, nil } func parseColor (runes []rune) (uint32, bool) { diff --git a/internal/style/tss/tss.go b/internal/style/tss/tss.go index f60e84c..7796bb4 100644 --- a/internal/style/tss/tss.go +++ b/internal/style/tss/tss.go @@ -1,21 +1,79 @@ package tss -import "io" import "os" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/event" -func BuildStyle (sheet Sheet) (*tomo.Style, event.Cookie, error) { - // TODO - return nil, nil, nil +type Sheet struct { + Variables map[string] ValueList + Rules []Rule + flat bool } +type Rule struct { + Selector Selector + Attrs map[string] []ValueList +} + +type Selector struct { + Package string + Object string + Tags []string +} + +type ValueList []Value + +type Value interface { + value () +} + +type ValueNumber int +func (ValueNumber) value () { } + +type ValueColor uint32 +func (ValueColor) value () { } +func (value ValueColor) RGBA () (r, g, b, a uint32) { + // extract components + bits := uint32(value) + r = (bits & 0xF000) >> 24 + g = (bits & 0x0F00) >> 16 + b = (bits & 0x00F0) >> 8 + a = (bits & 0x000A) + // extend to 16 bits per channel + r = r << 8 | r + g = g << 8 | g + b = b << 8 | b + a = a << 8 | a + // alpha premultiply + r = (r * a) / 256 + g = (g * a) / 256 + b = (b * a) / 256 + return +} + +type ValueString string +func (ValueString) value () { } + +type ValueKeyword string +func (ValueKeyword) value () { } + +type ValueVariable string +func (ValueVariable) value () { } + +type ValueCut struct { } +func (ValueCut) value () { } + +// LoadFile loads the stylesheet from the specified file. This may return a +// parse.Error, so use parse.Format to print it. func LoadFile (name string) (*tomo.Style, event.Cookie, error) { // TODO check cache for gobbed sheet. if the cache is nonexistent or // invalid, then open/load/cache. - + file, err := os.Open(name) if err != nil { return nil, nil, err } defer file.Close() - return Load(file) + + sheet, err := Parse(Lex(name, file)) + if err != nil { return nil, nil, err } + return BuildStyle(sheet) }