Renamed style directory to styles

This commit is contained in:
2024-08-12 17:12:43 -04:00
parent ecfb90957a
commit 4b53a5a019
17 changed files with 2 additions and 2 deletions

View File

@@ -0,0 +1,214 @@
// Colors
$ColorDot = #7391c0;
$ColorAccent = #5f8bc4;
$ColorHighlight = #5f8bc4;
$ColorBackground = #d4d4d4;
$ColorForeground = #000;
$ColorOutline = $ColorForeground;
$ColorGutter = #bfc6d1;
$ColorGutterHovered = #c5cbd6;
$ColorRaised = #e9eaea;
$ColorRaisedPressed = #ccd4dd;
$ColorRaisedFocused = #cfd6dd;
$ColorRaisedHovered = #f1f3f5;
$ColorSunken = #e9eaea;
$ColorSunkenFocused = #e0e6ee;
$ColorSunkenPressed = #e0e6ee;
$ColorCalendarWeekdayHeader = #d3cac2;
$ColorCalendarWeekend = #c2d3c4;
$ColorCalendarDay = #d6dae2;
// Borders
$BorderOutline = $ColorOutline / 1;
$BorderEngraved = #c3c3c5 #e3e3e3 #e9e9e9 #c2c2c2 / 1;
$BorderGap = #697c7c #566767 #566767 #697c7c / 1;
$BorderLifted = #f9fafc #c2c8d3 #a4afc0 #f5f6f8 / 1;
$BorderLiftedFocused = #f0f4f9 #b1baca #9aa6b7 #e4e9ee / 1;
$BorderFocused = #5f8bc4 #5f8bc4 #5f8bc4 #5f8bc4 / 1;
$BorderTear = $BorderEngraved;
$BorderTearFocused = #7f94b5 #ced7e4 #ced7e4 #7f94b5 / 1;
$BorderTearPad = #0000 / 3;
$BorderTearPadFocused = #7391c080 / 3;
$BorderInnerShadow = #a4afc0 / 1 0 0 1;
$BorderOuterShadow = #a4afc0 / 0 1 1 0;
*.* {
TextColor: $ColorForeground;
DotColor: $ColorDot;
Gap: 8;
}
*.Button {
Border: $BorderEngraved, $BorderGap, $BorderLifted;
Padding: 4 8;
Color: $ColorRaised;
}
*.Button[focused] {
Border: $BorderEngraved, $BorderGap, $BorderLiftedFocused;
Padding: 4 8;
Color: $ColorRaisedFocused;
}
*.Button[hovered] {
Color: $ColorRaisedHovered;
}
*.Button[pressed] {
Border: $BorderEngraved, $BorderGap, $BorderInnerShadow;
Padding: 5 8 4 9;
Color: $ColorRaisedPressed;
}
*.TextInput {
Border: $BorderEngraved, $BorderGap, $BorderInnerShadow;
Padding: 5 4 4 5;
Color: $ColorSunken;
}
*.TextInput[focused] {
Border: $BorderEngraved, $BorderFocused, $BorderInnerShadow;
}
*.TextView {
Border: $BorderEngraved, $BorderGap, $BorderInnerShadow;
Padding: 5 4 4 5;
Color: $ColorSunken;
}
*.NumberInput {
Gap: 0;
}
*.Container[sunken] {
Border: $BorderEngraved, $BorderGap, $BorderInnerShadow;
Padding: 5 4 4 5;
Color: $ColorSunken;
}
*.Container[outer] {
Color: $ColorBackground;
Padding: 8;
}
*.Container[menu] {
Border: $BorderGap, $BorderLifted;
Color: $ColorBackground;
Gap: 0;
}
*.Heading {
Align: middle middle;
}
*.Separator {
Border: $BorderEngraved;
}
*.Slider {
Border: $BorderEngraved, $BorderGap, $BorderInnerShadow;
Color: $ColorGutter;
}
*.Slider[focused] {
Border: $BorderEngraved, $BorderFocused, $BorderInnerShadow;
}
*.Slider[hovered] {
Color: $ColorGutterHovered;
}
*.Slider[horizontal] {
MinimumSize: 48 0;
}
*.Slider[vertical] {
MinimumSize: 0 48;
}
*.SliderHandle {
Border: $BorderOuterShadow, $BorderGap, $BorderLifted;
Color: $ColorRaised;
MinimumSize: 12;
}
*.ScrollContainer {
Gap: 0;
}
*.Checkbox {
Border: $BorderEngraved, $BorderGap, $BorderInnerShadow;
Color: $ColorSunken;
Padding: 0 1 1 0;
MinimumSize: 19;
}
*.Checkbox[focused] {
Border: $BorderEngraved, $BorderFocused, $BorderInnerShadow;
Color: $ColorSunkenFocused;
Padding: 0;
}
*.Checkbox[checked] {
// TODO
}
*.MenuItem {
Padding: 4;
Gap: 4;
Color: #0000;
}
*.MenuItem[hovered] {
Color: $ColorDot;
}
*.MenuItem[focused] {
Color: $ColorDot;
}
*.File {
Color: #0000;
}
*.File[focused] {
Color: $ColorDot;
}
*.TearLine {
Border: $BorderTearPad, $BorderTear;
}
*.TearLine[hovered] {
Border: $BorderTearPadFocused, $BorderTearFocused;
}
*.TearLine[focused] {
Border: $BorderTearPadFocused, $BorderTearFocused;
}
*.Calendar {
Border: $BorderOuterShadow, $BorderGap;
Color: $ColorRaised;
Padding: 2;
Gap: 2;
}
*.CalendarGrid {
Gap: 2 2;
}
*.CalendarWeekdayHeader {
Color: $ColorCalendarWeekdayHeader;
Padding: 2;
}
*.CalendarDay {
Color: $ColorCalendarDay;
Padding: 2;
MinimumSize: 32;
}
*.CalendarDay[weekend] {
Color: $ColorCalendarWeekend;
}

View File

