12 Commits

Author SHA1 Message Date
a5f7feb5eb Remove the FS thing and use normal paths 2024-06-06 22:55:14 -04:00
b87f3445e4 Revert aluminum styling changes for TextBox 2024-06-06 22:42:37 -04:00
1bc08bcfe4 Add ApplicationURLOpener interface
Eventually we can have nasin parse cli args and figure out what
files to open, instructing the application to open those files.
We will also be able to have nasin connect to dbus using the
application ID and open files in an already running instance of the
application.
2024-06-06 22:38:51 -04:00
d5d9f3abfb NewApplicationWindow automatically sets an application icon 2024-06-06 20:36:25 -04:00
6cb908ea6e Tweaks to the aluminum theme 2024-06-03 22:49:02 -04:00
2db501e66c Update clock example 2024-06-03 22:03:19 -04:00
8bd6fac8a8 Update internal theme to use Roles stored within Boxes 2024-06-03 21:59:28 -04:00
4bb7539718 Update dependencies 2024-06-03 21:59:08 -04:00
7511262309 Update backend version 2024-06-03 03:48:25 -04:00
19b71a7cec Add aluminum style 2024-06-03 03:48:13 -04:00
f9432efc82 Stop using the monolithic backend 2024-06-03 03:04:47 -04:00
8d9bdd5cb8 Restructure internal/theme 2024-06-03 03:04:29 -04:00
23 changed files with 466 additions and 211 deletions

View File

