23 Commits

Author SHA1 Message Date
a8be9280f1 Update aluminum theme 2024-09-14 22:48:31 -04:00
ddff2f9e47 Improve Notebook styling
...Once we get PageWrapper, that is
2024-09-12 15:50:09 -04:00
48990469cf Update fallback style 2024-09-12 15:32:40 -04:00
59f32e6c1f Update backend 2024-09-12 15:31:24 -04:00
11477554b4 Update objects 2024-09-12 15:27:37 -04:00
0f9608a50a NewApplicationWindow takes in a WindowKind 2024-09-12 02:46:35 -04:00
2312b48a28 Fix code relating to cookies 2024-09-12 02:42:55 -04:00
e08d14135b Update Tomo API 2024-09-12 02:36:10 -04:00
888c233181 Give Wintergreen's TabbedContainer a gap 2024-09-03 16:25:15 -04:00
545d553d1a Config impl watches for create *and* write
Editing config files with things like gedit work now
2024-08-28 00:56:40 -04:00
2aea7764d3 Update styles in response to new focus method for MenuItem 2024-08-24 14:43:48 -04:00
058b6ba8df Update backend 2024-08-23 17:27:08 -04:00
87af913c4b Add readme for fallback icon set assets 2024-08-23 13:27:04 -04:00
9727363b7f Convert fallback icon set to indexed color 2024-08-23 13:22:21 -04:00
8c647d118d Improve doc comments for ApplicationSystem*Dirs 2024-08-23 02:27:49 -04:00
27678b36b9 Improve README.md 2024-08-23 02:23:04 -04:00
1e92134a38 Move config spec into a README.md file for the config package. 2024-08-23 01:50:30 -04:00
0ebf3ff4cc Make sure File is an io.WriterTo 2024-08-23 01:16:17 -04:00
81bd635b09 Improve doc comment for GlobalConfig 2024-08-23 01:14:08 -04:00
4b29820452 Add accessor for global config 2024-08-23 01:12:02 -04:00
f512deb96e Icons and styles use xyz.holanet.Nasin config 2024-08-23 01:10:54 -04:00
a69c726482 Fixed diffing thinking orphaned keys were real 2024-08-22 20:27:22 -04:00
ed77634a50 Add flag to stop re-parsing the user file when we already know its
contents
2024-08-22 20:10:12 -04:00
19 changed files with 495 additions and 247 deletions

View File