@@ -0,0 +1,18 @@
package aluminumStyle
import "image/color"
import "git.tebibyte.media/tomo/tomo"
// New returns Aluminum, a futuristic, bluish-white style.
func New () *style.Style {
return &style.Style {
Colors: map[tomo.Color] color.Color {
tomo.ColorBackground: colorBackground,
tomo.ColorForeground: colorForeground,
tomo.ColorRaised: colorRaised,
tomo.ColorSunken: colorSunken,
tomo.ColorAccent: colorFocus,
},
Rules: rules,
}
}

View File

@@ -0,0 +1,391 @@
package aluminumStyle
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/font/basicfont"
func hex (color uint32) (c color.RGBA) {
c.A = uint8(color)
c.B = uint8(color >> 8)
c.G = uint8(color >> 16)
c.R = uint8(color >> 24)
return
}
func border (top, right, bottom, left uint32, width ...int) tomo.Border {
return tomo.Border {
Width: tomo.I(width...),
Color: [4]color.Color {
hex(top), hex(right),
hex(bottom), hex(left),
},
}
}
var colorDot = hex(0x7391c080)
var colorFocus = hex(0x5f8bc4FF)
var colorHighlight = hex(0x5f8bc4FF)
var colorBackground = hex(0xd4d4d4FF)
var colorForeground = color.Black
var colorOutline = color.Black
var colorGutter = hex(0xbfc6d1FF)
var colorGutterHovered = hex(0xc5cbd6FF)
var colorRaised = hex(0xe9eaeaFF)
var colorRaisedPressed = hex(0xccd4ddFF)
var colorRaisedFocused = hex(0xcfd6ddFF)
var colorRaisedHovered = hex(0xf1f3f5FF)
var colorSunken = hex(0xe9eaeaFF)
var colorSunkenFocused = hex(0xe0e6eeFF)
var colorSunkenPressed = hex(0xe0e6eeFF)
var colorCalendarWeekdayHeader = hex(0xd3cac2FF)
var colorCalendarWeekend = hex(0xc2d3c4FF)
var colorCalendarDay = hex(0xd6dae2FF)
var outline = tomo.Border {
Width: tomo.I(1),
Color: [4]color.Color {
colorOutline,
colorOutline,
colorOutline,
colorOutline,
},
}
var borderEngraved = border(0xc3c3c5FF, 0xe3e3e3FF, 0xe9e9e9ff, 0xc2c2c2ff, 1)
var borderGap = border(0x697c7cFF, 0x566767FF, 0x566767ff, 0x697c7cff, 1)
var borderLifted = border(0xf9fafcFF, 0xc2c8d3FF, 0xa4afc0ff, 0xf5f6f8ff, 1)
var borderLiftedFocused = border(0xf0f4f9FF, 0xb1bacaFF, 0x9aa6b7ff, 0xe4e9eeff, 1)
var borderFocused = border(0x5f8bc4FF, 0x5f8bc4FF, 0x5f8bc4ff, 0x5f8bc4ff, 1)
var borderTear = borderEngraved
var borderTearFocused = border(0x7f94b5FF, 0xced7e4FF, 0xced7e4FF, 0x7f94b5FF, 1)
var borderTearPad = border(0x00000000, 0x00000000, 0x00000000, 0x00000000, 3)
var borderTearPadFocused = border(0x7391c080, 0x7391c080, 0x7391c080, 0x7391c080, 3)
var borderInnerShadow = border(0xa4afc0FF, 0xa4afc0FF, 0xa4afc0ff, 0xa4afc0ff, 1, 0, 0, 1)
var borderOuterShadow = border(0xa4afc0FF, 0xa4afc0FF, 0xa4afc0ff, 0xa4afc0ff, 0, 1, 1, 0)
var rules = []style.Rule {
// *.*[*]
style.Rule {
Default: style.AS (
style.AttrFace { Face: basicfont.Face7x13 },
style.AttrTextColor { Color: tomo.ColorForeground },
style.AttrDotColor { Color: colorDot },
style.AttrGap { X: 8, Y: 8 },
),
},
// *.Button[*]
style.Rule {
Role: tomo.R("", "Button", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderLifted,
},
style.AttrPadding(tomo.I(4, 8)),
style.AttrColor { Color: tomo.ColorRaised },
),
Pressed: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrPadding(tomo.I(5, 8, 4, 9)),
style.AttrColor { Color: colorRaisedPressed },
),
Focused: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderLiftedFocused,
},
style.AttrPadding(tomo.I(4, 8)),
style.AttrColor { Color: colorRaisedFocused },
),
Hovered: style.AS (
style.AttrColor { Color: colorRaisedHovered },
),
},
// *.TextInput[*]
style.Rule {
Role: tomo.R("", "TextInput", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(5, 4, 4, 5)),
),
Focused: style.AS (
style.AttrBorder {
borderEngraved,
borderFocused,
borderInnerShadow,
},
),
},
// *.TextView[*]
style.Rule {
Role: tomo.R("", "TextView", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(8)),
),
},
// *.NumberInput[*]
style.Rule {
Role: tomo.R("", "NumberInput", ""),
Default: style.AS (
style.AttrGap { },
),
},
// *.Container[sunken]
style.Rule {
Role: tomo.R("", "Container", "sunken"),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(8)),
),
},
// *.Container[outer]
style.Rule {
Role: tomo.R("", "Container", "outer"),
Default: style.AS (
style.AttrColor { Color: tomo.ColorBackground },
style.AttrPadding(tomo.I(8)),
),
},
// *.Container[menu]
style.Rule {
Role: tomo.R("", "Container", "menu"),
Default: style.AS (
style.AttrBorder {
borderGap,
borderLifted,
},
style.AttrColor { Color: tomo.ColorBackground },
style.AttrGap { },
),
},
// *.Heading[*]
style.Rule {
Role: tomo.R("", "Heading", ""),
Default: style.AS (
style.AttrAlign { X: tomo.AlignMiddle, Y: tomo.AlignMiddle },
),
},
// *.Separator[*]
style.Rule {
Role: tomo.R("", "Separator", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
},
),
},
// *.Slider[*]
style.Rule {
Role: tomo.R("", "Slider", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: colorGutter },
),
Focused: style.AS (
style.AttrBorder {
borderEngraved,
borderFocused,
borderInnerShadow,
},
),
Hovered: style.AS (
style.AttrColor { Color: colorGutterHovered },
),
},
// *.Slider[horizontal]
style.Rule {
Role: tomo.R("", "Slider", "horizontal"),
Default: style.AS(style.AttrMinimumSize { X: 48 }),
},
// *.Slider[vertical]
style.Rule {
Role: tomo.R("", "Slider", "vertical"),
Default: style.AS(style.AttrMinimumSize { Y: 48 }),
},
// *.SliderHandle[*]
style.Rule {
Role: tomo.R("", "SliderHandle", ""),
Default: style.AS (
style.AttrBorder {
borderOuterShadow,
borderGap,
borderLifted,
},
style.AttrColor { Color: tomo.ColorRaised },
style.AttrMinimumSize { X: 12, Y: 12, },
),
},
// *.Checkbox[*]
style.Rule {
Role: tomo.R("", "Checkbox", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(0, 1, 1, 0)),
style.AttrMinimumSize { X: 19, Y: 19 },
),
Focused: style.AS (
style.AttrBorder {
borderEngraved,
borderFocused,
borderInnerShadow,
},
style.AttrPadding(tomo.I(0)),
style.AttrColor { Color: colorSunkenFocused },
),
},
// *.LabelCheckbox[*]
style.Rule {
Role: tomo.R("", "LabelCheckbox", ""),
Default: style.AS (
style.AttrGap { X: 8, Y: 8 },
),
},
// *.MenuItem[*]
style.Rule {
Role: tomo.R("", "MenuItem", ""),
Default: style.AS (
style.AttrPadding(tomo.I(4)),
style.AttrGap { X: 4, Y: 4 },
style.AttrColor { Color: color.Transparent },
),
Hovered: style.AS (
style.AttrColor { Color: colorDot },
),
Focused: style.AS (
style.AttrColor { Color: colorDot },
),
},
// *.File[*]
style.Rule {
Role: tomo.R("", "File", ""),
Default: style.AS (
style.AttrColor { Color: color.Transparent },
),
Focused: style.AS (
style.AttrColor { Color: colorDot },
),
},
// *.TearLine[*]
style.Rule {
Role: tomo.R("", "TearLine", ""),
Default: style.AS (
style.AttrBorder {
borderTearPad,
borderTear,
},
),
Hovered: style.AS (
style.AttrBorder {
borderTearPadFocused,
borderTearFocused,
},
),
Focused: style.AS (
style.AttrBorder {
borderTearPadFocused,
borderTearFocused,
},
),
},
// *.Calendar[*]
style.Rule {
Role: tomo.R("", "Calendar", ""),
Default: style.AS (
style.AttrBorder {
borderOuterShadow,
borderGap,
},
style.AttrColor { Color: tomo.ColorRaised },
style.AttrPadding(tomo.I(2)),
style.AttrGap { X: 2, Y: 2 },
),
},
// *.CalendarGrid[*]
style.Rule {
Role: tomo.R("", "CalendarGrid", ""),
Default: style.AS (
style.AttrGap { X: 2, Y: 2 },
),
},
// *.CalendarWeekdayHeader[*]
style.Rule {
Role: tomo.R("", "CalendarWeekdayHeader", ""),
Default: style.AS (
style.AttrPadding(tomo.I(2)),
style.AttrColor { Color: colorCalendarWeekdayHeader },
),
},
// *.CalendarDay[weekday]
style.Rule {
Role: tomo.R("", "CalendarDay", "weekday"),
Default: style.AS (
style.AttrPadding(tomo.I(2)),
style.AttrMinimumSize { X: 32, Y: 32 },
style.AttrColor { Color: colorCalendarDay },
),
},
// *.CalendarDay[weekend]
style.Rule {
Role: tomo.R("", "CalendarDay", "weekend"),
Default: style.AS (
style.AttrPadding(tomo.I(2)),
style.AttrMinimumSize { X: 32, Y: 32 },
style.AttrColor { Color: colorCalendarWeekend },
),
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

View File

@@ -0,0 +1,548 @@
package fallbackStyle
import "io"
import "bytes"
import "image"
import _ "embed"
import _ "image/png"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/backend/style"
var colorFocus = color.RGBA { R: 61, G: 128, B: 143, A: 255 }
var colorInput = color.RGBA { R: 208, G: 203, B: 150, A: 255 }
var colorCarved = color.RGBA { R: 151, G: 160, B: 150, A: 255 }
var colorGutter = color.RGBA { R: 116, G: 132, B: 126, A: 255 }
var colorShadow = color.RGBA { R: 57, G: 59, B: 57, A: 255 }
var colorShade = color.RGBA { A: 128 }
var colorInputShadow = color.RGBA { R: 143, G: 146, B: 91, A: 255 }
var colorHighlight = color.RGBA { R: 207, G: 215, B: 210, A: 255 }
var colorBackground = color.RGBA { R: 169, G: 171, B: 168, A: 255 }
var colorCarvedPressed = color.RGBA { R: 129, G: 142, B: 137, A: 255 }
var colorForeground = color.Black
var colorOutline = color.Black
var colorCalendarWeekdayHeader = color.RGBA { R: 194, G: 162, B: 132, A: 255 }
var colorCalendarWeekend = color.RGBA { R: 165, G: 185, B: 120, A: 255 }
var colorCalendarDay = color.RGBA { R: 194, G: 189, B: 132, A: 255 }
var colorInactive = color.RGBA { R: 131, G: 147, B: 134, A: 255 }
var outline = tomo.Border {
Width: tomo.I(1),
Color: [4]color.Color {
colorOutline,
colorOutline,
colorOutline,
colorOutline,
},
}
var borderColorOutline = [4]color.Color { colorOutline, colorOutline, colorOutline, colorOutline }
var borderColorEngraved = [4]color.Color { colorShadow, colorHighlight, colorHighlight, colorShadow }
var borderColorLifted = [4]color.Color { colorHighlight, colorShadow, colorShadow, colorHighlight }
var borderColorInput = [4]color.Color { colorInputShadow, colorInput, colorInput, colorInputShadow }
var borderColorFocused = [4]color.Color { colorFocus, colorFocus, colorFocus, colorFocus }
var borderColorShade = [4]color.Color { colorShade, colorShade, colorShade, colorShade }
//go:embed assets/atlas.png
var atlasBytes []byte
type closerCookie struct { io.Closer }
func (cookie closerCookie) Close () { cookie.Closer.Close() }
func newCloserCookie (closer io.Closer) event.Cookie {
return closerCookie { Closer: closer }
}
// New returns Wintergreen, the default Tomo style. It is neutral-gray with
// green and turquoise accents.
func New () (*style.Style, event.Cookie) {
atlasImage, _, err := image.Decode(bytes.NewReader(atlasBytes))
if err != nil { panic(err) }
atlasTexture := tomo.NewTexture(atlasImage)
textureCheckboxChecked := atlasTexture.SubTexture(image.Rect( 0, 0, 12, 11))
textureCorkboard := atlasTexture.SubTexture(image.Rect(16, 0, 28, 12))
textureTearLine := atlasTexture.SubTexture(image.Rect(16, 12, 18, 13))
textureHandleVertical := atlasTexture.SubTexture(image.Rect(28, 0, 29, 2))
textureHandleHorizontal := atlasTexture.SubTexture(image.Rect(28, 0, 30, 1))
cookie := event.MultiCookie(newCloserCookie(atlasTexture))
rules := []style.Rule {
// *.*
style.Ru(style.AS (
tomo.ATextColor (tomo.ColorForeground),
tomo.ADotColor (tomo.ColorAccent ),
tomo.AGap (8, 8 ),
), tomo.R("", "")),
// *.Button
style.Ru(style.AS (
tomo.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
tomo.AttrPadding(tomo.I(4, 8)),
tomo.AttrColor { Color: tomo.ColorRaised },
), tomo.R("", "Button")),
// *.Button[focused]
style.Ru(style.AS (
tomo.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
), tomo.R("", "Button"), "focused"),
// *.Button[pressed]
style.Ru(style.AS (
tomo.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
tomo.AttrPadding(tomo.I(5, 8, 4, 9)),
tomo.AttrColor { Color: colorCarvedPressed },
), tomo.R("", "Button"), "pressed"),
// *.TextInput
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorInput,
}),
tomo.AColor(colorInput),
tomo.APadding(5, 4, 4, 5),
), tomo.R("", "TextInput")),
// *.TextInput[focused]
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
}),
), tomo.R("", "TextInput"), "focused"),
// *.TextView
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
}),
tomo.AColor(tomo.ColorSunken),
tomo.APadding(8),
), tomo.R("", "TextView")),
// *.NumberInput
style.Ru(style.AS (
tomo.AGap(0, 0),
), tomo.R("", "NumberInput")),
// *.Container[sunken]
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
}),
tomo.AColor(nil),
tomo.ATexture(textureCorkboard),
tomo.APadding(8),
), tomo.R("", "Container"), "sunken"),
// *.Container[outer]
style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorBackground },
tomo.AttrPadding(tomo.I(8)),
), tomo.R("", "Container"), "outer"),
// *.Container[menu]
style.Ru(style.AS (
tomo.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
tomo.AttrColor { Color: tomo.ColorBackground },
tomo.AttrGap { },
), tomo.R("", "Container"), "menu"),
// *.Heading
style.Ru(style.AS (
tomo.AAlign(tomo.AlignMiddle, tomo.AlignMiddle),
), tomo.R("", "Heading")),
// *.Separator
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(1),
Color: borderColorEngraved,
},
},
), tomo.R("", "Separator")),
// *.Slider
style.Ru(style.AS (
tomo.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
tomo.AttrColor { Color: colorGutter },
tomo.AttrPadding(tomo.I(0, 1, 1, 0)),
), tomo.R("", "Slider")),
// *.Slider[focused]
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
}),
tomo.APadding(0),
), tomo.R("", "Slider"), "focused"),
// *.Slider[horizontal]
style.Ru(style.AS (
tomo.AMinimumSize(48, 0),
), tomo.R("", "Slider"), "horizontal"),
// *.Slider[vertical]
style.Ru(style.AS (
tomo.AMinimumSize(0, 48),
), tomo.R("", "Slider"), "vertical"),
// *.SliderHandle
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
tomo.Border {
Width: tomo.I(1),
Color: [4]color.Color {
tomo.ColorRaised, tomo.ColorRaised,
tomo.ColorRaised, tomo.ColorRaised,
},
}),
tomo.AColor(nil),
tomo.ATexture(textureHandleVertical),
tomo.AMinimumSize(12, 12),
), tomo.R("", "SliderHandle")),
// *.SliderHandle[horizontal]
style.Ru(style.AS (
tomo.ATexture(textureHandleHorizontal),
), tomo.R("", "SliderHandle"), "horizontal"),
// *.ScrollContainer
style.Ru(style.AS (
tomo.AGap(0, 0),
), tomo.R("", "ScrollContainer")),
// *.Checkbox
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
}),
tomo.AColor(tomo.ColorSunken),
tomo.APadding(0, 1, 1, 0),
tomo.AMinimumSize(19, 19),
tomo.ATexture(nil),
tomo.ATextureMode(tomo.TextureModeCenter),
), tomo.R("", "Checkbox")),
// *.Checkbox[focused]
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
}),
tomo.APadding(0),
), tomo.R("", "Checkbox"), "focused"),
// *.Checkbox[checked]
style.Ru(style.AS (
tomo.ATexture(textureCheckboxChecked),
), tomo.R("", "Checkbox"), "checked"),
// *.MenuItem
style.Ru(style.AS (
tomo.AttrPadding(tomo.I(4)),
tomo.AttrGap { X: 4, Y: 4 },
tomo.AttrColor { Color: color.Transparent },
), tomo.R("", "MenuItem")),
// *MenuItem[focused]
style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorAccent },
), tomo.R("", "MenuItem"), "focused"),
// *.MenuItem[hovered]
style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorAccent },
), tomo.R("", "MenuItem"), "hovered"),
// *.File
style.Ru(style.AS (
tomo.AttrColor { Color: color.Transparent },
), tomo.R("", "File")),
// *.File[focused]
style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorAccent },
), tomo.R("", "File"), "focused"),
// *.TearLine
style.Ru(style.AS (
tomo.ABorder (
tomo.Border {
Width: tomo.I(3),
Color: [4]color.Color {
color.Transparent,
color.Transparent,
color.Transparent,
color.Transparent,
},
}),
tomo.ATexture(textureTearLine),
tomo.APadding(1, 0, 0, 1),
), tomo.R("", "TearLine")),
// *.TearLine[focused]
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(3),
Color: borderColorFocused,
},
},
), tomo.R("", "TearLine"), "focused"),
// *.TearLine[hovered]
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(3),
Color: borderColorFocused,
},
},
), tomo.R("", "TearLine"), "hovered"),
// *.Calendar
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(0, 1, 1, 0),
Color: borderColorShade,
},
outline,
},
tomo.AttrColor { Color: colorInput },
tomo.AttrPadding(tomo.I(2)),
tomo.AttrGap { X: 2, Y: 2 },
), tomo.R("", "Calendar")),
// *.CalendarGrid
style.Ru(style.AS (
tomo.AttrGap { X: 2, Y: 2 },
), tomo.R("", "CalendarGrid")),
// *.CalendarWeekdayHeader
style.Ru(style.AS (
tomo.AttrPadding(tomo.I(2)),
tomo.AttrColor { Color: colorCalendarWeekdayHeader },
), tomo.R("", "CalendarWeekdayHeader")),
// *.CalendarDay[weekday]
style.Ru(style.AS (
tomo.AttrPadding(tomo.I(2)),
tomo.AttrMinimumSize { X: 32, Y: 32 },
tomo.AttrColor { Color: colorCalendarDay },
), tomo.R("", "CalendarDay"), "weekday"),
// *.CalendarDay[weekend]
style.Ru(style.AS (
tomo.AttrPadding(tomo.I(2)),
tomo.AttrMinimumSize { X: 32, Y: 32 },
tomo.AttrColor { Color: colorCalendarWeekend },
), tomo.R("", "CalendarDay"), "weekend"),
// *.TabbedContainer
style.Ru(style.AS (
tomo.AGap(0, 0),
), tomo.R("", "TabbedContainer")),
// *.TabRow
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(1, 1, 0, 1),
Color: borderColorOutline,
},
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
tomo.AttrGap { X: 0, Y: 0 },
tomo.AttrColor { Color: colorGutter },
tomo.AttrPadding(tomo.I(1, 0, 0, 0)),
), tomo.R("", "TabRow")),
// *.TabSpacer[left]
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(0, 0, 1, 0),
Color: borderColorEngraved,
},
tomo.Border {
Width: tomo.I(0, 0, 1, 0),
Color: borderColorOutline,
},
},
tomo.AttrMinimumSize { X: 1 },
), tomo.R("", "TabSpacer")),
// *.TabSpacer[right]
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(1, 0, 0, 0),
Color: [4]color.Color {
colorGutter, colorGutter,
colorGutter, colorGutter,
},
},
tomo.Border {
Width: tomo.I(0, 0, 1, 0),
Color: borderColorEngraved,
},
tomo.Border {
Width: tomo.I(0, 0, 1, 1),
Color: borderColorOutline,
},
tomo.Border {
Width: tomo.I(0, 0, 0, 1),
Color: borderColorShade,
},
},
tomo.AttrMinimumSize { X: 3 },
), tomo.R("", "TabSpacer"), "right"),
// *.Tab
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(1, 0, 0, 0),
Color: [4]color.Color {
colorGutter, colorGutter,
colorGutter, colorGutter,
},
},
tomo.Border {
Width: tomo.I(0, 0, 1, 0),
Color: borderColorEngraved,
},
tomo.Border {
Width: tomo.I(1, 0, 1, 1),
Color: borderColorOutline,
},
tomo.Border {
Width: tomo.I(1, 1, 0, 1),
Color: borderColorLifted,
},
},
tomo.AttrPadding(tomo.I(4, 8, 4, 8)),
tomo.AttrColor { Color: tomo.ColorRaised },
), tomo.R("", "Tab")),
// *.Tab[active]
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorOutline,
},
tomo.Border {
Width: tomo.I(1, 1, 0, 1),
Color: borderColorLifted,
},
},
tomo.AttrPadding(tomo.I(4, 8, 4, 8)),
tomo.AttrColor { Color: tomo.ColorBackground },
), tomo.R("", "Tab"), "active"),
// *.Swatch
style.Ru(style.AS (
tomo.AttrBorder {
outline,
},
tomo.AttrMinimumSize { X: 19, Y: 19 },
), tomo.R("", "Swatch")),
// *.Swatch[focused]
style.Ru(style.AS (
tomo.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
), tomo.R("", "Swatch"), "focused"),
// *.ColorPickerMap
style.Ru(style.AS (
tomo.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
tomo.AttrColor { Color: tomo.ColorSunken },
tomo.AttrMinimumSize { X: 128, Y: 128 },
), tomo.R("", "ColorPickerMap")),
}
return &style.Style {
Rules: rules,
Colors: map[tomo.Color] color.Color {
tomo.ColorBackground: colorBackground,
tomo.ColorForeground: colorForeground,
tomo.ColorRaised: colorCarved,
tomo.ColorSunken: colorCarved,
tomo.ColorAccent: colorFocus,
},
}, cookie
}