@@ -2,6 +2,8 @@ package nasin
import "log"
import "image"
import "strings"
import "net/url"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/nasin/internal/registrar"
@@ -14,6 +16,20 @@ type Application interface {
Init () error
}
// ApplicationURLOpener is an application that can open a URL.
type ApplicationURLOpener interface {
Application
// OpenURL opens a new window with the contents of the given URL. If the
// given URL is unsupported, it returns an error (for example, an image
// viewer is not expected to open a text file). If the URL scheme is
// empty, file:// is assumed.
//
// Applications should support the file:// scheme at the very least, and
// should also support others like http:// and https:// if possible.
OpenURL (*url.URL) error
}
// ApplicationDescription describes the name and type of an application.
type ApplicationDescription struct {
// The name of the application.
@@ -76,6 +92,15 @@ type ApplicationRole string; const (
RoleChecklist ApplicationRole = "Checklist"
)
// Icon returns the icon ID for this role.
func (role ApplicationRole) Icon () tomo.Icon {
if role == "" {
return tomo.IconApplication
} else {
return tomo.Icon("Application" + strings.ReplaceAll(string(role), " ", ""))
}
}
// RunApplication is like tomo.Run, but runs an application. If something fails
// to initialize, an error is written to the standard logger.
func RunApplication (application Application) {
@@ -90,10 +115,37 @@ func RunApplication (application Application) {
// NewApplicationWindow creates a window for an application. It will
// automatically set window information to signal to the OS that the window is
// owned by the application.
// 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
// found, the default icon for the application's ApplicationRole will used.
func NewApplicationWindow (application Application, bounds image.Rectangle) (tomo.MainWindow, error) {
window, err := tomo.NewWindow(bounds)
if err != nil { return nil, err }
window.SetTitle(application.Describe().String())
description := application.Describe()
window.SetTitle(description.Name)
setApplicationWindowIcon(window, description)
return window, nil
}
func setApplicationWindowIcon (window tomo.Window, description ApplicationDescription) {
allSizes := func (icon tomo.Icon) (sizes []image.Image) {
small := icon.Texture(tomo.IconSizeSmall)
medium := icon.Texture(tomo.IconSizeMedium)
large := icon.Texture(tomo.IconSizeLarge)
if small != nil { sizes = append(sizes, small) }
if medium != nil { sizes = append(sizes, medium) }
if large != nil { sizes = append(sizes, large) }
return sizes
}
if sizes := allSizes(tomo.Icon(description.ID)); len(sizes) > 0 {
println("direct icon worked", tomo.Icon(description.ID))
window.SetIcon(sizes...)
return
}
if sizes := allSizes(description.Role.Icon()); len(sizes) > 0 {
println("role icon worked", description.Role.Icon())
window.SetIcon(sizes...)
return
}
}

View File

@@ -68,7 +68,8 @@ func NewClockFace () *ClockFace {
box := &ClockFace {
CanvasBox: tomo.NewCanvasBox(),
}
tomo.Apply(box, tomo.R("nasin", "ClockFace", ""))
box.SetRole(tomo.R("nasin", "ClockFace", ""))
tomo.Apply(box)
box.SetDrawer(box)
return box
}

View File

@@ -7,7 +7,7 @@ import "git.tebibyte.media/tomo/nasin"
import "git.tebibyte.media/tomo/objects"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/objects/layouts"
import "git.tebibyte.media/tomo/nasin/internal/theme/xdgicons"
import "git.tebibyte.media/tomo/nasin/internal/theme/icons/xdg"
const scrollIcons = true

10
go.mod
View File

@@ -3,9 +3,9 @@ module git.tebibyte.media/tomo/nasin
go 1.20
require (
git.tebibyte.media/tomo/objects v0.15.0
git.tebibyte.media/tomo/tomo v0.34.0
git.tebibyte.media/tomo/x v0.9.0
git.tebibyte.media/tomo/backend v0.2.0
git.tebibyte.media/tomo/objects v0.16.0
git.tebibyte.media/tomo/tomo v0.35.0
git.tebibyte.media/tomo/xdg v0.1.0
golang.org/x/image v0.11.0
)
@@ -15,6 +15,6 @@ require (
git.tebibyte.media/tomo/xgbkb v1.0.1 // 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/jezek/xgb v1.1.0 // indirect
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111 // indirect
)

18
go.sum
View File

@@ -1,12 +1,12 @@
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/objects v0.15.0 h1:xCrRqOwCz8jDJk/sWw0B1HyrRCJafAuaPWN9nZj8V1U=
git.tebibyte.media/tomo/objects v0.15.0/go.mod h1:++pM0y/xuzhgmu1RpHTWQlqrmyHLfPEF9ahyrH8Tqvk=
git.tebibyte.media/tomo/tomo v0.34.0 h1:r5yJPks9rtzdDI2RyAUdqa1qb6BebG0QFe2cTmcFi+0=
git.tebibyte.media/tomo/tomo v0.34.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/backend v0.2.0 h1:9vMQxIRnfzyMVGP3wBTz9qoWzSwLcCYzKIogikk05a0=
git.tebibyte.media/tomo/backend v0.2.0/go.mod h1:os77Uo/YkQ9EmNZK8U1ljsEUkTK36P27dKmp5p8TMeE=
git.tebibyte.media/tomo/objects v0.16.0 h1:Q/w8yHZepj0ESYtPGvWtXxZXkTU+k03rfPxvoxXU5AQ=
git.tebibyte.media/tomo/objects v0.16.0/go.mod h1:jRRpraLGpBzwiOv6kl1Ram229BpJZxJaS4eqIhze194=
git.tebibyte.media/tomo/tomo v0.35.0 h1:1XvcUcWg1rBZXov3KfuX6VfiuBQ2mcJHIslHMLn07no=
git.tebibyte.media/tomo/tomo v0.35.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
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/x v0.9.0 h1:wMcbK0MOE7ea7wcU2Mgrr86ZprVGLRY2PgOG0vDJR6Y=
git.tebibyte.media/tomo/x v0.9.0/go.mod h1:OO4PYXhzrh4ZAY12d7bg+l/P4MbkFPu6f+YVXNDRhog=
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/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE=
@@ -15,10 +15,12 @@ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJ
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 h1:Pf/0BAbppEOq4azPH6fnvUX2dycAwZdGkdxFn25j44c=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0/go.mod h1:AHecLyFNy6AN9f/+0AH/h1MI7X1+JL5bmCz4XlVZk7Y=
github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111 h1:cX/mTy4LgFtWqr5dCadtdJ4zdh/KtPco5yFLsliaFyU=
github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111/go.mod h1:AHecLyFNy6AN9f/+0AH/h1MI7X1+JL5bmCz4XlVZk7Y=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

View File

@@ -3,13 +3,24 @@ package registrar
import "os"
import "log"
import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/backend/x"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/nasin/internal/theme/default"
import "git.tebibyte.media/tomo/nasin/internal/theme/xdgicons"
import dataTheme "git.tebibyte.media/tomo/nasin/internal/theme"
import "git.tebibyte.media/tomo/nasin/internal/theme/icons/xdg"
import "git.tebibyte.media/tomo/nasin/internal/theme/icons/fallback"
import "git.tebibyte.media/tomo/nasin/internal/theme/style/fallback"
import "git.tebibyte.media/tomo/nasin/internal/theme/style/aluminum"
func Init () error {
theme := defaultTheme.Theme()
var theme *dataTheme.Theme
// TODO eventually get rid of this when we make a file format for
// storing visual styles
if os.Getenv("TOMO_USE_ALUMINUM_STYLE") != "" {
theme = aluminumStyle.New()
} else {
theme = fallbackStyle.New()
}
theme.IconTheme = fallbackIcons.New()
iconThemeName := os.Getenv("TOMO_XDG_ICON_THEME")
if iconThemeName != "" {
@@ -22,6 +33,6 @@ func Init () error {
}
tomo.SetTheme(theme)
tomo.Register(1, x.NewBackend)
tomo.Register(1, x.New)
return nil
}

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,4 +1,4 @@
package defaultTheme
package fallbackIcons
import "bytes"
import "image"
@@ -7,6 +7,7 @@ import _ "image/png"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas"
import dataTheme "git.tebibyte.media/tomo/nasin/internal/theme"
//go:embed assets/icons-small.png
var atlasSmallBytes []byte
@@ -416,6 +417,11 @@ type iconTheme struct {
texturesLarge map[tomo.Icon] canvas.Texture
}
// New creates a new fallback icon theme.
func New () dataTheme.IconTheme {
return new(iconTheme)
}
func (this *iconTheme) ensure () {
if this.texturesSmall != nil { return }
this.texturesSmall = generateSource(atlasSmallBytes, 16)

View File

@@ -54,14 +54,14 @@ func (this *iconTheme) xdgIcon (name string, size tomo.IconSize) (canvas.Texture
iconFile, err := os.Open(icon.Path)
if err != nil {
// this failing indicates a broken icon theme
log.Println("nasin: icon file '%s' is inaccessible: %v", icon.Path, err)
log.Printf("nasin: icon file '%s' is inaccessible: %v\n", icon.Path, err)
return nil, false
}
iconImage, _, err := image.Decode(iconFile)
if err != nil {
// this failing indicates a broken icon theme
log.Println("nasin: icon file '%s' is broken: %v", icon.Path, err)
log.Printf("nasin: icon file '%s' is broken: %v\n", icon.Path, err)
return nil, false
}

View File

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

View File

@@ -0,0 +1,311 @@
package aluminumStyle
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/font/basicfont"
import dataTheme "git.tebibyte.media/tomo/nasin/internal/theme"
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 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 borderInnerShadow = border(0xa4afc0FF, 0xa4afc0FF, 0xa4afc0ff, 0xa4afc0ff, 1, 0, 0, 1)
var borderOuterShadow = border(0xa4afc0FF, 0xa4afc0FF, 0xa4afc0ff, 0xa4afc0ff, 0, 1, 1, 0)
var rules = []dataTheme.Rule {
// *.*[*]
dataTheme.Rule {
Default: dataTheme.AS (
dataTheme.AttrFace { Face: basicfont.Face7x13 },
dataTheme.AttrTextColor { Color: tomo.ColorForeground },
dataTheme.AttrDotColor { Color: colorDot },
dataTheme.AttrGap { X: 8, Y: 8 },
),
},
// *.Button[*]
dataTheme.Rule {
Role: tomo.R("", "Button", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderGap,
borderLifted,
},
dataTheme.AttrPadding(tomo.I(4, 8)),
dataTheme.AttrColor { Color: tomo.ColorRaised },
),
Pressed: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
dataTheme.AttrPadding(tomo.I(5, 8, 4, 9)),
dataTheme.AttrColor { Color: colorRaisedPressed },
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderGap,
borderLiftedFocused,
},
dataTheme.AttrPadding(tomo.I(4, 8)),
dataTheme.AttrColor { Color: colorRaisedFocused },
),
Hovered: dataTheme.AS (
dataTheme.AttrColor { Color: colorRaisedHovered },
),
},
// *.TextInput[*]
dataTheme.Rule {
Role: tomo.R("", "TextInput", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
dataTheme.AttrColor { Color: tomo.ColorSunken },
dataTheme.AttrPadding(tomo.I(5, 4, 4, 5)),
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderFocused,
borderInnerShadow,
},
),
},
// *.TextView[*]
dataTheme.Rule {
Role: tomo.R("", "TextView", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
dataTheme.AttrColor { Color: tomo.ColorSunken },
dataTheme.AttrPadding(tomo.I(8)),
),
},
// *.NumberInput[*]
dataTheme.Rule {
Role: tomo.R("", "NumberInput", ""),
Default: dataTheme.AS (
dataTheme.AttrGap { },
),
},
// *.Container[sunken]
dataTheme.Rule {
Role: tomo.R("", "Container", "sunken"),
Default: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
dataTheme.AttrColor { Color: tomo.ColorSunken },
dataTheme.AttrPadding(tomo.I(8)),
),
},
// *.Container[outer]
dataTheme.Rule {
Role: tomo.R("", "Container", "outer"),
Default: dataTheme.AS (
dataTheme.AttrColor { Color: tomo.ColorBackground },
dataTheme.AttrPadding(tomo.I(8)),
),
},
// *.Container[menu]
dataTheme.Rule {
Role: tomo.R("", "Container", "menu"),
Default: dataTheme.AS (
dataTheme.AttrBorder {
borderGap,
borderLifted,
},
dataTheme.AttrColor { Color: tomo.ColorBackground },
dataTheme.AttrGap { },
),
},
// *.Heading[*]
dataTheme.Rule {
Role: tomo.R("", "Heading", ""),
Default: dataTheme.AS (
dataTheme.AttrAlign { X: tomo.AlignMiddle, Y: tomo.AlignMiddle },
),
},
// *.Separator[*]
dataTheme.Rule {
Role: tomo.R("", "Separator", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
},
),
},
// *.Slider[*]
dataTheme.Rule {
Role: tomo.R("", "Slider", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
dataTheme.AttrColor { Color: colorGutter },
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderFocused,
borderInnerShadow,
},
),
Hovered: dataTheme.AS (
dataTheme.AttrColor { Color: colorGutterHovered },
),
},
// *.Slider[horizontal]
dataTheme.Rule {
Role: tomo.R("", "Slider", "horizontal"),
Default: dataTheme.AS(dataTheme.AttrMinimumSize { X: 48 }),
},
// *.Slider[vertical]
dataTheme.Rule {
Role: tomo.R("", "Slider", "vertical"),
Default: dataTheme.AS(dataTheme.AttrMinimumSize { Y: 48 }),
},
// *.SliderHandle[*]
dataTheme.Rule {
Role: tomo.R("", "SliderHandle", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
borderOuterShadow,
borderGap,
borderLifted,
},
dataTheme.AttrColor { Color: tomo.ColorRaised },
dataTheme.AttrMinimumSize { X: 12, Y: 12, },
),
},
// *.Checkbox[*]
dataTheme.Rule {
Role: tomo.R("", "Checkbox", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
dataTheme.AttrColor { Color: tomo.ColorSunken },
dataTheme.AttrPadding(tomo.I(0, 1, 1, 0)),
dataTheme.AttrMinimumSize { X: 19, Y: 19 },
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
borderEngraved,
borderFocused,
borderInnerShadow,
},
dataTheme.AttrPadding(tomo.I(0)),
dataTheme.AttrColor { Color: colorSunkenFocused },
),
},
// *.LabelCheckbox[*]
dataTheme.Rule {
Role: tomo.R("", "LabelCheckbox", ""),
Default: dataTheme.AS (
dataTheme.AttrGap { X: 8, Y: 8 },
),
},
// *.MenuItem[*]
dataTheme.Rule {
Role: tomo.R("", "MenuItem", ""),
Default: dataTheme.AS (
dataTheme.AttrPadding(tomo.I(4)),
dataTheme.AttrGap { X: 4, Y: 4 },
dataTheme.AttrColor { Color: color.Transparent },
),
Hovered: dataTheme.AS (
dataTheme.AttrColor { Color: colorDot },
),
Focused: dataTheme.AS (
dataTheme.AttrColor { Color: colorDot },
),
},
// *.File[*]
dataTheme.Rule {
Role: tomo.R("", "File", ""),
Default: dataTheme.AS (
dataTheme.AttrColor { Color: color.Transparent },
),
Focused: dataTheme.AS (
dataTheme.AttrColor { Color: colorDot },
),
},
}

