36 Commits

Author SHA1 Message Date
f676c2855e Aluminum has complete styling for tabs 2024-08-16 19:51:55 -04:00
269d302453 Style Scrollbar separately in Aluminum 2024-08-16 18:40:26 -04:00
fbdc285f2e Style ScrollBar separately in fallback 2024-08-16 18:39:00 -04:00
696460f323 Add Aluminum styling for Swatch, ColorPickerMap, Dropdown 2024-08-16 18:23:49 -04:00
ece3a3e2a0 Remove old aluminum style
Archival dirs are pointless, we have git for a reason
2024-08-16 18:12:15 -04:00
18b928acf6 Update dependency versions 2024-08-16 18:12:05 -04:00
7f0c34760d Add new icons to fallback icon set 2024-08-16 17:56:27 -04:00
8810e88422 Remove checkbox icons from fallback icon set 2024-08-16 17:30:20 -04:00
bc09a01aa7 Update Aluminum style 2024-08-16 17:26:47 -04:00
a03eab9898 Add fallback styling for dropdowns 2024-08-16 17:26:20 -04:00
e0f90f8305 Update Tomo API 2024-08-16 17:25:54 -04:00
e46d66885d Slight style tweaks to Wintergreen 2024-08-14 11:59:03 -04:00
7c5de5a370 Fix TSS parsing multiple tags 2024-08-14 11:58:33 -04:00
7da00e990e Nasin now counts application windows to determine when to shut down 2024-08-14 11:58:06 -04:00
bc26e78024 Update backend 2024-08-12 20:57:17 -04:00
5dcfdda5f1 Add checked checkbox texture to aluminum stylesheet 2024-08-12 20:16:45 -04:00
77e335a238 TSS now supports PNG image textures 2024-08-12 20:16:29 -04:00
24357bf19f TSS sheets store their file name 2024-08-12 19:13:25 -04:00
3ed8b70895 Update dependency versions 2024-08-12 19:07:57 -04:00
7db4c95592 Made ColorDot transparent like it was before 2024-08-12 19:06:29 -04:00
c29abe0cb1 Update TSS syntax highlighting file with constants 2024-08-12 17:26:31 -04:00
4b53a5a019 Renamed style directory to styles 2024-08-12 17:12:43 -04:00
ecfb90957a Re-organized style directory 2024-08-12 17:10:43 -04:00
16584abb42 Add micro syntax highlighting file for TSS 2024-08-12 17:07:59 -04:00
3e597404ac Added SetFaceSet to registrar 2024-08-11 10:42:22 -04:00
fa91b4f415 Remove outdated TODO 2024-08-11 10:36:58 -04:00
3a4038dad9 Update registrar 2024-08-11 10:36:29 -04:00
a8bc074aad Remove font logic from TSS 2024-08-10 22:20:34 -04:00
b0672ec8ee Add a fallback face set 2024-08-10 22:12:31 -04:00
961366b00a Rename fallback icon set 2024-08-10 22:12:21 -04:00
3127aad09a Update xdg icon set 2024-08-10 21:56:03 -04:00
1feb5f4ab1 Update fallback style 2024-08-10 21:55:05 -04:00
fee4e584e7 Update fallback icons 2024-08-10 21:54:57 -04:00
98fb8a3e01 We load fonts but it's a bit broken 2024-07-30 13:01:24 -04:00
90072d8a9e Test and fix ValueColor.RGBA 2024-07-29 15:45:17 -04:00
f42dee22f5 Initial stylesheet support 2024-07-29 15:13:02 -04:00
29 changed files with 1315 additions and 629 deletions

View File

@@ -15,8 +15,9 @@ type Application interface {
// Describe returns a description of the application. // Describe returns a description of the application.
Describe () ApplicationDescription Describe () ApplicationDescription
// Init performs the initial setup of the application. // Init performs the initial setup of the application. This behavior
Init () error // should return a window if it creates one.
Init () (tomo.Window, error)
} }
// ApplicationURLOpener is an application that can open a URL. // ApplicationURLOpener is an application that can open a URL.
@@ -29,12 +30,15 @@ type ApplicationURLOpener interface {
// //
// Applications should support the file:// scheme at the very least, and // Applications should support the file:// scheme at the very least, and
// should also support others like http:// and https:// if possible. // should also support others like http:// and https:// if possible.
OpenURL (*url.URL) error //
// This behavior should return a window if it creates one.
OpenURL (*url.URL) (tomo.Window, error)
// OpenNone is called when the application is launched without any URLs // OpenNone is called when the application is launched without any URLs
// to open. The application may create some sort of default starting // to open. The application may create some sort of default starting
// window, or call tomo.Stop(). // window, or do nothing. This behavior should return a window if it
OpenNone () // creates one.
OpenNone () (tomo.Window, error)
} }
// ApplicationFlagAdder is an application that supports reading command line // ApplicationFlagAdder is an application that supports reading command line
@@ -65,6 +69,15 @@ type ApplicationDescription struct {
Role ApplicationRole 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. // String satisfies the fmt.Stringer interface.
func (application ApplicationDescription) String () string { func (application ApplicationDescription) String () string {
if application.Name == "" { if application.Name == "" {
@@ -119,8 +132,9 @@ func (role ApplicationRole) Icon () tomo.Icon {
} }
} }
// RunApplication is like tomo.Run, but runs an application. If something fails // RunApplication is like tomo.Run, but runs an application. It automatically
// to initialize, an error is written to the standard logger. // sets up a backend. If something fails to initialize, an error is written to
// the standard logger.
func RunApplication (application Application) { func RunApplication (application Application) {
// TODO: see #4 // TODO: see #4
@@ -129,21 +143,28 @@ func RunApplication (application Application) {
} }
flag.Parse() flag.Parse()
err := registrar.RegisterBackend() reg := new(registrar.Registrar)
backend, err := reg.SetBackend()
if err != nil { log.Fatalln("nasin: could not register backend:", err) } if err != nil { log.Fatalln("nasin: could not register backend:", err) }
err = tomo.Run(func () { err = reg.SetTheme()
err := registrar.SetTheme()
err := registrar.SetIconSet()
if err != nil { log.Fatalln("nasin: could not set theme:", err) } if err != nil { log.Fatalln("nasin: could not set theme:", err) }
err = application.Init() err = reg.SetIconSet()
if err != nil { log.Fatalln("nasin: could not set icon set:", err) }
err = reg.SetFaceSet()
if err != nil { log.Fatalln("nasin: could not set face set:", err) }
window, err := application.Init()
if err != nil { log.Fatalln("nasin: could not run application:", err) } if err != nil { log.Fatalln("nasin: could not run application:", err) }
manageWindow(window)
// open URLs // open URLs
args := flag.Args() args := flag.Args()
applicationOpenUrls(application, args...) applicationOpenUrls(application, args...)
})
if windows > 0 {
err = backend.Run()
if err != nil { log.Fatalln("nasin: could not run application:", err) } if err != nil { log.Fatalln("nasin: could not run application:", err) }
} }
}
// NewApplicationWindow creates a window for an application. It will // NewApplicationWindow creates a window for an application. It will
// automatically set window information to signal to the OS that the window is // automatically set window information to signal to the OS that the window is
@@ -160,10 +181,54 @@ func NewApplicationWindow (application Application, bounds image.Rectangle) (tom
return window, nil return window, nil
} }
func applicationOpenUrls (application Application, args ...string) { var windows int
if application, ok := application.(ApplicationURLOpener); ok {
func manageWindow (window tomo.Window) {
if window == nil { return }
windows ++
window.OnClose(func () {
windows --
if windows < 1 {
tomo.Stop()
}
})
}
func errorPopupf (title, format string, v ...any) func (func ()) {
return func (callback func ()) {
dialog, err := objects.NewDialogOk (
objects.DialogError, nil,
title,
fmt.Sprintf(format, v...),
callback)
if err != nil { log.Fatal(err) }
dialog.SetVisible(true)
manageWindow(dialog)
}
}
func applicationOpenUrls (app Application, args ...string) {
application, ok := app.(ApplicationURLOpener)
if !ok {
if len(args) > 0 {
log.Fatal("nasin: this application cannot open URLs")
}
return
}
openNone := func () bool {
window, err := application.OpenNone()
if err != nil {
log.Fatalf("nasin: could not open main window: %v", err)
return false
}
manageWindow(window)
return true
}
if len(args) <= 0 { if len(args) <= 0 {
application.OpenNone() openNone()
return
} }
openedAny := false openedAny := false
@@ -177,27 +242,19 @@ func applicationOpenUrls (application Application, args ...string) {
if ur.Scheme == "" { if ur.Scheme == "" {
ur.Scheme = "file" ur.Scheme = "file"
} }
err = application.OpenURL(ur) window, err := application.OpenURL(ur)
if err != nil { if err != nil {
dialog, err := objects.NewDialogOk ( errorPopupf(
objects.DialogError, nil,
"Could Not Open URL", "Could Not Open URL",
fmt.Sprintf (
"Could not open %v: %v", "Could not open %v: %v",
arg, err), arg, err,
func () { )(func () {
if !openedAny { if !openedAny {
application.OpenNone() openNone()
} }
}) })
if err != nil { log.Fatal(err) }
dialog.SetVisible(true)
}
}
} else {
if len(args) > 0 {
log.Fatal("nasin: this application cannot open URLs")
} }
manageWindow(window)
} }
} }