View File

@@ -0,0 +1,21 @@
$colorBlack = #000000FF;
$borderOutline = $black 1;
*.Slider {
Border: $borderOutline, $borderColorFocused 1;
Color: $colorGutter;
Padding: 0 1 1 0;
}
*.Slider[focused] {
Border: $borderOutline;
Padding: 0;
}
*.Slider[horizontal] {
MinimumSize: 48 0;
}
*.Slider[vertical] {
MinimumSize: 0 48;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,373 @@
package tss
import "fmt"
import "image"
import "errors"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/backend/style"
type styleBuilder struct {
sheet Sheet
}
type faceKey struct {
name string
size int
}
// 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,
}
return builder.build()
}
func (this *styleBuilder) build () (*style.Style, event.Cookie, error) {
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 }
}
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"),
},
}
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
}
// TODO include all the faces in this.faces (ONCE EACH) in the
// multicookie
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":
// 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, 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 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
}
}

View File

@@ -0,0 +1,17 @@
package tss
import "testing"
func TestValueColor (test *testing.T) {
testValueColorRGBA(test, 0xFB380CFF, 0xFBFB, 0x3838, 0x0C0C, 0xFFFF)
testValueColorRGBA(test, 0xFB380C00, 0x0000, 0x0000, 0x0000, 0x0000)
}
func testValueColorRGBA (test *testing.T, col ValueColor, r, g, b, a uint32) {
gr, gg, gb, ga := col.RGBA()
test.Logf("testing RGBA for color #%08X", col)
if gr != r { test.Errorf("r component inequal (%04X != %04X)", gr, r) }
if gg != g { test.Errorf("g component inequal (%04X != %04X)", gg, g) }
if gb != b { test.Errorf("b component inequal (%04X != %04X)", gb, b) }
if ga != a { test.Errorf("a component inequal (%04X != %04X)", ga, a) }
}