View File

@@ -1,4 +1,4 @@
package defaultTheme
package fallbackStyle
import "image/color"
import "git.tebibyte.media/tomo/tomo"
@@ -295,4 +295,15 @@ var rules = []dataTheme.Rule {
dataTheme.AttrColor { Color: tomo.ColorAccent },
),
},
// *.File[*]
dataTheme.Rule {
Role: tomo.R("", "File", ""),
Default: dataTheme.AS (
dataTheme.AttrColor { Color: color.Transparent },
),
Focused: dataTheme.AS (
dataTheme.AttrColor { Color: tomo.ColorAccent },
),
},
}

View File

@@ -1,12 +1,12 @@
package defaultTheme
package fallbackStyle
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import dataTheme "git.tebibyte.media/tomo/nasin/internal/theme"
// Theme returns Wintergreen, the default Tomo tomo. It is neutral-gray with
// green and turquoise accents.
func Theme () *dataTheme.Theme {
// New returns Wintergreen, the default Tomo tomo. It is neutral-gray with green
// and turquoise accents.
func New () *dataTheme.Theme {
return &dataTheme.Theme {
Colors: map[tomo.Color] color.Color {
tomo.ColorBackground: colorBackground,
@@ -16,6 +16,5 @@ func Theme () *dataTheme.Theme {
tomo.ColorAccent: colorFocus,
},
Rules: rules,
IconTheme: &iconTheme { },
}
}

View File

@@ -186,10 +186,11 @@ func (this *Theme) setsFor (role tomo.Role) (defaul, hovered, pressed, focused A
return defaul, hovered, pressed, focused
}
func (this *Theme) Apply (object tomo.Object, role tomo.Role) event.Cookie {
func (this *Theme) Apply (object tomo.Object) event.Cookie {
pressed := false
hovered := false
box := object.GetBox()
box := object.GetBox()
role := box.Role()
defaultSet, hoveredSet, pressedSet, focusedSet := this.setsFor(role)

198
path.go
View File

@@ -1,190 +1,32 @@
package nasin
import "io"
import "os"
import "io/fs"
import "strings"
import "path/filepath"
// FS is Tomo's implementation of fs.FS. It provides access to a specific part
// of the filesystem.
type FS struct {
path string
// ApplicationUserDataDir returns the directory path where an application can
// store its user data files. If the directory does not exist, it will be
// created.
func ApplicationUserDataDir (app ApplicationDescription) (string, error) {
return userMkdirAll(app.ID, userDataDir)
}
// FileWriter is a writable version of fs.File.
type FileWriter interface {
fs.File
io.Writer
// ApplicationUserConfigDir returns the directory path where an application can
// store its user configuration files.
func ApplicationUserConfigDir (app ApplicationDescription) (string, error) {
return userMkdirAll(app.ID, userConfigDir)
}
// ApplicationUserDataFS returns an FS that an application can use to store user
// data files.
func ApplicationUserDataFS (app ApplicationDescription) (*FS, error) {
dataDir, err := userDataDir()
if err != nil { return nil, err }
return appFs(dataDir, app)
// ApplicationUserCacheDir returns the directory path where an application can
// store its user cache files.
func ApplicationUserCacheDir (app ApplicationDescription) (string, error) {
return userMkdirAll(app.ID, userCacheDir)
}
// ApplicationUserConfigFS returns an FS that an application can use to store
// user configuration files.
func ApplicationUserConfigFS (app ApplicationDescription) (*FS, error) {
configDir, err := userConfigDir()
if err != nil { return nil, err }
return appFs(configDir, app)
}
// ApplicationUserCacheFS returns an FS that an application can use to store
// user cache files.
func ApplicationUserCacheFS (app ApplicationDescription) (*FS, error) {
cacheDir, err := userCacheDir()
if err != nil { return nil, err }
return appFs(cacheDir, app)
}
func pathErr (op, path string, err error) error {
return &fs.PathError {
Op: op,
Path: path,
Err: err,
}
}
func appFs (root string, app ApplicationDescription) (*FS, error) {
// remove slashes
appid := app.ID
appid = strings.ReplaceAll(appid, "/", "-")
appid = strings.ReplaceAll(appid, "\\", "-")
path := filepath.Join(root, appid)
// ensure the directory actually exists
err := os.MkdirAll(path, 700)
if err != nil { return nil, err }
return &FS { path: path }, nil
}
func (this FS) subPath (name string) (string, error) {
if !fs.ValidPath(name) { return "", fs.ErrInvalid }
if strings.Contains(name, "/") { return "", fs.ErrInvalid }
return filepath.Join(this.path, name), nil
}
// Open opens the named file.
func (this FS) Open (name string) (fs.File, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("open", name, err)
}
return os.Open(path)
}
// Create creates or truncates the named file.
func (this FS) Create (name string) (FileWriter, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("create", name, err)
}
return os.Create(path)
}
// OpenFile is the generalized open call; most users will use Open or Create
// instead.
func (this FS) OpenFile (
name string,
flag int,
perm os.FileMode,
) (
FileWriter,
error,
) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("open", name, err)
}
return os.OpenFile(path, flag, perm)
}
// ReadDir reads the named directory and returns a list of directory entries
// sorted by filename.
func (this FS) ReadDir (name string) ([]fs.DirEntry, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("readdir", name, err)
}
return os.ReadDir(path)
}
// ReadFile reads the named file and returns its contents.
// A successful call returns a nil error, not io.EOF.
// (Because ReadFile reads the whole file, the expected EOF
// from the final Read is not treated as an error to be reported.)
//
// The caller is permitted to modify the returned byte slice.
func (this FS) ReadFile (name string) ([]byte, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("readfile", name, err)
}
return os.ReadFile(path)
}
// WriteFile writes data to the named file, creating it if necessary.
func (this FS) WriteFile (name string, data []byte, perm os.FileMode) error {
path, err := this.subPath(name)
if err != nil {
return pathErr("writefile", name, err)
}
return os.WriteFile(path, data, perm)
}
// Stat returns a FileInfo describing the file.
func (this FS) Stat (name string) (fs.FileInfo, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("stat", name, err)
}
return os.Stat(path)
}
// Remove removes the named file or (empty) directory.
func (this FS) Remove (name string) error {
path, err := this.subPath(name)
if err != nil {
return pathErr("remove", name, err)
}
return os.Remove(path)
}
// RemoveAll removes name and any children it contains.
func (this FS) RemoveAll (name string) error {
path, err := this.subPath(name)
if err != nil {
return pathErr("removeall", name, err)
}
return os.RemoveAll(path)
}
// Rename renames (moves) oldname to newname.
func (this FS) Rename (oldname, newname string) error {
oldpath, err := this.subPath(oldname)
if err != nil {
return pathErr("rename", oldname, err)
}
newpath, err := this.subPath(newname)
if err != nil {
return pathErr("rename", newname, err)
}
return os.Rename(oldpath, newpath)
func userMkdirAll (sub string, getter func () (string, error)) (string, error) {
path, err := getter()
if err != nil { return "", err }
path = filepath.Join(path, sub)
err = os.MkdirAll(path, 0700)
if err != nil { return "", err }
return path, nil
}