6
go.mod
View File

@@ -4,9 +4,9 @@ go 1.22.2
require ( require (
git.tebibyte.media/sashakoshka/goparse v0.2.0 git.tebibyte.media/sashakoshka/goparse v0.2.0
git.tebibyte.media/tomo/backend v0.5.1 git.tebibyte.media/tomo/backend v0.7.0
git.tebibyte.media/tomo/objects v0.20.1 git.tebibyte.media/tomo/objects v0.22.0
git.tebibyte.media/tomo/tomo v0.41.1 git.tebibyte.media/tomo/tomo v0.46.1
git.tebibyte.media/tomo/xdg v0.1.0 git.tebibyte.media/tomo/xdg v0.1.0
golang.org/x/image v0.11.0 golang.org/x/image v0.11.0
) )

12
go.sum
View File

@@ -1,12 +1,12 @@
git.tebibyte.media/sashakoshka/goparse v0.2.0 h1:uQmKvOCV2AOlCHEDjg9uclZCXQZzq2PxaXfZ1aIMiQI= git.tebibyte.media/sashakoshka/goparse v0.2.0 h1:uQmKvOCV2AOlCHEDjg9uclZCXQZzq2PxaXfZ1aIMiQI=
git.tebibyte.media/sashakoshka/goparse v0.2.0/go.mod h1:tSQwfuD+EujRoKr6Y1oaRy74ZynatzkRLxjE3sbpCmk= git.tebibyte.media/sashakoshka/goparse v0.2.0/go.mod h1:tSQwfuD+EujRoKr6Y1oaRy74ZynatzkRLxjE3sbpCmk=
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q= git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/backend v0.5.1 h1:u3DLVcNWNdQsIxAEGcZ+kGG7zuew8tpPbyEFBx8ehjM= git.tebibyte.media/tomo/backend v0.7.0 h1:12A+IsbwIKCmg4jKjD9xCDz+o7R3X6Yp8cZup+wOGIM=
git.tebibyte.media/tomo/backend v0.5.1/go.mod h1:urnfu+D56Q9AOCZ/qp5YqkmlRRTIB5p9RbzBN7yIibQ= git.tebibyte.media/tomo/backend v0.7.0/go.mod h1:G3Kh6N2MuiAwsnuPe3h9CwWL65vmmsaqgapA38MPyhk=
git.tebibyte.media/tomo/objects v0.20.1 h1:dCMMzaHmj7w016XtTb8QWvKMHVK2Fru/2a70NqApzHQ= git.tebibyte.media/tomo/objects v0.22.0 h1:2t21W32HW2xvPBICqmArVMVWxg9ohhTJw6ChZ0DcdYY=
git.tebibyte.media/tomo/objects v0.20.1/go.mod h1:hbBlQem9cNOt/G2rBvH/C94FT1oWgbtEftwg6ield9U= git.tebibyte.media/tomo/objects v0.22.0/go.mod h1:f5J5tAhO+eN5glVbCJLPSopIeTylXqLgKLVAIg8iAPQ=
git.tebibyte.media/tomo/tomo v0.41.1 h1:XdbyF3VjsLj1Zppr70gUaufuh49hU32JQo2ENnw4PcA= git.tebibyte.media/tomo/tomo v0.46.1 h1:/8fT6I9l4TK529zokrThbNDHGRvUsNgif1Zs++0PBSQ=
git.tebibyte.media/tomo/tomo v0.41.1/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps= git.tebibyte.media/tomo/tomo v0.46.1/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs=
git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8= git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/xdg v0.1.0 h1:6G2WYPPiM2IXleCpKKHuJA34BxumwNWuLsUoX3yu5zA= git.tebibyte.media/tomo/xdg v0.1.0 h1:6G2WYPPiM2IXleCpKKHuJA34BxumwNWuLsUoX3yu5zA=

View File