View File

@@ -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
}

363
internal/styles/tss/lex.go Normal file
View File

@@ -0,0 +1,363 @@
package tss
import "io"
import "bufio"
import "unicode"
import "unicode/utf8"
import "git.tebibyte.media/sashakoshka/goparse"
const (
Comment parse.TokenKind = iota
LBrace
RBrace
LBracket
RBracket
Equals
Colon
Comma
Semicolon
Star
Dot
Dollar
Slash
Color
Ident
Number
String
)
var tokenNames = map[parse.TokenKind] string {
parse.EOF: "EOF",
Comment: "Comment",
LBrace: "LBrace",
RBrace: "RBrace",
LBracket: "LBracket",
RBracket: "RBracket",
Equals: "Equals",
Colon: "Colon",
Comma: "Comma",
Semicolon: "Semicolon",
Star: "Star",
Dot: "Dot",
Dollar: "Dollar",
Slash: "Slash",
Color: "Color",
Ident: "Ident",
Number: "Number",
String: "String",
}
type lexer struct {
filename string
lineScanner *bufio.Scanner
rune rune
line string
lineFood string
offset int
row int
column int
eof bool
}
func Lex (filename string, reader io.Reader) parse.Lexer {
lex := &lexer {
filename: filename,
lineScanner: bufio.NewScanner(reader),
}
lex.nextRune()
return lex
}
func (this *lexer) Next () (parse.Token, error) {
for {
token, err := this.next()
if err == io.EOF { return token, this.errUnexpectedEOF() }
if err != nil { return token, err }
if !token.Is(Comment) {
return token, err
}
}
}
func (this *lexer) next () (token parse.Token, err error) {
err = this.skipWhitespace()
token.Position = this.pos()
if this.eof {
token.Kind = parse.EOF
err = nil
return
}
if err != nil { return }
appendRune := func () {
token.Value += string(this.rune)
err = this.nextRune()
}
skipRune := func () {
err = this.nextRune()
}
defer func () {
newPos := this.pos()
newPos.End --
token.Position = token.Position.Union(newPos)
} ()
switch {
case this.rune == '/':
token.Kind = Comment
skipRune()
if err != nil { return }
if this.rune == '/' {
for this.rune != '\n' {
skipRune()
if err != nil { return }
}
} else {
token.Kind = Slash
}
if this.eof { err = nil; return }
case this.rune == '{':
token.Kind = LBrace
appendRune()
if this.eof { err = nil; return }
case this.rune == '}':
token.Kind = RBrace
appendRune()
if this.eof { err = nil; return }
case this.rune == '[':
token.Kind = LBracket
appendRune()
if this.eof { err = nil; return }
case this.rune == ']':
token.Kind = RBracket
appendRune()
if this.eof { err = nil; return }
case this.rune == '=':
token.Kind = Equals
appendRune()
if this.eof { err = nil; return }
case this.rune == ':':
token.Kind = Colon
appendRune()
if this.eof { err = nil; return }
case this.rune == ',':
token.Kind = Comma
appendRune()
if this.eof { err = nil; return }
case this.rune == ';':
token.Kind = Semicolon
appendRune()
if this.eof { err = nil; return }
case this.rune == '*':
token.Kind = Star
appendRune()
if this.eof { err = nil; return }
case this.rune == '.':
token.Kind = Dot
appendRune()
if this.eof { err = nil; return }
case this.rune == '$':
token.Kind = Dollar
appendRune()
if this.eof { err = nil; return }
case this.rune == '#':
token.Kind = Color
skipRune()
if err != nil { return }
for isHexDigit(this.rune) {
appendRune()
if this.eof { err = nil; return }
}
if this.eof { err = nil; return }
case unicode.IsLetter(this.rune):
token.Kind = Ident
for unicode.IsLetter(this.rune) || unicode.IsNumber(this.rune) {
appendRune()
if this.eof { err = nil; return }
}
if this.eof { err = nil; return }
case this.rune == '-':
token.Kind = Number
appendRune()
for isDigit(this.rune) {
appendRune()
if this.eof { err = nil; return }
}
if this.eof { err = nil; return }
case isDigit(this.rune):
token.Kind = Number
for isDigit(this.rune) {
appendRune()
if this.eof { err = nil; return }
}
if this.eof { err = nil; return }
case this.rune == '\'', this.rune == '"':
stringDelimiter := this.rune
token.Kind = String
err = this.nextRune()
if err != nil { return }
for this.rune != stringDelimiter {
if this.rune == '\\' {
var result rune
result, err = this.escapeSequence(stringDelimiter)
if err != nil { return }
token.Value += string(result)
} else {
appendRune()
if this.eof { err = nil; return }
if err != nil { return }
}
}
err = this.nextRune()
if this.eof { err = nil; return }
if err != nil { return }
default:
err = parse.Errorf (
this.pos(), "unexpected rune %U",
this.rune)
}
return
}
func (this *lexer) nextRune () error {
if this.lineFood == "" {
ok := this.lineScanner.Scan()
if ok {
this.line = this.lineScanner.Text()
this.lineFood = this.line
this.rune = '\n'
this.column = 0
this.row ++
} else {
err := this.lineScanner.Err()
if err == nil {
this.eof = true
return io.EOF
} else {
return err
}
}
} else {
var ch rune
var size int
for ch == 0 && this.lineFood != "" {
ch, size = utf8.DecodeRuneInString(this.lineFood)
this.lineFood = this.lineFood[size:]
}
this.rune = ch
this.column ++
}
return nil
}
func (this *lexer) escapeSequence (stringDelimiter rune) (rune, error) {
err := this.nextRune()
if err != nil { return 0, err }
if isDigit(this.rune) {
var number rune
for index := 0; index < 3; index ++ {
if !isDigit(this.rune) { break }
number *= 8
number += this.rune - '0'
err = this.nextRune()
if err != nil { return 0, err }
}
return number, nil
}
defer this.nextRune()
switch this.rune {
case '\\', '\n', stringDelimiter:
return this.rune, nil
case 'a': return '\a', nil
case 'b': return '\b', nil
case 't': return '\t', nil
case 'n': return '\n', nil
case 'v': return '\v', nil
case 'f': return '\f', nil
case 'r': return '\r', nil
default: return 0, this.errBadEscapeSequence()
}
}
func (this *lexer) skipWhitespace () error {
for isWhitespace(this.rune) {
err := this.nextRune()
if err != nil { return err }
}
return nil
}
func (this *lexer) pos () parse.Position {
return parse.Position {
File: this.filename,
Line: this.lineScanner.Text(),
Row: this.row - 1,
Start: this.column - 1,
End: this.column,
}
}
func (this *lexer) errUnexpectedEOF () error {
return parse.Errorf(this.pos(), "unexpected EOF")
}
func (this *lexer) errBadEscapeSequence () error {
return parse.Errorf(this.pos(), "bad escape sequence")
}
func isWhitespace (char rune) bool {
switch char {
case ' ', '\t', '\r', '\n': return true
default: return false
}
}
func isSymbol (char rune) bool {
switch char {
case
'~', '!', '@', '#', '$', '%', '^', '&', '-', '_', '=', '+',
'\\', '|', ';', ',', '<', '>', '/', '?':
return true
default:
return false
}
}
func isDigit (char rune) bool {
return char >= '0' && char <= '9'
}
func isHexDigit (char rune) bool {
return isDigit(char) ||
char >= 'a' && char <= 'f' ||
char >= 'A' && char <= 'F'
}