@@ -2,12 +2,79 @@
[![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/nasin.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/nasin) [![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/nasin.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/nasin)
Nasin provides an easy way to write applications with Tomo. To get started, take Nasin builds an application framework on top of Tomo to ease and encourage the
a look at the [examples](examples) directory and the development of consistent and stable application software. It has these
wonderful features, and more:
- Use the Application interface to create applications with relatively low
boilerplate
- CLI argument parsing and URI opening
- Automatic setup/teardown of the backend
- Advanced configuration system that can watch config files for changes
- Default style and icon set, as well as a fully featured stylesheet language
for creating custom styles, and support for XDG icon themes
## Getting Started
Here is a basic "hello world" application, with explanations as comments:
```go
package main
import "image"
import "git.tebibyte.media/tomo/nasin"
import "git.tebibyte.media/tomo/objects"
import "git.tebibyte.media/tomo/objects/layouts"
func main () {
nasin.RunApplication(new(Application))
}
type Application struct { }
// Describe returns the application's name and ID, and optionally what type of
// application it is.
func (this *Application) Describe () nasin.ApplicationDescription {
return nasin.ApplicationDescription {
// This is the name of the application. New application windows
// will have this as their title by default.
Name: "Example",
// This is a "well-known" name, which typically is a domain name
// owned by the application author.
ID: "com.example.Example",
}
}
// Init performs initial setup of the application. Since this is a single-window
// application that doesn't open any files, we create the window here.
func (this *Application) Init () error {
// Passing an empty rectangle when creating a new window will cause it
// to auto-expand to fit the minimum size of its contents.
window, err := nasin.NewApplicationWindow(this, image.Rectangle { })
if err != nil { return err }
// Here we create a new container with a basic vertical layout, place a
// text label that says "Hello world!" in it, and set it as the root
// object of the window.
window.SetRoot(objects.NewOuterContainer (
layouts.ContractVertical,
objects.NewLabel("Hello world!")))
window.SetVisible(true)
// Nasin will not exit until all windows it is "waiting for" have
// been closed.
nasin.WaitFor(window)
return nil
}
// Stop cleanly closes things like system resources or background tasks. We do
// not have any here, so nothing is done.
func (this *Application) Stop () { }
```
To learn more, take a look at the [examples](examples) directory and the
[online documentation](https://pkg.go.dev/git.tebibyte.media/tomo/nasin). [online documentation](https://pkg.go.dev/git.tebibyte.media/tomo/nasin).
Related repositories: ## Related Repositories
- [Tomo API](https://git.tebibyte.media/tomo/tomo): The API that all other parts - [Tomo API](https://git.tebibyte.media/tomo/tomo): The API that all other parts
of the toolkit agree on of the toolkit agree on
- [Objects](https://git.tebibyte.media/tomo/objects): A standard collection of - [Objects](https://git.tebibyte.media/tomo/objects): A standard collection of
re-usable objects and other GUI components re-usable objects and other GUI components
- [Backend](https://git.tebibyte.media/tomo/backend): The software responsible
for managing and rendering things behind the scenes

View File

@@ -144,15 +144,47 @@ func RunApplication (application Application) {
} }
flag.Parse() flag.Parse()
// open config
globalConfig, err := ApplicationConfig(GlobalApplicationDescription())
if err != nil { log.Fatalln("nasin: could not open config:", err) }
currentGlobalConfig = globalConfig
defer func () {
globalConfig.Close()
currentGlobalConfig = nil
} ()
styleConfigKey := "Style"
iconSetConfigKey := "IconSet"
// registry
// TODO: rebuild registry around the config
reg := new(registrar.Registrar) reg := new(registrar.Registrar)
backend, err := reg.SetBackend() 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 = reg.SetTheme()
if err != nil { log.Fatalln("nasin: could not set theme:", err) }
err = reg.SetIconSet()
if err != nil { log.Fatalln("nasin: could not set icon set:", err) }
err = reg.SetFaceSet() err = reg.SetFaceSet()
if err != nil { log.Fatalln("nasin: could not set face set:", err) } if err != nil { log.Fatalln("nasin: could not set face set:", err) }
updateStyle := func () {
value, err := globalConfig.GetString(styleConfigKey, "")
if err != nil { log.Fatalln("nasin: could not set theme:", err) }
err = reg.SetStyle(value)
if err != nil { log.Fatalln("nasin: could not set theme:", err) }
}
updateIconSet := func () {
value, err := globalConfig.GetString(iconSetConfigKey, "")
if err != nil { log.Fatalln("nasin: could not set icon set:", err) }
err = reg.SetIconSet(value)
if err != nil { log.Fatalln("nasin: could not set icon set:", err) }
}
updateStyle()
updateIconSet()
globalConfig.OnChange(func (key string) {
switch key {
case styleConfigKey: updateStyle()
case iconSetConfigKey: updateIconSet()
}
})
// init application
err = application.Init() 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) }
@@ -172,8 +204,8 @@ func RunApplication (application Application) {
// owned by the application. The window's icon will be automatically set by // owned by the application. The window's icon will be automatically set by
// looking for an icon with the name of the application's ID. If that is not // looking for an icon with the name of the application's ID. If that is not
// found, the default icon for the application's ApplicationRole will used. // found, the default icon for the application's ApplicationRole will used.
func NewApplicationWindow (application Application, bounds image.Rectangle) (tomo.Window, error) { func NewApplicationWindow (application Application, kind tomo.WindowKind, bounds image.Rectangle) (tomo.Window, error) {
window, err := tomo.NewWindow(bounds) window, err := tomo.NewWindow(kind, bounds)
if err != nil { return nil, err } if err != nil { return nil, err }
description := application.Describe() description := application.Describe()
window.SetTitle(description.Name) window.SetTitle(description.Name)
@@ -196,6 +228,14 @@ func ApplicationConfig (app ApplicationDescription) (config.ConfigCloser, error)
return config.NewConfig(user, system...) return config.NewConfig(user, system...)
} }
var currentGlobalConfig config.Config
// GlobalConfig returns the global config. It contains options that apply to
// Tomo/Nasin itself, such as the style sheet and the icon set. This is managed
// by Nasin and must not be closed by the application.
func GlobalConfig () config.Config {
return currentGlobalConfig
}
func errorPopupf (title, format string, v ...any) func (func ()) { func errorPopupf (title, format string, v ...any) func (func ()) {
return func (callback func ()) { return func (callback func ()) {
dialog, err := objects.NewDialogOk ( dialog, err := objects.NewDialogOk (

78
config/README.md Normal file
View File

@@ -0,0 +1,78 @@
# config
Package config provides a configuration system for applications.
## Config File Location
Config files are stored in standard operating system locations. Each application
has exactly one user-level config directory within the user-level config
location, and a system-level subdirectory in each of the system-level config
locations (if applicable). Each subdirectory bears the application's well-known
name as specified in ApplicationDescription. Each subdirectory contains a file
called config.conf, which is where the actual config data is stored.
The user-level configuration file takes precendence over system configuration
files, and system configuration files take precedence over eachother depending
on what order they are specified in. How they are specified depends on the
operating system.
### Linux, Most Unixes
In terms of the XDG Base Directory Specification, an application with the
well-known name com.example.Example would have its config files stored at
`$XDG_CONFIG_DIRS/com.example.Example/config.conf`. On most systems where this
specification is applicable, this will result in a file at
`/etc/xdg/com.example.Example/config.conf` and another at
`$HOME/.config/com.example.Example/config.conf`. The location for config files
on systems that do not make use of this specification is yet to be determined.
## Config File Format
The general format of the file is as follows:
- Encoded in UTF-8
- Consists of lines, separated by \n, or \r\n
- Lines can be any of these:
- Blank line: has only whitespace
- Comment: begins with a '#'
- Entry: a key/value pair separated by an '=' sign
### Entries
For entries, all whitespace on either side of the '=' sign, the key, or the
value is ignored. The key may contain any letter or digit, as well as '-' and
'.'. The value is always identified by its first rune (after the preliminary
whitespace of course) and can be one of:
- String
- Number
- Bool
#### String
A string can be either double-quoted, or any string of runes not identifiable
as any other kind of value. Quoted strings are always unquoted when they are
read. Either way, these escape sequences are supported, and resolved when they
are read:
- '\\\\': a literal backslash
- '\a': alert, bell
- '\b': backspace
- '\t': horizontal tab
- '\n': line feed
- '\v': vertical tab
- '\f': form feed
- '\r': carriage return
- '\\"': double quote
Be aware that some unquoted strings, within reason, are subject to being read
as some other value in the future. For example, if there were suddenly a
third boolean value called glorble, the unquoted string glorble would be read
as a boolean value instead of a string.
#### Number
A number is a floating point value. It can be of the form:
- Inf: positive infinity
- -Inf: negative infinity
- NaN: "not a number"
- [0-9]+: a whole number
- [0-9]+\.[0-9]*: a fractional number
#### Bool
A bool is a boolean value. It can be one of:
- true
- false

View File

@@ -13,6 +13,8 @@ type line any
type comment string type comment string
type entry struct { key string; value Value } type entry struct { key string; value Value }
var _ io.WriterTo = new(File)
// File represents a config file. It preserves the order of the lines, as well // File represents a config file. It preserves the order of the lines, as well
// as blank lines and comments. // as blank lines and comments.
type File struct { type File struct {
@@ -29,47 +31,8 @@ func NewFile () *File {
// Parse parses a config file from a reader. This function operates on a // Parse parses a config file from a reader. This function operates on a
// best-effort basis: A file will always be returned, and any errors encountered // best-effort basis: A file will always be returned, and any errors encountered
// will be joined together. // will be joined together. For a description of the format, see the README.md
// // of this package.
// The general format of the file is as follows:
// - Encoded in UTF-8
// - Consists of lines, separated by \n, or \r\n
// - Lines can be any of these:
// - Blank line: has only whitespace
// - Comment: begins with a '#'
// - Entry: a key/value pair separated by an '=' sign
//
// For entries, all whitespace on either side of the '=' sign, the key, or the
// value is ignored. The key may contain any letter or digit, as well as '-'
// and '.'. The value is always identified by its first rune (after the
// preliminary whitespace of course) and can be one of:
// - String: either a double-quoted string, or any string of runes not
// identifiable as any other kind of value. The quoted string is always
// unquoted when it is read. Either way, these escape sequences are
// supported, and resolved when they are read:
// - '\\': a literal backslash
// - '\a': alert, bell
// - '\b': backspace
// - '\t': horizontal tab
// - '\n': line feed
// - '\v': vertical tab
// - '\f': form feed
// - '\r': carriage return
// - '\"': double quote
// - Number: a floating point value. It can be of the form:
// - Inf
// - -Inf
// - NaN
// - [0-9]+
// - [0-9]+\.[0-9]*
// - Bool: a boolean value. It can be one of:
// - true
// - false
//
// Be aware that some unquoted strings, within reason, are subject to being read
// as some other value in the future. For example, if there were suddenly a
// third boolean value called glorble, the unquoted string glorble would be read
// as a boolean value instead of a string.
func Parse (reader io.Reader) (*File, error) { func Parse (reader io.Reader) (*File, error) {
file := &File { file := &File {
keys: make(map[string] int), keys: make(map[string] int),
@@ -147,8 +110,12 @@ func ParseValue (str string) (Value, error) {
// ErrMalformedKey. // ErrMalformedKey.
func (this *File) Has (key string) (bool, error) { func (this *File) Has (key string) (bool, error) {
if !KeyValid(key) { return false, ErrMalformedKey } if !KeyValid(key) { return false, ErrMalformedKey }
_, ok := this.keys[key] if index, ok := this.keys[key]; ok {
return ok, nil if _, ok := this.lines[index].(entry); ok {
return true, nil
}
}
return false, nil
} }
// Get gets the keyed value. If the value is unspecified, it returns nil, // Get gets the keyed value. If the value is unspecified, it returns nil,
@@ -156,7 +123,7 @@ func (this *File) Has (key string) (bool, error) {
func (this *File) Get (key string) (Value, error) { func (this *File) Get (key string) (Value, error) {
if !KeyValid(key) { return nil, ErrMalformedKey } if !KeyValid(key) { return nil, ErrMalformedKey }
if index, ok := this.keys[key]; ok { if index, ok := this.keys[key]; ok {
if lin := this.lines[index].(entry); ok { if lin, ok := this.lines[index].(entry); ok {
return lin.value, nil return lin.value, nil
} }
} }
@@ -166,14 +133,16 @@ func (this *File) Get (key string) (Value, error) {
// Set sets a value. If the key is invalid, it returns ErrMalformedKey. // Set sets a value. If the key is invalid, it returns ErrMalformedKey.
func (this *File) Set (key string, value Value) error { func (this *File) Set (key string, value Value) error {
if !KeyValid(key) { return ErrMalformedKey } if !KeyValid(key) { return ErrMalformedKey }
ent := entry {
key: key,
value: value,
}
if index, ok := this.keys[key]; ok { if index, ok := this.keys[key]; ok {
ent := this.lines[index].(entry)
ent.value = value
this.lines[index] = ent this.lines[index] = ent
return nil return nil
} }
this.keys[key] = len(this.lines) this.keys[key] = len(this.lines)
this.lines = append(this.lines, value) this.lines = append(this.lines, ent)
return nil return nil
} }
@@ -212,19 +181,31 @@ func (this *File) Diff (other *File) map[string] struct { } {
// - keys only we have // - keys only we have
// - keys we both have, but are different // - keys we both have, but are different
for key, index := range this.keys { for key, index := range this.keys {
thisEntry, ok := this.lines[index].(entry)
if !ok { continue }
otherIndex, ok := other.keys[key] otherIndex, ok := other.keys[key]
if !ok { if !ok {
diff[key] = struct { } { } diff[key] = struct { } { }
continue continue
} }
if !this.lines[index].(entry).value.Equals(other.lines[otherIndex].(entry).value) { otherEntry, ok := other.lines[otherIndex].(entry)
if !ok {
diff[key] = struct { } { }
continue
}
if !thisEntry.value.Equals(otherEntry.value) {
diff[key] = struct { } { } diff[key] = struct { } { }
} }
} }
// - keys only they have // - keys only they have
for key := range other.keys { for key := range other.keys {
if _, has := this.keys[key]; !has { if otherHas, _ := other.Has(key); !otherHas {
continue
}
if thisHas, _ := this.Has(key); !thisHas {
diff[key] = struct { } { } diff[key] = struct { } { }
} }
} }
@@ -233,8 +214,8 @@ func (this *File) Diff (other *File) map[string] struct { } {
} }
// WriteTo writes the data in this file to an io.Writer. // WriteTo writes the data in this file to an io.Writer.
func (file *File) WriteTo (writer io.Writer) (n int64, err error) { func (this *File) WriteTo (writer io.Writer) (n int64, err error) {
for _, lin := range file.lines { for _, lin := range this.lines {
nint := 0 nint := 0
switch lin := lin.(type) { switch lin := lin.(type) {
case comment: case comment:

View File

@@ -123,6 +123,36 @@ yetAnotherThing = 0.23498
} }
} }
func TestDiffReset (test *testing.T) {
file1 := parseFileString(test,
`key4=0
key1=value1
keyToDelete=true
# comment
key2=34`)
file2 := parseFileString(test,
`key1=value2
key2=34
anotherKeyToDelete=false
# comment
key3=0.2`)
file1.Reset("keyToDelete")
file2.Reset("anotherKeyToDelete")
diff := file1.Diff(file2)
correct := map[string] struct { } {
"key1": struct { } { },
"key3": struct { } { },
"key4": struct { } { },
}
if !maps.Equal(diff, correct) {
test.Error("diffs do not match")
test.Errorf("EXPECTED:\n%v", correct)
test.Errorf("GOT:\n%v", diff)
test.Fail()
}
}
func TestDiff (test *testing.T) { func TestDiff (test *testing.T) {
file1 := parseFileString(test, file1 := parseFileString(test,
`key4=0 `key4=0

View File

@@ -16,9 +16,10 @@ import "git.tebibyte.media/tomo/tomo/event"
// directly followed by a deferred call to Unlock. // directly followed by a deferred call to Unlock.
type config struct { type config struct {
open bool open bool
watcher *fsnotify.Watcher watcher *fsnotify.Watcher
lock sync.RWMutex lock sync.RWMutex
ignoreNextUserUpdate bool
paths struct { paths struct {
user string user string
@@ -64,14 +65,17 @@ func NewConfig (user string, system ...string) (ConfigCloser, error) {
func (this *config) lockAndProcessEvent (event fsnotify.Event) { func (this *config) lockAndProcessEvent (event fsnotify.Event) {
this.lock.Lock() this.lock.Lock()
defer this.lock.Unlock() defer this.lock.Unlock()
if !(event.Has(fsnotify.Write)) { return } if !(event.Has(fsnotify.Write | fsnotify.Create)) { return }
if _, ok := this.paths.watching[event.Name]; !ok { return } if _, ok := this.paths.watching[event.Name]; !ok { return }
if event.Name == this.paths.user { if event.Name == this.paths.user {
previousUser := this.data.user if !this.ignoreNextUserUpdate {
this.reloadUser() previousUser := this.data.user
newUser := this.data.user this.reloadUser()
this.processUserDiff(newUser.Diff(previousUser)) newUser := this.data.user
this.processUserDiff(newUser.Diff(previousUser))
}
this.ignoreNextUserUpdate = false
} else { } else {
index := slices.Index(this.paths.system, event.Name) index := slices.Index(this.paths.system, event.Name)
if index > 0 { if index > 0 {
@@ -148,6 +152,7 @@ func (this *config) saveUser () error {
defer file.Close() defer file.Close()
_, err = this.data.user.WriteTo(file) _, err = this.data.user.WriteTo(file)
if err != nil { return err } if err != nil { return err }
this.ignoreNextUserUpdate = true
return nil return nil
} }
@@ -262,7 +267,10 @@ func (this *config) Set (key string, value Value) error {
if this.data.user == nil { this.data.user = NewFile() } if this.data.user == nil { this.data.user = NewFile() }
err := this.data.user.Set(key, value) err := this.data.user.Set(key, value)
if err != nil { return err } if err != nil { return err }
return this.saveUser() err = this.saveUser()
if err != nil { return err }
this.broadcastChange(key)
return nil
} }
func (this *config) Reset (key string) error { func (this *config) Reset (key string) error {
@@ -271,7 +279,10 @@ func (this *config) Reset (key string) error {
if this.data.user == nil { this.data.user = NewFile() } if this.data.user == nil { this.data.user = NewFile() }
err := this.data.user.Reset(key) err := this.data.user.Reset(key)
if err != nil { return err } if err != nil { return err }
return this.saveUser() err = this.saveUser()
if err != nil { return err }
this.broadcastChange(key)
return nil
} }
func (this *config) OnChange (callback func (string)) event.Cookie { func (this *config) OnChange (callback func (string)) event.Cookie {

9
go.mod
View File

@@ -4,16 +4,17 @@ 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.7.0 git.tebibyte.media/tomo/backend v0.8.0
git.tebibyte.media/tomo/objects v0.22.0 git.tebibyte.media/tomo/objects v0.24.0
git.tebibyte.media/tomo/tomo v0.46.1 git.tebibyte.media/tomo/tomo v0.48.0
git.tebibyte.media/tomo/xdg v0.1.0 git.tebibyte.media/tomo/xdg v0.1.0
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.7.0
golang.org/x/image v0.11.0 golang.org/x/image v0.11.0
) )
require ( require (
git.tebibyte.media/tomo/typeset v0.7.1 // indirect git.tebibyte.media/sashakoshka/goutil v0.3.1 // indirect
git.tebibyte.media/tomo/typeset v0.8.0 // indirect
git.tebibyte.media/tomo/xgbkb v1.0.1 // indirect git.tebibyte.media/tomo/xgbkb v1.0.1 // indirect
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect

18
go.sum
View File

@@ -1,14 +1,16 @@
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/goutil v0.3.1 h1:zvAMKS+aea96q6oTttCWfNLXqOHisI3IKAwX6BWKfY0=
git.tebibyte.media/sashakoshka/goutil v0.3.1/go.mod h1:Yo/M2sbi9IbzZCFsEj8/Fg7sNwHkDaJ6saTHOha+Dow=
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.7.0 h1:12A+IsbwIKCmg4jKjD9xCDz+o7R3X6Yp8cZup+wOGIM= git.tebibyte.media/tomo/backend v0.8.0 h1:mPP6g60lL7v9GOjyUl/oGkHK/CV4ZJB8OUlw0E7WEhk=
git.tebibyte.media/tomo/backend v0.7.0/go.mod h1:G3Kh6N2MuiAwsnuPe3h9CwWL65vmmsaqgapA38MPyhk= git.tebibyte.media/tomo/backend v0.8.0/go.mod h1:yIWW8XXDsaHfIhAuxg336oYlgX0uCn3lwgaydh8BagE=
git.tebibyte.media/tomo/objects v0.22.0 h1:2t21W32HW2xvPBICqmArVMVWxg9ohhTJw6ChZ0DcdYY= git.tebibyte.media/tomo/objects v0.24.0 h1:O91CxHJ1eA5/WJzDNm46lTA3Lm6t1CrLtf8gLKtuX7U=
git.tebibyte.media/tomo/objects v0.22.0/go.mod h1:f5J5tAhO+eN5glVbCJLPSopIeTylXqLgKLVAIg8iAPQ= git.tebibyte.media/tomo/objects v0.24.0/go.mod h1:atTxG2mRqDQBFS/0KoqP9VR50BozFLi7XRz/AkFJtMo=
git.tebibyte.media/tomo/tomo v0.46.1 h1:/8fT6I9l4TK529zokrThbNDHGRvUsNgif1Zs++0PBSQ= git.tebibyte.media/tomo/tomo v0.48.0 h1:AE21ElHwUSPsX82ZWCnoNxJFi9Oswyd3dPDPMbxTueQ=
git.tebibyte.media/tomo/tomo v0.46.1/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs= git.tebibyte.media/tomo/tomo v0.48.0/go.mod h1:WrtilgKB1y8O2Yu7X4mYcRiqOlPR8NuUnoA/ynkQWrs=
git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8= git.tebibyte.media/tomo/typeset v0.8.0 h1:4qA6oW4/3oPHj6/Zrp+JFJ53OmFSDvxs+J6BhO3DW00=
git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= git.tebibyte.media/tomo/typeset v0.8.0/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=
git.tebibyte.media/tomo/xdg v0.1.0/go.mod h1:tuaRwRkyYW7mqlxA7P2+V+e10KzcamNoUzcOgaIYKAY= git.tebibyte.media/tomo/xdg v0.1.0/go.mod h1:tuaRwRkyYW7mqlxA7P2+V+e10KzcamNoUzcOgaIYKAY=
git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE= git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE=

View File

@@ -0,0 +1,7 @@
# assets
These are the assets for the fallback (Wintergreen) icon set. When making edits
to these icons, open both the png and xcf files. When you wish to save your
changes, delete the contents of the png file and paste in the "Icons" layer of
the xcf file, then export the png file and save the xcf file. This is because
the exported pngs use indexed color to save space as they are embedded into
application binaries.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -6,9 +6,10 @@ import _ "embed"
import _ "image/png" 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/nasin/internal/util" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style" import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/nasin/internal/util"
//go:embed assets/icons-small.png //go:embed assets/icons-small.png
var atlasSmallBytes []byte var atlasSmallBytes []byte
@@ -35,7 +36,7 @@ const (
iconXOfficeSpreadsheet = tomo.Icon("x-office-spreadsheet") iconXOfficeSpreadsheet = tomo.Icon("x-office-spreadsheet")
) )
func generateSource (data []byte, width int) map[tomo.Icon] canvas.Texture { func generateSource (data []byte, width int) (canvas.TextureCloser, map[tomo.Icon] canvas.Texture) {
atlasImage, _, err := image.Decode(bytes.NewReader(data)) atlasImage, _, err := image.Decode(bytes.NewReader(data))
if err != nil { panic(err) } if err != nil { panic(err) }
atlasTexture := tomo.NewTexture(atlasImage) atlasTexture := tomo.NewTexture(atlasImage)
@@ -448,23 +449,26 @@ func generateSource (data []byte, width int) map[tomo.Icon] canvas.Texture {
col(tomo.IconWeatherSnow) col(tomo.IconWeatherSnow)
col(tomo.IconWeatherStorm) col(tomo.IconWeatherStorm)
return source return atlasTexture, source
} }
type iconSet struct { type iconSet struct {
atlasSmall canvas.TextureCloser
atlasLarge canvas.TextureCloser
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 set. // New creates a new fallback icon set.
func New () style.IconSet { func New () (style.IconSet, event.Cookie) {
return new(iconSet) iconSet := new(iconSet)
return iconSet, iconSet
} }
func (this *iconSet) ensure () { func (this *iconSet) ensure () {
if this.texturesSmall != nil { return } if this.texturesSmall != nil { return }
this.texturesSmall = generateSource(atlasSmallBytes, 16) this.atlasSmall, this.texturesSmall = generateSource(atlasSmallBytes, 16)
this.texturesLarge = generateSource(atlasLargeBytes, 32) this.atlasLarge, this.texturesLarge = generateSource(atlasLargeBytes, 32)
} }
func (this *iconSet) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture { func (this *iconSet) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture {
@@ -500,3 +504,13 @@ func (this *iconSet) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Textur
return source[tomo.Icon(iconApplicationXGeneric)] return source[tomo.Icon(iconApplicationXGeneric)]
} }
} }
func (this *iconSet) Close () error {
if this.atlasSmall != nil {
this.atlasSmall.Close()
}
if this.atlasLarge != nil {
this.atlasLarge.Close()
}
return nil
}

View File

@@ -9,6 +9,7 @@ import "strings"
import _ "image/png" 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/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style" import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/nasin/internal/util" import "git.tebibyte.media/tomo/nasin/internal/util"
@@ -17,27 +18,27 @@ import xdgIconTheme "git.tebibyte.media/tomo/xdg/icon-theme"
type iconTheme struct { type iconTheme struct {
xdg xdgIconTheme.Theme xdg xdgIconTheme.Theme
fallback style.IconSet fallback style.IconSet
texturesSmall map[tomo.Icon] canvas.Texture texturesSmall map[tomo.Icon] canvas.TextureCloser
texturesMedium map[tomo.Icon] canvas.Texture texturesMedium map[tomo.Icon] canvas.TextureCloser
texturesLarge map[tomo.Icon] canvas.Texture texturesLarge map[tomo.Icon] canvas.TextureCloser
} }
func FindThemeWarn (name string, fallback style.IconSet, path ...string) (style.IconSet, error) { func FindThemeWarn (name string, fallback style.IconSet, path ...string) (style.IconSet, event.Cookie, error) {
this := &iconTheme { this := &iconTheme {
fallback: fallback, fallback: fallback,
texturesLarge: make(map[tomo.Icon] canvas.Texture), texturesLarge: make(map[tomo.Icon] canvas.TextureCloser),
texturesMedium: make(map[tomo.Icon] canvas.Texture), texturesMedium: make(map[tomo.Icon] canvas.TextureCloser),
texturesSmall: make(map[tomo.Icon] canvas.Texture), texturesSmall: make(map[tomo.Icon] canvas.TextureCloser),
} }
xdg, err := xdgIconTheme.FindThemeWarn(name, path...) xdg, err := xdgIconTheme.FindThemeWarn(name, path...)
if err != nil { return nil, err } if err != nil { return nil, nil, err }
this.xdg = xdg this.xdg = xdg
return this, nil return this, this, nil
} }
func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture { func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.TextureCloser {
switch size { switch size {
case tomo.IconSizeMedium: return this.texturesMedium case tomo.IconSizeMedium: return this.texturesMedium
case tomo.IconSizeLarge: return this.texturesLarge case tomo.IconSizeLarge: return this.texturesLarge
@@ -45,7 +46,7 @@ func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.T
} }
} }
func (this *iconTheme) xdgIcon (name string, size tomo.IconSize) (canvas.Texture, bool) { func (this *iconTheme) xdgIcon (name string, size tomo.IconSize) (canvas.TextureCloser, bool) {
// TODO use scaling factor instead of 1 // TODO use scaling factor instead of 1
// find icon file // find icon file
icon, err := this.xdg.FindIcon(name, iconSizePixels(size), 1, xdgIconTheme.PNG) icon, err := this.xdg.FindIcon(name, iconSizePixels(size), 1, xdgIconTheme.PNG)
@@ -100,7 +101,21 @@ func (this *iconTheme) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Text
} }
} }
func (this *iconTheme) icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture { func (this *iconTheme) Close () error {
closeAllIn := func (mp map[tomo.Icon] canvas.TextureCloser) {
for _, texture := range mp {
if texture != nil {
texture.Close()
}
}
}
closeAllIn(this.texturesSmall)
closeAllIn(this.texturesMedium)
closeAllIn(this.texturesLarge)
return nil
}
func (this *iconTheme) icon (icon tomo.Icon, size tomo.IconSize) canvas.TextureCloser {
if texture, ok := this.xdgIcon(XdgIconName(icon), size); ok { if texture, ok := this.xdgIcon(XdgIconName(icon), size); ok {
return texture return texture
} }
@@ -110,7 +125,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 *iconTheme) mimeIcon (mime data.Mime, size tomo.IconSize) canvas.TextureCloser {
if texture, ok := this.xdgIcon(xdgFormatMime(mime), size); ok { if texture, ok := this.xdgIcon(xdgFormatMime(mime), size); ok {
return texture return texture
} }

View File

@@ -1,10 +1,10 @@
//go:build unix && (!darwin) //go:build unix && (!darwin)
package registrar package registrar
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/tomo/tomo/event"
import "git.tebibyte.media/sashakoshka/goparse" 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/styles/tss" import "git.tebibyte.media/tomo/nasin/internal/styles/tss"
@@ -14,6 +14,8 @@ import "git.tebibyte.media/tomo/nasin/internal/faces/fallback"
type Registrar struct { type Registrar struct {
backend *x.Backend backend *x.Backend
iconSetCookie event.Cookie
styleCookie event.Cookie
} }
func (this *Registrar) SetBackend () (tomo.Backend, error) { func (this *Registrar) SetBackend () (tomo.Backend, error) {
@@ -24,38 +26,50 @@ func (this *Registrar) SetBackend () (tomo.Backend, error) {
return backend, nil return backend, nil
} }
func (this *Registrar) SetTheme () error { func (this *Registrar) SetStyle (name string) error {
styleSheetName := os.Getenv("TOMO_STYLE_SHEET") if this.styleCookie != nil {
if styleSheetName != "" { this.styleCookie.Close()
styl, _, err := tss.LoadFile(styleSheetName) this.styleCookie = nil
}
if name != "" {
styl, cookie, err := tss.LoadFile(name)
if err == nil { if err == nil {
this.backend.SetStyle(styl) this.backend.SetStyle(styl)
this.styleCookie = cookie
return nil return nil
} else { } else {
log.Printf ( log.Printf (
"nasin: could not load style sheet '%s'\n%v", "nasin: could not load style sheet '%s'\n%v",
styleSheetName, parse.Format(err)) name, parse.Format(err))
} }
} }
styl, _ := fallbackStyle.New() styl, cookie := fallbackStyle.New()
this.styleCookie = cookie
this.backend.SetStyle(styl) this.backend.SetStyle(styl)
return nil return nil
} }
func (this *Registrar) SetIconSet () error { func (this *Registrar) SetIconSet (name string) error {
iconSet := fallbackIcons.New() if this.iconSetCookie != nil {
iconSetName := os.Getenv("TOMO_XDG_ICON_THEME") this.iconSetCookie.Close()
if iconSetName != "" { this.iconSetCookie = nil
xdgIconSet, err := xdgIcons.FindThemeWarn(iconSetName, iconSet) }
iconSet, cookie := fallbackIcons.New()
if name != "" {
xdgIconSet, xdgCookie, err := xdgIcons.FindThemeWarn(name, iconSet)
cookie = event.MultiCookie(cookie, xdgCookie)
if err == nil { if err == nil {
iconSet = xdgIconSet iconSet = xdgIconSet
} else { } else {
log.Printf("nasin: could not load icon theme '%s': %v", iconSetName, err) log.Printf("nasin: could not load icon theme '%s': %v", name, err)
} }
} }
this.backend.SetIconSet(iconSet) this.backend.SetIconSet(iconSet)
this.iconSetCookie = cookie
return nil return nil
} }

View File

@@ -17,6 +17,7 @@ $ColorSunkenPressed = #e0e6ee;
$ColorCalendarWeekdayHeader = #d3cac2; $ColorCalendarWeekdayHeader = #d3cac2;
$ColorCalendarWeekend = #c2d3c4; $ColorCalendarWeekend = #c2d3c4;
$ColorCalendarDay = #d6dae2; $ColorCalendarDay = #d6dae2;
$ColorOptionSegment = #c6c8cc;
// Borders // Borders
$BorderOutline = $ColorOutline ; $BorderOutline = $ColorOutline ;
@@ -31,6 +32,7 @@ $BorderTearPad = #0000 ;
$BorderTearPadFocused = #7391c080 ; $BorderTearPadFocused = #7391c080 ;
$BorderInnerShadow = #a4afc0 ; $BorderInnerShadow = #a4afc0 ;
$BorderOuterShadow = #a4afc0 ; $BorderOuterShadow = #a4afc0 ;
$BorderOptionSegment = #b0b6c0 ;
*.* { *.* {
TextColor: $ColorForeground; TextColor: $ColorForeground;
@@ -80,29 +82,6 @@ $BorderOuterShadow = #a4afc0 ;
Gap: 0; 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 { *.Heading {
Align: middle middle; Align: middle middle;
} }
@@ -112,6 +91,30 @@ $BorderOuterShadow = #a4afc0 ;
Padding: 4 8; Padding: 4 8;
} }
*.Pegboard {
Border: $BorderEngraved / 1, $BorderGap / 1, $BorderInnerShadow / 1 0 0 1;
Padding: 5 4 4 5;
Color: $ColorSunken;
}
*.Segment {
Padding: 8;
}
*.Segment[command] {
Padding: 8 8 0 8;
}
*.Segment[status] {
Color: $ColorOptionSegment;
Border: $BorderOptionSegment / 1 0 0 0;
}
*.Segment[option] {
Color: $ColorOptionSegment;
Border: $BorderOptionSegment / 1 0 0 0;
}
*.Separator { *.Separator {
Border: $BorderEngraved / 1; Border: $BorderEngraved / 1;
} }
@@ -198,10 +201,6 @@ $BorderOuterShadow = #a4afc0 ;
Color: #0000; Color: #0000;
} }
*.MenuItem[hovered] {
Color: $ColorDot;
}
*.MenuItem[focused] { *.MenuItem[focused] {
Color: $ColorDot; Color: $ColorDot;
} }
@@ -218,10 +217,6 @@ $BorderOuterShadow = #a4afc0 ;
Border: $BorderTearPad / 3, $BorderTear / 1; Border: $BorderTearPad / 3, $BorderTear / 1;
} }
*.TearLine[hovered] {
Border: $BorderTearPadFocused / 3, $BorderTearFocused / 1;
}
*.TearLine[focused] { *.TearLine[focused] {
Border: $BorderTearPadFocused / 3, $BorderTearFocused / 1; Border: $BorderTearPadFocused / 3, $BorderTearFocused / 1;
} }
@@ -252,6 +247,23 @@ $BorderOuterShadow = #a4afc0 ;
Color: $ColorCalendarWeekend; Color: $ColorCalendarWeekend;
} }
*.Root {
Color: $ColorBackground;
Gap: 0;
}
*.Root[menu] {
Border: $BorderGap / 1, $BorderLifted / 1;
Color: $ColorBackground;
Gap: 0;
}
*.Root[menu, torn] {
Border: ;
Color: $ColorBackground;
Gap: 0;
}
*.TabbedContainer { *.TabbedContainer {
Gap: 0; Gap: 0;
} }

View File

@@ -1,6 +1,5 @@
package fallbackStyle package fallbackStyle
import "io"
import "bytes" import "bytes"
import "image" import "image"
import _ "embed" import _ "embed"
@@ -10,6 +9,9 @@ import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/backend/style" import "git.tebibyte.media/tomo/backend/style"
// Note: these are color.RGBA values. Remember alpha premultiplication.
var colorTransparent = color.RGBA { A: 0 }
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 }
var colorCarved = color.RGBA { R: 151, G: 160, B: 150, A: 255 } var colorCarved = color.RGBA { R: 151, G: 160, B: 150, A: 255 }
@@ -47,12 +49,6 @@ var borderColorShade = [4]color.Color { colorShade, colorShade, col
//go:embed assets/atlas.png //go:embed assets/atlas.png
var atlasBytes []byte 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 // New returns Wintergreen, the default Tomo style. It is neutral-gray with
// green and turquoise accents. // green and turquoise accents.
func New () (*style.Style, event.Cookie) { func New () (*style.Style, event.Cookie) {
@@ -66,7 +62,7 @@ func New () (*style.Style, event.Cookie) {
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(atlasTexture)
rules := []style.Rule { rules := []style.Rule {
// *.* // *.*
@@ -152,26 +148,12 @@ rules := []style.Rule {
tomo.AGap(0, 0), tomo.AGap(0, 0),
), tomo.R("", "NumberInput")), ), tomo.R("", "NumberInput")),
// *.Container[sunken] // *.Root
style.Ru(style.AS ( style.Ru(style.AS (
tomo.ABorder ( tomo.AGap(0, 0),
outline, ), tomo.R("", "Root")),
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
}),
tomo.AColor(tomo.ColorSunken),
// tomo.ATexture(textureCorkboard),
tomo.APadding(8),
), tomo.R("", "Container"), "sunken"),
// *.Container[outer] // *.Root[menu]
style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorBackground },
tomo.AttrPadding(tomo.I(8)),
), tomo.R("", "Container"), "outer"),
// *.Container[menu]
style.Ru(style.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AttrBorder {
outline, outline,
@@ -182,7 +164,38 @@ rules := []style.Rule {
}, },
tomo.AttrColor { Color: tomo.ColorBackground }, tomo.AttrColor { Color: tomo.ColorBackground },
tomo.AttrGap { }, tomo.AttrGap { },
), tomo.R("", "Container"), "menu"), ), tomo.R("", "Root"), "menu"),
// *.Pegboard
style.Ru(style.AS (
tomo.ABorder (
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
}),
tomo.AColor(tomo.ColorSunken),
// tomo.ATexture(textureCorkboard),
tomo.APadding(8),
), tomo.R("", "Pegboard")),
// *.Segment
style.Ru(style.AS (
tomo.AttrBorder {
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
tomo.AttrColor { Color: tomo.ColorBackground },
tomo.AttrPadding(tomo.I(8)),
), tomo.R("", "Segment")),
// *.Segment[option]
style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorSunken },
tomo.AttrPadding(tomo.I(8)),
), tomo.R("", "Segment"), "option"),
// *.Heading // *.Heading
style.Ru(style.AS ( style.Ru(style.AS (
@@ -369,11 +382,6 @@ rules := []style.Rule {
style.Ru(style.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]
style.Ru(style.AS (
tomo.AttrColor { Color: tomo.ColorAccent },
), tomo.R("", "MenuItem"), "hovered"),
// *.File // *.File
style.Ru(style.AS ( style.Ru(style.AS (
@@ -410,16 +418,6 @@ rules := []style.Rule {
}, },
}, },
), tomo.R("", "TearLine"), "focused"), ), 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 // *.Calendar
style.Ru(style.AS ( style.Ru(style.AS (
@@ -459,52 +457,41 @@ rules := []style.Rule {
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 // *.Notebook
style.Ru(style.AS ( style.Ru(style.AS (
tomo.AGap(0, 0), tomo.AGap(0, 0),
), tomo.R("", "TabbedContainer")), ), tomo.R("", "Notebook")),
// *.PageWrapper
style.Ru(style.AS (
tomo.ABorder (
tomo.Border {
Width: tomo.I(0, 1, 1, 1),
Color: borderColorOutline,
},
tomo.Border {
Width: tomo.I(0, 1, 1, 1),
Color: borderColorLifted,
}),
tomo.AColor(tomo.ColorBackground),
tomo.APadding(8),
), tomo.R("", "PageWrapper")),
// *.TabRow // *.TabRow
style.Ru(style.AS ( style.Ru(style.AS (
tomo.AttrBorder { tomo.AGap(0, 0),
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.AttrPadding(tomo.I(1, 0, 0, 0)),
), tomo.R("", "TabRow")), ), 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] // *.TabSpacer[right]
style.Ru(style.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),
Color: [4]color.Color { Color: [4]color.Color {
colorGutter, colorGutter, colorTransparent, colorTransparent,
colorGutter, colorGutter, colorTransparent, colorTransparent,
}, },
}, },
tomo.Border { tomo.Border {
@@ -515,10 +502,6 @@ rules := []style.Rule {
Width: tomo.I(0, 0, 1, 1), Width: tomo.I(0, 0, 1, 1),
Color: borderColorOutline, Color: borderColorOutline,
}, },
tomo.Border {
Width: tomo.I(0, 0, 0, 1),
Color: borderColorShade,
},
}, },
tomo.AttrMinimumSize { X: 3 }, tomo.AttrMinimumSize { X: 3 },
), tomo.R("", "TabSpacer"), "right"), ), tomo.R("", "TabSpacer"), "right"),
@@ -529,8 +512,8 @@ rules := []style.Rule {
tomo.Border { tomo.Border {
Width: tomo.I(1, 0, 0, 0), Width: tomo.I(1, 0, 0, 0),
Color: [4]color.Color { Color: [4]color.Color {
colorGutter, colorGutter, colorTransparent, colorTransparent,
colorGutter, colorGutter, colorTransparent, colorTransparent,
}, },
}, },
tomo.Border { tomo.Border {

View File

@@ -1,7 +1,6 @@
package tss package tss
import "os" import "os"
import "io"
import "fmt" import "fmt"
import "image" import "image"
import "errors" import "errors"
@@ -81,7 +80,7 @@ func (this *styleBuilder) build () (*style.Style, event.Cookie, error) {
// add each texture to the cookies list // add each texture to the cookies list
for _, texture := range this.textures { for _, texture := range this.textures {
cookies = append(cookies, closerCookie { Closer: texture }) cookies = append(cookies, texture)
} }
return sty, event.MultiCookie(cookies...), nil return sty, event.MultiCookie(cookies...), nil
@@ -398,10 +397,3 @@ func copyBorderValue[T any, U ~[]T] (destination, source U) bool {
return false return false
} }
} }
type closerCookie struct {
io.Closer
}
func (cookie closerCookie) Close () {
cookie.Closer.Close()
}

View File

@@ -7,11 +7,6 @@ var manager struct {
count int count int
} }
type funcCookie func ()
func (cookie funcCookie) Close () {
cookie()
}
// WaitFor ensures that the application will stay running while the given window // WaitFor ensures that the application will stay running while the given window
// is open. // is open.
func WaitFor (window tomo.Window) event.Cookie { func WaitFor (window tomo.Window) event.Cookie {
@@ -27,7 +22,12 @@ func WaitFor (window tomo.Window) event.Cookie {
} }
} }
handleWaitClose := func () error {
handleClose();
return nil
}
return event.MultiCookie ( return event.MultiCookie (
window.OnClose(handleClose), window.OnClose(handleClose),
funcCookie(handleClose)) event.FuncCookie(handleWaitClose))
} }

11
path.go
View File

@@ -11,8 +11,9 @@ func ApplicationUserDataDir (app ApplicationDescription) (string, error) {
} }
// ApplicationSystemDataDirs returns a list of directory paths where an // ApplicationSystemDataDirs returns a list of directory paths where an
// application can look for its system-level data files. These directories may // application can look for its system-level data files. Directories returned
// or may not exist. This function may return an empty slice on some platforms. // by this function may or may not actually exist. This function may return an
// empty slice on some platforms.
func ApplicationSystemDataDirs (app ApplicationDescription) ([]string, error) { func ApplicationSystemDataDirs (app ApplicationDescription) ([]string, error) {
return systemDirs(app.ID, systemDataDirs) return systemDirs(app.ID, systemDataDirs)
} }
@@ -24,9 +25,9 @@ func ApplicationUserConfigDir (app ApplicationDescription) (string, error) {
} }
// ApplicationSystemDataDirs returns a list of directory paths where an // ApplicationSystemDataDirs returns a list of directory paths where an
// application can look for its system-level configuration files. These // application can look for its system-level configuration files. Directories
// directories may or may not exist. This function may return an empty slice on // returned by this function may or may not actually exist. This function may
// some platforms. // return an empty slice on some platforms.
func ApplicationSystemConfigDirs (app ApplicationDescription) ([]string, error) { func ApplicationSystemConfigDirs (app ApplicationDescription) ([]string, error) {
return systemDirs(app.ID, systemConfigDirs) return systemDirs(app.ID, systemConfigDirs)
} }