@@ -0,0 +1,35 @@
package fallbackFaces
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/font/basicfont"
import "git.tebibyte.media/tomo/backend/style"
type faceSet struct {
regular font.Face
bold font.Face
italic font.Face
boldItalic font.Face
}
// New creates a new fallback face set.
func New () style.FaceSet {
// TODO maybe pre-generate different variations of this face
return &faceSet {
regular: basicfont.Face7x13,
bold: basicfont.Face7x13,
italic: basicfont.Face7x13,
boldItalic: basicfont.Face7x13,
}
}
func (this *faceSet) Face (face tomo.Face) font.Face {
bold := face.Weight >= 500
italic := face.Italic >= 0.1 || face.Slant >= 0.1
switch {
case bold && italic: return this.boldItalic
case bold: return this.bold
case italic: return this.italic
default: return this.regular
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -7,6 +7,7 @@ import _ "image/png"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data" import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
//go:embed assets/icons-small.png //go:embed assets/icons-small.png
var atlasSmallBytes []byte var atlasSmallBytes []byte
@@ -110,9 +111,14 @@ func generateSource (data []byte, width int) map[tomo.Icon] canvas.Texture {
col(tomo.IconInsertLink) col(tomo.IconInsertLink)
col(tomo.IconInsertObject) col(tomo.IconInsertObject)
col(tomo.IconInsertText) col(tomo.IconInsertText)
row()
// actions: list // actions: list
col(tomo.IconListAdd) col(tomo.IconListAdd)
col(tomo.IconListRemove) col(tomo.IconListRemove)
col(tomo.IconListChoose)
col(tomo.IconListExpand)
col(tomo.IconListContract)
row() row()
// actions: mail // actions: mail
@@ -310,9 +316,6 @@ func generateSource (data []byte, width int) map[tomo.Icon] canvas.Texture {
col(tomo.IconPlacePreferences) col(tomo.IconPlacePreferences)
row() row()
// status: checkbox
col(tomo.IconCheckboxChecked)
col(tomo.IconCheckboxUnchecked)
// status: appointments // status: appointments
col(tomo.IconAppointmentMissed) col(tomo.IconAppointmentMissed)
col(tomo.IconAppointmentSoon) col(tomo.IconAppointmentSoon)
@@ -411,23 +414,23 @@ func generateSource (data []byte, width int) map[tomo.Icon] canvas.Texture {
return source return source
} }
type iconTheme struct { type iconSet struct {
texturesSmall map[tomo.Icon] canvas.Texture texturesSmall map[tomo.Icon] canvas.Texture
texturesLarge map[tomo.Icon] canvas.Texture texturesLarge map[tomo.Icon] canvas.Texture
} }
// New creates a new fallback icon theme. // New creates a new fallback icon set.
func New () tomo.IconSet { func New () style.IconSet {
return new(iconTheme) return new(iconSet)
} }
func (this *iconTheme) ensure () { func (this *iconSet) ensure () {
if this.texturesSmall != nil { return } if this.texturesSmall != nil { return }
this.texturesSmall = generateSource(atlasSmallBytes, 16) this.texturesSmall = generateSource(atlasSmallBytes, 16)
this.texturesLarge = generateSource(atlasLargeBytes, 32) this.texturesLarge = generateSource(atlasLargeBytes, 32)
} }
func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture { func (this *iconSet) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture {
if size == tomo.IconSizeSmall { if size == tomo.IconSizeSmall {
return this.texturesSmall return this.texturesSmall
} else { } else {
@@ -435,7 +438,7 @@ func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.T
} }
} }
func (this *iconTheme) Icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture { func (this *iconSet) Icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture {
this.ensure() this.ensure()
source := this.selectSource(size) source := this.selectSource(size)
if texture, ok := source[icon]; ok { if texture, ok := source[icon]; ok {
@@ -444,7 +447,7 @@ func (this *iconTheme) Icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture
return nil return nil
} }
func (this *iconTheme) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture { func (this *iconSet) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture {
this.ensure() this.ensure()
source := this.selectSource(size) source := this.selectSource(size)
if mime == data.M("inode", "directory") { if mime == data.M("inode", "directory") {

View File

@@ -8,19 +8,20 @@ import "regexp"
import "strings" import "strings"
import _ "image/png" import _ "image/png"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import xdgIconTheme "git.tebibyte.media/tomo/xdg/icon-theme"
import "git.tebibyte.media/tomo/tomo/data" import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style"
import xdgIconTheme "git.tebibyte.media/tomo/xdg/icon-theme"
type iconTheme struct { type iconTheme struct {
xdg xdgIconTheme.Theme xdg xdgIconTheme.Theme
fallback tomo.IconSet fallback style.IconSet
texturesSmall map[tomo.Icon] canvas.Texture texturesSmall map[tomo.Icon] canvas.Texture
texturesMedium map[tomo.Icon] canvas.Texture texturesMedium map[tomo.Icon] canvas.Texture
texturesLarge map[tomo.Icon] canvas.Texture texturesLarge map[tomo.Icon] canvas.Texture
} }
func FindThemeWarn (name string, fallback tomo.IconSet, path ...string) (tomo.IconSet, error) { func FindThemeWarn (name string, fallback style.IconSet, path ...string) (style.IconSet, error) {
this := &iconTheme { this := &iconTheme {
fallback: fallback, fallback: fallback,
texturesLarge: make(map[tomo.Icon] canvas.Texture), texturesLarge: make(map[tomo.Icon] canvas.Texture),

View File

@@ -5,35 +5,45 @@ import "os"
import "log" import "log"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/backend/x" 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/icons/xdg"
import "git.tebibyte.media/tomo/nasin/internal/style/tss" import "git.tebibyte.media/tomo/nasin/internal/styles/tss"
import "git.tebibyte.media/tomo/nasin/internal/icons/fallback" import "git.tebibyte.media/tomo/nasin/internal/icons/fallback"
import "git.tebibyte.media/tomo/nasin/internal/style/fallback" import "git.tebibyte.media/tomo/nasin/internal/styles/fallback"
import "git.tebibyte.media/tomo/nasin/internal/faces/fallback"
func RegisterBackend () error { type Registrar struct {
tomo.Register(1, x.New) backend *x.Backend
return nil
} }
func SetTheme () error { func (this *Registrar) SetBackend () (tomo.Backend, error) {
// TODO eventually get rid of this when we make a file format for backend, err := x.New()
// storing visual styles if err != nil { return nil, err }
this.backend = backend.(*x.Backend)
tomo.SetBackend(backend)
return backend, nil
}
func (this *Registrar) SetTheme () error {
styleSheetName := os.Getenv("TOMO_STYLE_SHEET") styleSheetName := os.Getenv("TOMO_STYLE_SHEET")
if styleSheetName != "" { if styleSheetName != "" {
styl, _, err := tss.LoadFile(styleSheetName) styl, _, err := tss.LoadFile(styleSheetName)
if err == nil { if err == nil {
tomo.SetStyle(styl) this.backend.SetStyle(styl)
return nil
} else { } 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))
} }
} }
styl, _ := fallbackStyle.New() styl, _ := fallbackStyle.New()
tomo.SetStyle(styl) this.backend.SetStyle(styl)
return nil return nil
} }
func SetIconSet () error { func (this *Registrar) SetIconSet () error {
iconSet := fallbackIcons.New() iconSet := fallbackIcons.New()
iconSetName := os.Getenv("TOMO_XDG_ICON_THEME") iconSetName := os.Getenv("TOMO_XDG_ICON_THEME")
if iconSetName != "" { if iconSetName != "" {
@@ -45,6 +55,14 @@ func SetIconSet () error {
} }
} }
tomo.SetIconSet(iconSet) this.backend.SetIconSet(iconSet)
return nil
}
func (this *Registrar) SetFaceSet () error {
// TODO replace this with something that uses findfont, and caches and
// refcounts the faces
faceSet := fallbackFaces.New()
this.backend.SetFaceSet(faceSet)
return nil return nil
} }

View File

@@ -1,18 +0,0 @@
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

@@ -1,391 +0,0 @@
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 },
),
},
}

View File

@@ -1,21 +0,0 @@
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
}
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)
}

View File

@@ -0,0 +1,319 @@
// Colors
$ColorDot = #7391c080;
$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 ;
$BorderEngraved = #c3c3c5 #e3e3e3 #e9e9e9 #c2c2c2;
$BorderGap = #697c7c #566767 #566767 #697c7c;
$BorderLifted = #f9fafc #c2c8d3 #a4afc0 #f5f6f8;
$BorderLiftedFocused = #f0f4f9 #b1baca #9aa6b7 #e4e9ee;
$BorderFocused = #5f8bc4 #5f8bc4 #5f8bc4 #5f8bc4;
$BorderTear = $BorderEngraved ;
$BorderTearFocused = #7f94b5 #ced7e4 #ced7e4 #7f94b5;
$BorderTearPad = #0000 ;
$BorderTearPadFocused = #7391c080 ;
$BorderInnerShadow = #a4afc0 ;
$BorderOuterShadow = #a4afc0 ;
*.* {
TextColor: $ColorForeground;
DotColor: $ColorDot;
Gap: 8;
}
*.Button {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderLifted / 1;
Padding: 4 8;
Color: $ColorRaised;
}
*.Button[focused] {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderLiftedFocused / 1;
Padding: 4 8;
Color: $ColorRaisedFocused;
}
*.Button[hovered] {
Color: $ColorRaisedHovered;
}
*.Button[pressed] {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderInnerShadow / 1 0 0 1;
Padding: 5 8 4 9;
Color: $ColorRaisedPressed;
}
*.TextInput {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderInnerShadow / 1 0 0 1;
Padding: 5 4 4 5;
Color: $ColorSunken;
}
*.TextInput[focused] {
Border: $BorderEngraved / 1, $BorderFocused / 1, $BorderInnerShadow / 1 0 0 1;
}
*.TextView {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderInnerShadow / 1 0 0 1;
Padding: 5 4 4 5;
Color: $ColorSunken;
}
*.NumberInput {
Gap: 0;
}
*.Container[sunken] {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderInnerShadow / 1 0 0 1;
Padding: 5 4 4 5;
Color: $ColorSunken;
}
*.Container[outer] {
Color: $ColorBackground;
Padding: 8;
}
*.Container[menu] {
Border: $BorderGap / 1, $BorderLifted / 1;
Color: $ColorBackground;
Gap: 0;
}
*.Container[menu, torn] {
Border: ;
Color: $ColorBackground;
Gap: 0;
}
*.Heading {
Align: middle middle;
}
*.Heading[menu] {
Align: middle middle;
Padding: 4 8;
}
*.Separator {
Border: $BorderEngraved / 1;
}
*.Slider {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderInnerShadow / 1 0 0 1;
Color: $ColorGutter;
}
*.Slider[focused] {
Border: $BorderEngraved / 1, $BorderFocused / 1, $BorderInnerShadow / 1 0 0 1;
}
*.Slider[hovered] {
Color: $ColorGutterHovered;
}
*.Slider[horizontal] {
MinimumSize: 48 0;
}
*.Slider[vertical] {
MinimumSize: 0 48;
}
*.SliderHandle {
Border: $BorderOuterShadow / 0 1 1 0, $BorderGap / 1, $BorderLifted / 1;
Color: $ColorRaised;
MinimumSize: 12;
}
*.Scrollbar {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderInnerShadow / 1 0 0 1;
Color: $ColorGutter;
}
*.Scrollbar[focused] {
Border: $BorderEngraved / 1, $BorderFocused / 1, $BorderInnerShadow / 1 0 0 1;
}
*.Scrollbar[hovered] {
Color: $ColorGutterHovered;
}
*.Scrollbar[horizontal] {
MinimumSize: 48 0;
}
*.Scrollbar[vertical] {
MinimumSize: 0 48;
}
*.ScrollbarHandle {
Border: $BorderOuterShadow / 0 1 1 0, $BorderGap / 1, $BorderLifted / 1;
Color: $ColorRaised;
MinimumSize: 12;
}
*.ScrollContainer {
Gap: 0;
}
*.Checkbox {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderInnerShadow / 1 0 0 1;
Color: $ColorSunken;
Padding: 0 1 1 0;
MinimumSize: 19;
}
*.Checkbox[focused] {
Border: $BorderEngraved / 1, $BorderFocused / 1, $BorderInnerShadow / 1 0 0 1;
Color: $ColorSunkenFocused;
Padding: 0;
}
*.Checkbox[checked] {
Texture: "assets/checkbox-checked.png";
TextureMode: center;
}
*.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 / 3, $BorderTear / 1;
}
*.TearLine[hovered] {
Border: $BorderTearPadFocused / 3, $BorderTearFocused / 1;
}
*.TearLine[focused] {
Border: $BorderTearPadFocused / 3, $BorderTearFocused / 1;
}
*.Calendar {
Border: $BorderOuterShadow / 0 1 1 0, $BorderGap / 1;
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;
}
*.TabbedContainer {
Gap: 0;
}
*.TabRow {
Border: $BorderEngraved / 1 1 0 1, $BorderGap / 1 1 0 1, $BorderInnerShadow / 1 0 0 1;
Color: $ColorSunken;
Gap: 0;
Padding: 1 0 0 0;
}
*.Tab {
Border: #0000 / 1 0 0 0, $BorderOuterShadow / 0 1 0 0, $BorderGap / 1 1 0 1, $BorderLifted / 1;
Color: $ColorSunken;
Padding: 4 8 2 8;
}
*.Tab[active] {
Border: $BorderOuterShadow / 0 1 0 0, $BorderGap / 1 1 0 1, $BorderLifted / 1 1 0 1;
Color: $ColorBackground;
Padding: 4 8;
}
*.TabSpacer {
Border: $BorderLifted / 0 0 1 0;
MinimumSize: 1 0;
}
*.Swatch {
Border: $BorderEngraved / 1, $BorderGap / 1;
Color: #FFF;
MinimumSize: 19;
}
*.Swatch[focused] {
Border: $BorderEngraved / 1, $BorderFocused / 1;
}
*.ColorPickerMap {
Border: $BorderEngraved / 1, $BorderGap / 1;
Color: $ColorSunken;
MinimumSize: 128;
}
*.Dropdown {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderLifted / 1;
Padding: 4 8;
Color: $ColorRaised;
}
*.Dropdown[focused] {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderLiftedFocused / 1;
Padding: 4 8;
Color: $ColorRaisedFocused;
}
*.Dropdown[hovered] {
Color: $ColorRaisedHovered;
}
*.Dropdown[pressed] {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderInnerShadow / 1 0 0 1;
Padding: 5 8 4 9;
Color: $ColorRaisedPressed;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

View File

Before

Width:  |  Height:  |  Size: 409 B

After

Width:  |  Height:  |  Size: 409 B

View File

@@ -7,8 +7,8 @@ import _ "embed"
import _ "image/png" import _ "image/png"
import "image/color" import "image/color"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/font/basicfont"
import "git.tebibyte.media/tomo/tomo/event" 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 colorFocus = color.RGBA { R: 61, G: 128, B: 143, A: 255 }
var colorInput = color.RGBA { R: 208, G: 203, B: 150, A: 255 } var colorInput = color.RGBA { R: 208, G: 203, B: 150, A: 255 }
@@ -55,30 +55,29 @@ func newCloserCookie (closer io.Closer) event.Cookie {
// New returns Wintergreen, the default Tomo style. It is neutral-gray with // New returns Wintergreen, the default Tomo style. It is neutral-gray with
// green and turquoise accents. // green and turquoise accents.
func New () (*tomo.Style, event.Cookie) { func New () (*style.Style, event.Cookie) {
atlasImage, _, err := image.Decode(bytes.NewReader(atlasBytes)) atlasImage, _, err := image.Decode(bytes.NewReader(atlasBytes))
if err != nil { panic(err) } if err != nil { panic(err) }
atlasTexture := tomo.NewTexture(atlasImage) atlasTexture := tomo.NewTexture(atlasImage)
textureCheckboxChecked := atlasTexture.SubTexture(image.Rect( 0, 0, 12, 11)) textureCheckboxChecked := atlasTexture.SubTexture(image.Rect( 0, 0, 12, 11))
textureCorkboard := atlasTexture.SubTexture(image.Rect(16, 0, 28, 12)) // textureCorkboard := atlasTexture.SubTexture(image.Rect(16, 0, 28, 12))
textureTearLine := atlasTexture.SubTexture(image.Rect(16, 12, 18, 13)) textureTearLine := atlasTexture.SubTexture(image.Rect(16, 12, 18, 13))
textureHandleVertical := atlasTexture.SubTexture(image.Rect(28, 0, 29, 2)) textureHandleVertical := atlasTexture.SubTexture(image.Rect(28, 0, 29, 2))
textureHandleHorizontal := atlasTexture.SubTexture(image.Rect(28, 0, 30, 1)) textureHandleHorizontal := atlasTexture.SubTexture(image.Rect(28, 0, 30, 1))
cookie := event.MultiCookie(newCloserCookie(atlasTexture)) cookie := event.MultiCookie(newCloserCookie(atlasTexture))
rules := []tomo.Rule { rules := []style.Rule {
// *.* // *.*
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AFace (basicfont.Face7x13 ),
tomo.ATextColor (tomo.ColorForeground), tomo.ATextColor (tomo.ColorForeground),
tomo.ADotColor (tomo.ColorAccent ), tomo.ADotColor (tomo.ColorAccent ),
tomo.AGap (8, 8 ), tomo.AGap (8, 8 ),
), tomo.R("", "")), ), tomo.R("", "")),
// *.Button // *.Button
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
outline, outline,
tomo.Border { tomo.Border {
@@ -91,7 +90,7 @@ rules := []tomo.Rule {
), tomo.R("", "Button")), ), tomo.R("", "Button")),
// *.Button[focused] // *.Button[focused]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
outline, outline,
tomo.Border { tomo.Border {
@@ -102,7 +101,7 @@ rules := []tomo.Rule {
), tomo.R("", "Button"), "focused"), ), tomo.R("", "Button"), "focused"),
// *.Button[pressed] // *.Button[pressed]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
outline, outline,
tomo.Border { tomo.Border {
@@ -115,7 +114,7 @@ rules := []tomo.Rule {
), tomo.R("", "Button"), "pressed"), ), tomo.R("", "Button"), "pressed"),
// *.TextInput // *.TextInput
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.ABorder (
outline, outline,
tomo.Border { tomo.Border {
@@ -127,7 +126,7 @@ rules := []tomo.Rule {
), tomo.R("", "TextInput")), ), tomo.R("", "TextInput")),
// *.TextInput[focused] // *.TextInput[focused]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.ABorder (
outline, outline,
tomo.Border { tomo.Border {
@@ -137,7 +136,7 @@ rules := []tomo.Rule {
), tomo.R("", "TextInput"), "focused"), ), tomo.R("", "TextInput"), "focused"),
// *.TextView // *.TextView
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.ABorder (
outline, outline,
tomo.Border { tomo.Border {
@@ -149,31 +148,31 @@ rules := []tomo.Rule {
), tomo.R("", "TextView")), ), tomo.R("", "TextView")),
// *.NumberInput // *.NumberInput
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AGap(0, 0), tomo.AGap(0, 0),
), tomo.R("", "NumberInput")), ), tomo.R("", "NumberInput")),
// *.Container[sunken] // *.Container[sunken]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.ABorder (
outline, outline,
tomo.Border { tomo.Border {
Width: tomo.I(1, 0, 0, 1), Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved, Color: borderColorEngraved,
}), }),
tomo.AColor(nil), tomo.AColor(tomo.ColorSunken),
tomo.ATexture(textureCorkboard), // tomo.ATexture(textureCorkboard),
tomo.APadding(8), tomo.APadding(8),
), tomo.R("", "Container"), "sunken"), ), tomo.R("", "Container"), "sunken"),
// *.Container[outer] // *.Container[outer]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorBackground }, tomo.AttrColor { Color: tomo.ColorBackground },
tomo.AttrPadding(tomo.I(8)), tomo.AttrPadding(tomo.I(8)),
), tomo.R("", "Container"), "outer"), ), tomo.R("", "Container"), "outer"),
// *.Container[menu] // *.Container[menu]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
outline, outline,
tomo.Border { tomo.Border {
@@ -186,12 +185,17 @@ rules := []tomo.Rule {
), tomo.R("", "Container"), "menu"), ), tomo.R("", "Container"), "menu"),
// *.Heading // *.Heading
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AAlign(tomo.AlignMiddle, tomo.AlignMiddle), tomo.AAlign(tomo.AlignMiddle, tomo.AlignMiddle),
), tomo.R("", "Heading")), ), tomo.R("", "Heading")),
// *.Heading
style.Ru(style.AS (
tomo.APadding(4, 8),
), tomo.R("", "Heading"), "menu"),
// *.Separator // *.Separator
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
tomo.Border { tomo.Border {
Width: tomo.I(1), Width: tomo.I(1),
@@ -201,7 +205,7 @@ rules := []tomo.Rule {
), tomo.R("", "Separator")), ), tomo.R("", "Separator")),
// *.Slider // *.Slider
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
outline, outline,
tomo.Border { tomo.Border {
@@ -214,7 +218,7 @@ rules := []tomo.Rule {
), tomo.R("", "Slider")), ), tomo.R("", "Slider")),
// *.Slider[focused] // *.Slider[focused]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.ABorder (
outline, outline,
tomo.Border { tomo.Border {
@@ -225,17 +229,17 @@ rules := []tomo.Rule {
), tomo.R("", "Slider"), "focused"), ), tomo.R("", "Slider"), "focused"),
// *.Slider[horizontal] // *.Slider[horizontal]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AMinimumSize(48, 0), tomo.AMinimumSize(48, 0),
), tomo.R("", "Slider"), "horizontal"), ), tomo.R("", "Slider"), "horizontal"),
// *.Slider[vertical] // *.Slider[vertical]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AMinimumSize(0, 48), tomo.AMinimumSize(0, 48),
), tomo.R("", "Slider"), "vertical"), ), tomo.R("", "Slider"), "vertical"),
// *.SliderHandle // *.SliderHandle
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.ABorder (
outline, outline,
tomo.Border { tomo.Border {
@@ -255,17 +259,76 @@ rules := []tomo.Rule {
), tomo.R("", "SliderHandle")), ), tomo.R("", "SliderHandle")),
// *.SliderHandle[horizontal] // *.SliderHandle[horizontal]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ATexture(textureHandleHorizontal), tomo.ATexture(textureHandleHorizontal),
), tomo.R("", "SliderHandle"), "horizontal"), ), tomo.R("", "SliderHandle"), "horizontal"),
// *.Scrollbar
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("", "Scrollbar")),
// *.Scrollbar[focused]
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
}),
tomo.APadding(0),
), tomo.R("", "Scrollbar"), "focused"),
// *.Scrollbar[horizontal]
style.Ru(style.AS (
tomo.AMinimumSize(48, 0),
), tomo.R("", "Scrollbar"), "horizontal"),
// *.Scrollbar[vertical]
style.Ru(style.AS (
tomo.AMinimumSize(0, 48),
), tomo.R("", "Scrollbar"), "vertical"),
// *.ScrollbarHandle
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("", "ScrollbarHandle")),
// *.ScrollbarHandle[horizontal]
style.Ru(style.AS (
tomo.ATexture(textureHandleHorizontal),
), tomo.R("", "ScrollbarHandle"), "horizontal"),
// *.ScrollContainer // *.ScrollContainer
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AGap(0, 0), tomo.AGap(0, 0),
), tomo.R("", "ScrollContainer")), ), tomo.R("", "ScrollContainer")),
// *.Checkbox // *.Checkbox
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.ABorder (
outline, outline,
tomo.Border { tomo.Border {
@@ -280,7 +343,7 @@ rules := []tomo.Rule {
), tomo.R("", "Checkbox")), ), tomo.R("", "Checkbox")),
// *.Checkbox[focused] // *.Checkbox[focused]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.ABorder (
outline, outline,
tomo.Border { tomo.Border {
@@ -291,39 +354,39 @@ rules := []tomo.Rule {
), tomo.R("", "Checkbox"), "focused"), ), tomo.R("", "Checkbox"), "focused"),
// *.Checkbox[checked] // *.Checkbox[checked]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ATexture(textureCheckboxChecked), tomo.ATexture(textureCheckboxChecked),
), tomo.R("", "Checkbox"), "checked"), ), tomo.R("", "Checkbox"), "checked"),
// *.MenuItem // *.MenuItem
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrPadding(tomo.I(4)), tomo.AttrPadding(tomo.I(4)),
tomo.AttrGap { X: 4, Y: 4 }, tomo.AttrGap { X: 4, Y: 4 },
tomo.AttrColor { Color: color.Transparent }, tomo.AttrColor { Color: color.Transparent },
), tomo.R("", "MenuItem")), ), tomo.R("", "MenuItem")),
// *MenuItem[focused] // *MenuItem[focused]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorAccent }, tomo.AttrColor { Color: tomo.ColorAccent },
), tomo.R("", "MenuItem"), "focused"), ), tomo.R("", "MenuItem"), "focused"),
// *.MenuItem[hovered] // *.MenuItem[hovered]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorAccent }, tomo.AttrColor { Color: tomo.ColorAccent },
), tomo.R("", "MenuItem"), "hovered"), ), tomo.R("", "MenuItem"), "hovered"),
// *.File // *.File
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrColor { Color: color.Transparent }, tomo.AttrColor { Color: color.Transparent },
), tomo.R("", "File")), ), tomo.R("", "File")),
// *.File[focused] // *.File[focused]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorAccent }, tomo.AttrColor { Color: tomo.ColorAccent },
), tomo.R("", "File"), "focused"), ), tomo.R("", "File"), "focused"),
// *.TearLine // *.TearLine
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.ABorder (
tomo.Border { tomo.Border {
Width: tomo.I(3), Width: tomo.I(3),
@@ -339,7 +402,7 @@ rules := []tomo.Rule {
), tomo.R("", "TearLine")), ), tomo.R("", "TearLine")),
// *.TearLine[focused] // *.TearLine[focused]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
tomo.Border { tomo.Border {
Width: tomo.I(3), Width: tomo.I(3),
@@ -349,7 +412,7 @@ rules := []tomo.Rule {
), tomo.R("", "TearLine"), "focused"), ), tomo.R("", "TearLine"), "focused"),
// *.TearLine[hovered] // *.TearLine[hovered]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
tomo.Border { tomo.Border {
Width: tomo.I(3), Width: tomo.I(3),
@@ -359,7 +422,7 @@ rules := []tomo.Rule {
), tomo.R("", "TearLine"), "hovered"), ), tomo.R("", "TearLine"), "hovered"),
// *.Calendar // *.Calendar
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
tomo.Border { tomo.Border {
Width: tomo.I(0, 1, 1, 0), Width: tomo.I(0, 1, 1, 0),
@@ -373,37 +436,37 @@ rules := []tomo.Rule {
), tomo.R("", "Calendar")), ), tomo.R("", "Calendar")),
// *.CalendarGrid // *.CalendarGrid
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrGap { X: 2, Y: 2 }, tomo.AttrGap { X: 2, Y: 2 },
), tomo.R("", "CalendarGrid")), ), tomo.R("", "CalendarGrid")),
// *.CalendarWeekdayHeader // *.CalendarWeekdayHeader
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrPadding(tomo.I(2)), tomo.AttrPadding(tomo.I(2)),
tomo.AttrColor { Color: colorCalendarWeekdayHeader }, tomo.AttrColor { Color: colorCalendarWeekdayHeader },
), tomo.R("", "CalendarWeekdayHeader")), ), tomo.R("", "CalendarWeekdayHeader")),
// *.CalendarDay[weekday] // *.CalendarDay[weekday]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrPadding(tomo.I(2)), tomo.AttrPadding(tomo.I(2)),
tomo.AttrMinimumSize { X: 32, Y: 32 }, tomo.AttrMinimumSize { X: 32, Y: 32 },
tomo.AttrColor { Color: colorCalendarDay }, tomo.AttrColor { Color: colorCalendarDay },
), tomo.R("", "CalendarDay"), "weekday"), ), tomo.R("", "CalendarDay"), "weekday"),
// *.CalendarDay[weekend] // *.CalendarDay[weekend]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrPadding(tomo.I(2)), tomo.AttrPadding(tomo.I(2)),
tomo.AttrMinimumSize { X: 32, Y: 32 }, tomo.AttrMinimumSize { X: 32, Y: 32 },
tomo.AttrColor { Color: colorCalendarWeekend }, tomo.AttrColor { Color: colorCalendarWeekend },
), tomo.R("", "CalendarDay"), "weekend"), ), tomo.R("", "CalendarDay"), "weekend"),
// *.TabbedContainer // *.TabbedContainer
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AGap(0, 0), tomo.AGap(0, 0),
), tomo.R("", "TabbedContainer")), ), tomo.R("", "TabbedContainer")),
// *.TabRow // *.TabRow
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
tomo.Border { tomo.Border {
Width: tomo.I(1, 1, 0, 1), Width: tomo.I(1, 1, 0, 1),
@@ -420,7 +483,7 @@ rules := []tomo.Rule {
), tomo.R("", "TabRow")), ), tomo.R("", "TabRow")),
// *.TabSpacer[left] // *.TabSpacer[left]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
tomo.Border { tomo.Border {
Width: tomo.I(0, 0, 1, 0), Width: tomo.I(0, 0, 1, 0),
@@ -435,7 +498,7 @@ rules := []tomo.Rule {
), tomo.R("", "TabSpacer")), ), tomo.R("", "TabSpacer")),
// *.TabSpacer[right] // *.TabSpacer[right]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
tomo.Border { tomo.Border {
Width: tomo.I(1, 0, 0, 0), Width: tomo.I(1, 0, 0, 0),
@@ -461,7 +524,7 @@ rules := []tomo.Rule {
), tomo.R("", "TabSpacer"), "right"), ), tomo.R("", "TabSpacer"), "right"),
// *.Tab // *.Tab
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
tomo.Border { tomo.Border {
Width: tomo.I(1, 0, 0, 0), Width: tomo.I(1, 0, 0, 0),
@@ -488,7 +551,7 @@ rules := []tomo.Rule {
), tomo.R("", "Tab")), ), tomo.R("", "Tab")),
// *.Tab[active] // *.Tab[active]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
tomo.Border { tomo.Border {
Width: tomo.I(1, 0, 0, 1), Width: tomo.I(1, 0, 0, 1),
@@ -499,12 +562,12 @@ rules := []tomo.Rule {
Color: borderColorLifted, Color: borderColorLifted,
}, },
}, },
tomo.AttrPadding(tomo.I(4, 8, 4, 8)), tomo.AttrPadding(tomo.I(4, 8, 7, 8)),
tomo.AttrColor { Color: tomo.ColorBackground }, tomo.AttrColor { Color: tomo.ColorBackground },
), tomo.R("", "Tab"), "active"), ), tomo.R("", "Tab"), "active"),
// *.Swatch // *.Swatch
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
outline, outline,
}, },
@@ -512,7 +575,7 @@ rules := []tomo.Rule {
), tomo.R("", "Swatch")), ), tomo.R("", "Swatch")),
// *.Swatch[focused] // *.Swatch[focused]
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
outline, outline,
tomo.Border { tomo.Border {
@@ -523,7 +586,7 @@ rules := []tomo.Rule {
), tomo.R("", "Swatch"), "focused"), ), tomo.R("", "Swatch"), "focused"),
// *.ColorPickerMap // *.ColorPickerMap
tomo.Ru(tomo.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
outline, outline,
tomo.Border { tomo.Border {
@@ -534,9 +597,46 @@ rules := []tomo.Rule {
tomo.AttrColor { Color: tomo.ColorSunken }, tomo.AttrColor { Color: tomo.ColorSunken },
tomo.AttrMinimumSize { X: 128, Y: 128 }, tomo.AttrMinimumSize { X: 128, Y: 128 },
), tomo.R("", "ColorPickerMap")), ), tomo.R("", "ColorPickerMap")),
// *.Dropdown
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("", "Dropdown")),
// *.Dropdown[focused]
style.Ru(style.AS (
tomo.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
), tomo.R("", "Dropdown"), "focused"),
// *.Dropdown[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("", "Dropdown"), "pressed"),
} }
return &tomo.Style { return &style.Style {
Rules: rules, Rules: rules,
Colors: map[tomo.Color] color.Color { Colors: map[tomo.Color] color.Color {
tomo.ColorBackground: colorBackground, tomo.ColorBackground: colorBackground,

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,407 @@
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()
}

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
}

View File

@@ -19,6 +19,7 @@ const (
Star Star
Dot Dot
Dollar Dollar
Slash
Color Color
Ident Ident
Number Number
@@ -39,6 +40,7 @@ var tokenNames = map[parse.TokenKind] string {
Star: "Star", Star: "Star",
Dot: "Dot", Dot: "Dot",
Dollar: "Dollar", Dollar: "Dollar",
Slash: "Slash",
Color: "Color", Color: "Color",
Ident: "Ident", Ident: "Ident",
Number: "Number", Number: "Number",
@@ -115,6 +117,8 @@ func (this *lexer) next () (token parse.Token, err error) {
skipRune() skipRune()
if err != nil { return } if err != nil { return }
} }
} else {
token.Kind = Slash
} }
if this.eof { err = nil; return } if this.eof { err = nil; return }

View File

@@ -4,43 +4,6 @@ import "io"
import "strconv" import "strconv"
import "git.tebibyte.media/sashakoshka/goparse" 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 { type parser struct {
parse.Parser parse.Parser
sheet Sheet sheet Sheet
@@ -105,9 +68,9 @@ func (this *parser) parseVariable () (string, ValueList, error) {
if err != nil { return "", nil, err } if err != nil { return "", nil, err }
err = this.ExpectNext(Ident) err = this.ExpectNext(Ident)
if err != nil { return "", nil, err } if err != nil { return "", nil, err }
name := this.Value()
err = this.ExpectNext(Equals) err = this.ExpectNext(Equals)
if err != nil { return "", nil, err } if err != nil { return "", nil, err }
name := this.Value()
this.Next() this.Next()
values, err := this.parseValueList() values, err := this.parseValueList()
if err != nil { return "", nil, err } if err != nil { return "", nil, err }
@@ -124,15 +87,18 @@ func (this *parser) parseRule () (Rule, error) {
selector, err := this.parseSelector() selector, err := this.parseSelector()
if err != nil { return Rule { }, err } if err != nil { return Rule { }, err }
rule.Selector = selector rule.Selector = selector
err = this.Expect(LBracket) err = this.Expect(LBrace)
if err != nil { return Rule { }, err } if err != nil { return Rule { }, err }
for { for {
this.Next()
if this.Is(RBrace) { break }
pos := this.Pos() pos := this.Pos()
name, attr, err := this.parseAttr() name, attr, err := this.parseAttr()
if err != nil { break } if err != nil { return Rule { }, err }
_, exists := rule.Attrs[name] err = this.Expect(Semicolon)
if !exists { if err != nil { return Rule { }, err }
if _, exists := rule.Attrs[name]; exists {
return Rule { }, parse.Errorf ( return Rule { }, parse.Errorf (
pos, pos,
"attribute %s already declared in this rule", "attribute %s already declared in this rule",
@@ -141,8 +107,6 @@ func (this *parser) parseRule () (Rule, error) {
rule.Attrs[name] = attr rule.Attrs[name] = attr
} }
err = this.Expect(LBracket)
if err != nil { return Rule { }, err }
return rule, this.Next() return rule, this.Next()
} }
@@ -167,20 +131,23 @@ func (this *parser) parseSelector () (Selector, error) {
} }
// tags // tags
err = this.ExpectNext(LBrace) err = this.ExpectNext(LBracket)
if err == nil { if err == nil {
this.Next()
for { for {
err := this.Expect(Ident, String, RBrace) this.Next()
err := this.Expect(Ident, String, RBracket)
if err != nil { return Selector { }, err } if err != nil { return Selector { }, err }
if this.Is(RBrace) { break } if this.Is(RBracket) { break }
if this.Is(Comma) { this.Next() }
selector.Tags = append(selector.Tags, this.Value()) selector.Tags = append(selector.Tags, this.Value())
err = this.ExpectNext(Comma, RBrace) err = this.ExpectNext(Comma, RBracket)
if err != nil { return Selector { }, err } if err != nil { return Selector { }, err }
if this.Is(RBracket) { break }
} }
this.Next()
} }
return selector, this.Next() return selector, nil
} }
func (this *parser) parseAttr () (string, []ValueList, error) { func (this *parser) parseAttr () (string, []ValueList, error) {
@@ -195,18 +162,20 @@ func (this *parser) parseAttr () (string, []ValueList, error) {
this.Next() this.Next()
for { for {
err := this.ExpectDesc ( err := this.ExpectDesc (
"value, comma, or semicolon", "value, Comma, or Semicolon",
Number, Color, String, Ident, Dollar, Comma, Semicolon) Number, Color, String, Ident, Dollar, Slash,
Comma, Semicolon)
if err != nil { return "", nil, err } if err != nil { return "", nil, err }
if this.Is(Semicolon) { break } if this.Is(Semicolon) { break }
if this.Is(Comma) { this.Next() }
valueList, err := this.parseValueList() valueList, err := this.parseValueList()
if err != nil { return "", nil, err } if err != nil { return "", nil, err }
attr = append(attr, valueList) attr = append(attr, valueList)
err = this.ExpectNext(Comma, Semicolon) err = this.Expect(Comma, Semicolon)
if err != nil { return "", nil, err } if err != nil { return "", nil, err }
} }
return name, attr, this.Next() return name, attr, nil
} }
func (this *parser) parseValueList () (ValueList, error) { func (this *parser) parseValueList () (ValueList, error) {
@@ -214,7 +183,7 @@ func (this *parser) parseValueList () (ValueList, error) {
for { for {
err := this.ExpectDesc ( err := this.ExpectDesc (
"value", "value",
Number, Color, String, Ident, Dollar) Number, Color, String, Ident, Dollar, Slash)
if err != nil { break } if err != nil { break }
switch this.Kind() { switch this.Kind() {
case Number: case Number:
@@ -237,9 +206,12 @@ func (this *parser) parseValueList () (ValueList, error) {
err := this.ExpectNext(Ident) err := this.ExpectNext(Ident)
if err != nil { return nil, err } if err != nil { return nil, err }
list = append(list, ValueVariable(this.Value())) 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) { func parseColor (runes []rune) (uint32, bool) {

View File

@@ -0,0 +1,81 @@
package tss
import "os"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/backend/style"
type Sheet struct {
Path string
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 }
sheet.Path = name
return BuildStyle(sheet)
}

View File

@@ -0,0 +1,35 @@
# 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]*)"
- constant: "\\b(tile|center)\\b"
- constant: "\\b(start|middle|end|even)\\b"
- constant.bool: "\\b(true|false)\\b"
- 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]*"