View File

@@ -0,0 +1,66 @@
package tss
import "fmt"
import "strings"
import "testing"
import "git.tebibyte.media/sashakoshka/goparse"
func TestLexSimple (test *testing.T) {
testString(test,
`hello #BABE {#Beef}, 384920 #0ab3fc840`,
tok(Ident, "hello"),
tok(Color, "BABE"),
tok(LBrace, "{"),
tok(Color, "Beef"),
tok(RBrace, "}"),
tok(Comma, ","),
tok(Number, "384920"),
tok(Color, "0ab3fc840"),
tok(parse.EOF, ""),
)}
func testString (test *testing.T, input string, correct ...parse.Token) {
lexer := Lex("test.tss", strings.NewReader(input))
index := 0
for {
token, err := lexer.Next()
if err != nil { test.Fatalf("lexer returned error:\n%v", parse.Format(err)) }
if index >= len(correct) {
test.Logf("%d:\t%-16s | !", index, tokStr(token))
test.Fatalf("index %d greater than %d", index, len(correct))
}
correctToken := correct[index]
test.Logf (
"%d:\t%-16s | %s",
index,
tokStr(token),
tokStr(correctToken))
if correctToken.Kind != token.Kind || correctToken.Value != token.Value {
test.Fatalf("tokens at %d do not match up", index)
}
if token.Is(parse.EOF) { break }
index ++
}
if index < len(correct) - 1 {
test.Fatalf("index %d less than %d", index, len(correct) - 1)
}
}
func tokStr (token parse.Token) string {
name, ok := tokenNames[token.Kind]
if !ok {
name = fmt.Sprintf("Token(%d)", token.Kind)
}
if token.Value == "" {
return name
} else {
return fmt.Sprintf("%s:\"%s\"", name, token.Value)
}
}
func tok (kind parse.TokenKind, value string) parse.Token {
return parse.Token {
Kind: kind,
Value: value,
}
}

View File

@@ -0,0 +1,253 @@
package tss
import "io"
import "strconv"
import "git.tebibyte.media/sashakoshka/goparse"
type parser struct {
parse.Parser
sheet Sheet
lexer parse.Lexer
}
func newParser (lexer parse.Lexer) *parser {
return &parser {
sheet: Sheet {
Variables: make(map[string] ValueList),
},
Parser: parse.Parser {
Lexer: lexer,
TokenNames: tokenNames,
},
}
}
func Parse (lexer parse.Lexer) (Sheet, error) {
parser := newParser(lexer)
err := parser.parse()
if err == io.EOF { err = nil }
if err != nil { return Sheet { }, err }
return parser.sheet, nil
}
func (this *parser) parse () error {
err := this.Next()
if err != nil { return err }
for this.Token.Kind != parse.EOF {
err = this.parseTopLevel()
if err != nil { return err }
}
return nil
}
func (this *parser) parseTopLevel () error {
err := this.ExpectDesc("variable or rule", Dollar, Ident, Star)
if err != nil { return err }
if this.EOF() { return nil }
pos := this.Pos()
switch this.Kind() {
case Dollar:
name, variable, err := this.parseVariable()
if err != nil { return err }
if _, exists := this.sheet.Variables[name]; exists {
return parse.Errorf(pos, "variable %s already declared", name)
}
this.sheet.Variables[name] = variable
case Ident, Star:
rule, err := this.parseRule()
if err != nil { return err }
this.sheet.Rules = append(this.sheet.Rules, rule)
}
return nil
}
func (this *parser) parseVariable () (string, ValueList, error) {
err := this.Expect(Dollar)
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 }
this.Next()
values, err := this.parseValueList()
if err != nil { return "", nil, err }
err = this.Expect(Semicolon)
if err != nil { return "", nil, err }
return name, values, this.Next()
}
func (this *parser) parseRule () (Rule, error) {
rule := Rule {
Attrs: make(map[string] []ValueList),
}
selector, err := this.parseSelector()
if err != nil { return Rule { }, err }
rule.Selector = selector
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 { 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",
name)
}
rule.Attrs[name] = attr
}
return rule, this.Next()
}
func (this *parser) parseSelector () (Selector, error) {
selector := Selector { }
// package
err := this.ExpectDesc("selector", Ident, Star)
if err != nil { return Selector { }, err }
if this.Is(Ident) {
selector.Package = this.Value()
}
err = this.ExpectNext(Dot)
if err != nil { return Selector { }, err }
// object
err = this.ExpectNext(Ident, Star)
if err != nil { return Selector { }, err }
if this.Is(Ident) {
selector.Object = this.Value()
}
// tags
err = this.ExpectNext(LBracket)
if err == nil {
this.Next()
for {
err := this.Expect(Ident, String, RBracket)
if err != nil { return Selector { }, err }
if this.Is(RBracket) { break }
if this.Is(Comma) { this.Next() }
selector.Tags = append(selector.Tags, this.Value())
err = this.ExpectNext(Comma, RBracket)
if err != nil { return Selector { }, err }
}
this.Next()
}
return selector, nil
}
func (this *parser) parseAttr () (string, []ValueList, error) {
err := this.ExpectDesc("attr", Ident)
if err != nil { return "", nil, err }
name := this.Value()
err = this.ExpectNext(Colon)
if err != nil { return "", nil, err }
attr := []ValueList { }
this.Next()
for {
err := this.ExpectDesc (
"value, Comma, or Semicolon",
Number, Color, String, Ident, Dollar, Slash,
Comma, Semicolon)
if err != nil { return "", nil, err }
if this.Is(Semicolon) { break }
if this.Is(Comma) { this.Next() }
valueList, err := this.parseValueList()
if err != nil { return "", nil, err }
attr = append(attr, valueList)
err = this.Expect(Comma, Semicolon)
if err != nil { return "", nil, err }
}
return name, attr, nil
}
func (this *parser) parseValueList () (ValueList, error) {
list := ValueList { }
for {
err := this.ExpectDesc (
"value",
Number, Color, String, Ident, Dollar, Slash)
if err != nil { break }
switch this.Kind() {
case Number:
number, err := strconv.Atoi(this.Value())
if err != nil { return nil, err }
list = append(list, ValueNumber(number))
case Color:
color, ok := parseColor([]rune(this.Value()))
if !ok {
return nil, parse.Errorf (
this.Pos(),
"malformed color literal")
}
list = append(list, ValueColor(color))
case String:
list = append(list, ValueString(this.Value()))
case Ident:
list = append(list, ValueKeyword(this.Value()))
case Dollar:
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, nil
}
func parseColor (runes []rune) (uint32, bool) {
digits := make([]uint32, len(runes))
for index, run := range runes {
digit := hexDigit(run)
if digit < 0 { return 0, false }
digits[index] = uint32(digit)
}
switch len(runes) {
case 3:
return digits[0] << 28 | digits[0] << 24 |
digits[1] << 20 | digits[1] << 16 |
digits[2] << 12 | digits[2] << 8 | 0xFF, true
case 6:
return digits[0] << 28 | digits[1] << 24 |
digits[2] << 20 | digits[3] << 16 |
digits[4] << 12 | digits[5] << 8 | 0xFF, true
case 4:
return digits[0] << 28 | digits[0] << 24 |
digits[1] << 20 | digits[1] << 16 |
digits[2] << 12 | digits[2] << 8 |
digits[3] << 4 | digits[3] << 0, true
case 8:
return digits[0] << 28 | digits[1] << 24 |
digits[2] << 20 | digits[3] << 16 |
digits[4] << 12 | digits[5] << 8 |
digits[6] << 4 | digits[7] << 0, true
default: return 0, false
}
}
func hexDigit (digit rune) int {
switch {
case digit >= '0' && digit <= '9': return int(digit - '0')
case digit >= 'a' && digit <= 'f': return int(digit - 'a') + 10
case digit >= 'A' && digit <= 'F': return int(digit - 'A') + 10
default: return -1
}
}

View File

@@ -0,0 +1,79 @@
package tss
import "os"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/backend/style"
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 & 0xFF000000) >> 24
g = (bits & 0x00FF0000) >> 16
b = (bits & 0x0000FF00) >> 8
a = (bits & 0x000000FF)
// 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) / 0xFFFF
g = (g * a) / 0xFFFF
b = (b * a) / 0xFFFF
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) (*style.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()
sheet, err := Parse(Lex(name, file))
if err != nil { return nil, nil, err }
return BuildStyle(sheet)
}

View File

@@ -0,0 +1,32 @@
# Happen to use Micro to edit text?
# Drop this in ~/.config/micro/syntax and get syntax highlighting for TSS files!
filetype: tss
detect:
filename: "\\.tss$"
rules:
- type: "\\b([A-Z][a-zA-Z0-9]*).*:"
- identifier.var: "\\$[a-zA-Z0-9]*\\b"
- identifier.class: "(\\*|[a-z][a-zA-Z0-9]*)\\.(\\*|[A-Z][a-zA-Z0-9]*)"
- special: "(\\/|,|\\;|:|\\.)"
- symbol.operator: "(=|\\*)"
- symbol.brackets: "(\\{\\[|\\}\\])"
- comment:
start: "//"
end: "$"
rules:
- todo: "(TODO|XXX|FIXME|BUG):?"
- constant.string:
start: "\""
end: "\""
skip: "\\\\."
rules:
- constant.specialChar: "\\\\[abfnrtv'\\\"\\\\]"
- constant.specialChar: "\\\\([0-7]{3})"
- constant.number: "\\b[0-9][0-9.]*\\b"
- constant.string: "\\B#[0-9a-fA-F]*"