24 Commits

Author SHA1 Message Date
ac51d3dc9f Upgrade objects, x 2024-05-13 19:56:07 -04:00
573ab6bcdc Update Wintergreen theme 2024-05-13 19:55:57 -04:00
c1686e336b Upgrade X version 2024-05-07 20:25:15 -04:00
dfd566b23d Include new objects in Wintergreen theme 2024-05-07 20:22:53 -04:00
da4af8d240 Add examples 2024-05-07 20:22:39 -04:00
d90fb327db Add icons example 2024-05-07 01:30:12 -04:00
2b9f17b612 Add clock example 2024-05-07 01:30:06 -04:00
2cde2cc6d5 Update X backend 2024-05-07 00:46:52 -04:00
69576c7aca Add basic icons to wintergreen theme 2024-05-06 23:25:53 -04:00
a11cc2cb89 Rename registry to registrar 2024-05-06 23:25:44 -04:00
f59949c3bb Integrated Wintergreen theme as default 2024-05-03 19:58:28 -04:00
a5dfdda651 Theme improvements 2024-05-03 19:58:10 -04:00
82657bf251 Data driven themes update their style on application 2024-05-03 16:23:21 -04:00
992a5b23f9 Color map takes in any color value 2024-05-03 15:36:48 -04:00
f45476a5c9 Changed Theme.Color to Theme.RGBA 2024-05-03 15:29:04 -04:00
4a400b68c2 Added mechanism for data-driven themes 2024-05-03 15:25:34 -04:00
2d7ac914a4 Update go.sum 2024-05-03 15:25:27 -04:00
f39e5245fc Upgrade Tomo version 2024-05-03 13:39:42 -04:00
7e879defbd Fix RunApplication 2024-05-03 13:27:32 -04:00
aeea57a09c What egven 2024-05-03 13:26:33 -04:00
407049097d Applications can return init errors 2024-05-03 12:46:53 -04:00
7c7d93d6d1 Backend registering is platform-dependent 2024-05-03 12:46:27 -04:00
2eb82d9035 Add application FS fuctionality 2024-05-03 12:30:52 -04:00
ab0d84140f Add godoc badge to README.md 2024-04-30 04:31:02 +00:00
20 changed files with 1239 additions and 22 deletions

View File

@@ -1,4 +1,6 @@
# nasin # 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. Parts of Tomo that Nasin provides an easy way to write applications with Tomo. Parts of Tomo that
aren't the GUI toolkit may be found here. aren't the GUI toolkit may be found here.

View File

@@ -1,7 +1,9 @@
package nasin package nasin
import "log"
import "image" import "image"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/nasin/internal/registrar"
// Application represents an application object. // Application represents an application object.
type Application interface { type Application interface {
@@ -9,7 +11,7 @@ type Application interface {
Describe () ApplicationDescription Describe () ApplicationDescription
// Init performs the initial setup of the application. // Init performs the initial setup of the application.
Init () Init () error
} }
// ApplicationDescription describes the name and type of an application. // ApplicationDescription describes the name and type of an application.
@@ -74,9 +76,16 @@ type ApplicationRole string; const (
RoleChecklist ApplicationRole = "Checklist" RoleChecklist ApplicationRole = "Checklist"
) )
// RunApplication is like Run, but runs an application. // RunApplication is like tomo.Run, but runs an application. If something fails
func RunApplication (application Application) error { // to initialize, an error is written to the standard logger.
return tomo.Run(application.Init) func RunApplication (application Application) {
err := registrar.Init()
if err != nil { log.Fatal("nasin: could not init registry:", err) }
err = tomo.Run(func () {
err := application.Init()
if err != nil { log.Fatal("nasin: could not run application:", err) }
})
if err != nil { log.Fatal("nasin: could not run application:", err) }
} }
// NewApplicationWindow creates a window for an application. It will // NewApplicationWindow creates a window for an application. It will

View File

@@ -1,8 +0,0 @@
package nasin
import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/tomo"
func init () {
tomo.Register(1, x.NewBackend)
}

126
examples/clock/main.go Normal file
View File

@@ -0,0 +1,126 @@
// Example clock demonstrates the use of goroutines and tomo.Do() to provide
// live-updating information.
package main
import "time"
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/nasin"
import "git.tebibyte.media/tomo/objects"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/objects/layouts"
type Application struct {
clockFace *ClockFace
timeLabel *objects.Label
}
func (this *Application) Describe () nasin.ApplicationDescription {
return nasin.ApplicationDescription {
Name: "Clock",
ID: "xyz.holanet.TomoClockExample",
}
}
func (this *Application) Init () error {
window, err := nasin.NewApplicationWindow(this, image.Rect(0, 0, 128, 160))
if err != nil { return err }
this.clockFace = NewClockFace()
this.timeLabel = objects.NewLabel("")
this.timeLabel.SetAlign(tomo.AlignMiddle, tomo.AlignMiddle)
container := objects.NewOuterContainer (
layouts.NewGrid([]bool { true }, []bool { true, false }),
this.clockFace,
this.timeLabel)
window.SetRoot(container)
go func () {
for {
tomo.Do(this.updateTime)
time.Sleep(time.Second)
}
} ()
window.OnClose(tomo.Stop)
window.Show()
return nil
}
func (this *Application) updateTime () {
now := time.Now()
this.clockFace.SetTime(now)
this.timeLabel.SetText(now.Format(time.DateTime))
}
func main () {
nasin.RunApplication(&Application { })
}
type ClockFace struct {
tomo.CanvasBox
time time.Time
}
func NewClockFace () *ClockFace {
box := &ClockFace {
CanvasBox: tomo.NewCanvasBox(),
}
theme.Apply(box, theme.R("nasin", "ClockFace", ""))
box.SetDrawer(box)
return box
}
// TODO move ClockFace to objects
func (this *ClockFace) SetTime (when time.Time) {
this.time = when
this.Invalidate()
}
func (this *ClockFace) Draw (destination canvas.Canvas) {
pen := destination.Pen()
pen.Fill(color.Transparent)
pen.Rectangle(destination.Bounds())
for hour := 0; hour < 12; hour ++ {
radialLine (
destination,
theme.ColorForeground,
0.8, 0.9, float64(hour) / 6 * math.Pi)
}
second := float64(this.time.Second())
minute := float64(this.time.Minute()) + second / 60
hour := float64(this.time.Hour()) + minute / 60
radialLine(destination, theme.ColorForeground, 0, 0.5, (hour - 3) / 6 * math.Pi)
radialLine(destination, theme.ColorForeground, 0, 0.7, (minute - 15) / 30 * math.Pi)
radialLine(destination, theme.ColorAccent, 0, 0.7, (second - 15) / 30 * math.Pi)
}
func radialLine (
destination canvas.Canvas,
source color.Color,
inner float64,
outer float64,
theta float64,
) {
pen := destination.Pen()
bounds := destination.Bounds()
width := float64(bounds.Dx()) / 2
height := float64(bounds.Dy()) / 2
min := bounds.Min.Add(image.Pt (
int(math.Cos(theta) * inner * width + width),
int(math.Sin(theta) * inner * height + height)))
max := bounds.Min.Add(image.Pt (
int(math.Cos(theta) * outer * width + width),
int(math.Sin(theta) * outer * height + height)))
pen.Stroke(source)
pen.StrokeWeight(1)
pen.Path(min, max)
}

199
examples/icons/main.go Normal file
View File

@@ -0,0 +1,199 @@
// Example icons demonstrates the use of icons, and buttons with icons.
package main
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/nasin"
import "git.tebibyte.media/tomo/objects"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/objects/layouts"
const scrollIcons = false
type Application struct {
size theme.IconSize
grid tomo.ContainerBox
}
func (this *Application) Describe () nasin.ApplicationDescription {
return nasin.ApplicationDescription {
Name: "Tomo Icon Example",
ID: "xyz.holanet.TomoIconExample",
}
}
func (this *Application) Init () error {
window, err := nasin.NewApplicationWindow(this, image.Rect(0, 0, 128, 128))
if err != nil { return err }
this.grid = objects.NewInnerContainer (
layouts.NewGrid([]bool {
false, false, false, false, false, false, false, false, false, false, false, false,
}, []bool { }),
)
this.resizeIcons(theme.IconSizeSmall)
iconButtons := objects.NewInnerContainer(layouts.NewGrid([]bool { true, true, true}, []bool { false }))
button := objects.NewButton("small")
button.SetIcon(theme.IconActionZoomOut)
button.OnClick(func () { this.resizeIcons(theme.IconSizeSmall) })
iconButtons.Add(button)
button = objects.NewButton("medium")
button.SetIcon(theme.IconActionZoomReset)
button.OnClick(func () { this.resizeIcons(theme.IconSizeMedium) })
iconButtons.Add(button)
button = objects.NewButton("large")
button.SetIcon(theme.IconActionZoomIn)
button.OnClick(func () { this.resizeIcons(theme.IconSizeLarge) })
iconButtons.Add(button)
container := objects.NewOuterContainer (
layouts.NewGrid([]bool { true }, []bool { false, true, false }),
objects.NewLabel("A smorgasbord of icons:"))
if scrollIcons {
iconScroller := objects.NewScrollContainer(objects.ScrollVertical)
this.grid.SetOverflow(false, true)
iconScroller.SetRoot(this.grid)
container.Add(iconScroller)
} else {
container.Add(this.grid)
}
container.Add(iconButtons)
window.SetRoot(container)
window.OnClose(tomo.Stop)
window.Show()
return nil
}
func (this *Application) resizeIcons (size theme.IconSize) {
this.size = size
this.grid.Clear()
icons := []theme.Icon {
theme.IconUnknown,
theme.IconFile,
theme.IconDirectory,
theme.IconStorage,
theme.IconApplication,
theme.IconNetwork,
theme.IconDevice,
theme.IconPeripheral,
theme.IconPort,
theme.IconActionOpen,
theme.IconActionOpenIn,
theme.IconActionSave,
theme.IconActionSaveAs,
theme.IconActionPrint,
theme.IconActionNew,
theme.IconActionNewDirectory,
theme.IconActionDelete,
theme.IconActionRename,
theme.IconActionGetInformation,
theme.IconActionChangePermissions,
theme.IconActionRevert,
theme.IconActionAdd,
theme.IconActionRemove,
theme.IconActionAddBookmark,
theme.IconActionRemoveBookmark,
theme.IconActionAddFavorite,
theme.IconActionRemoveFavorite,
theme.IconActionPlay,
theme.IconActionPause,
theme.IconActionStop,
theme.IconActionFastForward,
theme.IconActionRewind,
theme.IconActionToBeginning,
theme.IconActionToEnd,
theme.IconActionRecord,
theme.IconActionVolumeUp,
theme.IconActionVolumeDown,
theme.IconActionMute,
theme.IconActionUndo,
theme.IconActionRedo,
theme.IconActionCut,
theme.IconActionCopy,
theme.IconActionPaste,
theme.IconActionFind,
theme.IconActionReplace,
theme.IconActionSelectAll,
theme.IconActionSelectNone,
theme.IconActionIncrement,
theme.IconActionDecrement,
theme.IconActionClose,
theme.IconActionQuit,
theme.IconActionIconify,
theme.IconActionShade,
theme.IconActionMaximize,
theme.IconActionFullScreen,
theme.IconActionRestore,
theme.IconActionExpand,
theme.IconActionContract,
theme.IconActionBack,
theme.IconActionForward,
theme.IconActionUp,
theme.IconActionDown,
theme.IconActionReload,
theme.IconActionZoomIn,
theme.IconActionZoomOut,
theme.IconActionZoomReset,
theme.IconActionMove,
theme.IconActionResize,
theme.IconActionGoTo,
theme.IconActionTransform,
theme.IconActionTranslate,
theme.IconActionRotate,
theme.IconActionScale,
theme.IconActionWarp,
theme.IconActionCornerPin,
theme.IconActionSelectRectangle,
theme.IconActionSelectEllipse,
theme.IconActionSelectLasso,
theme.IconActionSelectGeometric,
theme.IconActionSelectAuto,
theme.IconActionCrop,
theme.IconActionFill,
theme.IconActionGradient,
theme.IconActionPencil,
theme.IconActionBrush,
theme.IconActionEraser,
theme.IconActionText,
theme.IconActionEyedropper,
theme.IconStatusInformation,
theme.IconStatusQuestion,
theme.IconStatusWarning,
theme.IconStatusError,
theme.IconStatusCancel,
theme.IconStatusOkay,
theme.IconStatusCellSignal0,
theme.IconStatusCellSignal1,
theme.IconStatusCellSignal2,
theme.IconStatusCellSignal3,
theme.IconStatusWirelessSignal0,
theme.IconStatusWirelessSignal1,
theme.IconStatusWirelessSignal2,
theme.IconStatusWirelessSignal3,
theme.IconStatusBattery0,
theme.IconStatusBattery1,
theme.IconStatusBattery2,
theme.IconStatusBattery3,
theme.IconStatusBrightness0,
theme.IconStatusBrightness1,
theme.IconStatusBrightness2,
theme.IconStatusBrightness3,
theme.IconStatusVolume0,
theme.IconStatusVolume1,
theme.IconStatusVolume2,
theme.IconStatusVolume3,
}
for _, icon := range icons {
this.grid.Add(objects.NewIcon(icon, size))
}
}
func main () {
nasin.RunApplication(&Application { })
}

38
examples/inputs/main.go Normal file
View File

@@ -0,0 +1,38 @@
// Example inputs demonstrates the use of various user input methods.
package main
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/nasin"
import "git.tebibyte.media/tomo/objects"
// import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/objects/layouts"
type Application struct { }
func (this *Application) Describe () nasin.ApplicationDescription {
return nasin.ApplicationDescription {
Name: "Tomo Input Example",
ID: "xyz.holanet.TomoInputExample",
}
}
func (this *Application) Init () error {
window, err := nasin.NewApplicationWindow(this, image.Rect(0, 0, 128, 128))
if err != nil { return err }
window.SetRoot(objects.NewOuterContainer(layouts.Column { },
objects.NewTextInput(""),
objects.NewHorizontalSlider(0.5),
objects.NewLabelCheckbox(false, "checkbox"),
objects.NewNumberInput(5),
))
window.OnClose(tomo.Stop)
window.Show()
return nil
}
func main () {
nasin.RunApplication(&Application { })
}

9
go.mod
View File

@@ -3,17 +3,18 @@ module git.tebibyte.media/tomo/nasin
go 1.20 go 1.20
require ( require (
git.tebibyte.media/tomo/tomo v0.30.0 git.tebibyte.media/tomo/objects v0.11.0
git.tebibyte.media/tomo/x v0.6.0 git.tebibyte.media/tomo/tomo v0.31.0
git.tebibyte.media/tomo/x v0.7.3
git.tebibyte.media/tomo/xdg v0.1.0 git.tebibyte.media/tomo/xdg v0.1.0
golang.org/x/image v0.11.0
) )
require ( require (
git.tebibyte.media/tomo/typeset v0.7.0 // indirect git.tebibyte.media/tomo/typeset v0.7.1 // 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
github.com/jezek/xgb v1.1.0 // indirect github.com/jezek/xgb v1.1.0 // indirect
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 // indirect github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 // indirect
golang.org/x/image v0.11.0 // indirect
) )

14
go.sum
View File

@@ -1,10 +1,12 @@
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/tomo v0.30.0 h1:JoTklJ7yFVrzre4AwuKBMwzho9GomC9ySw354wDB4f4= git.tebibyte.media/tomo/objects v0.11.0 h1:ESv6/9UtLOX2lJKopQhPNuxd6U6jWOrU1XnHaH+dgv8=
git.tebibyte.media/tomo/tomo v0.30.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps= git.tebibyte.media/tomo/objects v0.11.0/go.mod h1:34UDkPEHxBgIsAYWyqqE4u1KvVtwzwdpCO6AdkgsrKo=
git.tebibyte.media/tomo/typeset v0.7.0 h1:JFpEuGmN6R2XSCvkINYxpH0AyYUqqs+dZYr6OSd91y0= git.tebibyte.media/tomo/tomo v0.31.0 h1:LHPpj3AWycochnC8F441aaRNS6Tq6w6WnBrp/LGjyhM=
git.tebibyte.media/tomo/typeset v0.7.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= git.tebibyte.media/tomo/tomo v0.31.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/x v0.6.0 h1:80BRiSwhZCqu6IPKZoQj7t1puKXXJpMB9eWVHQliTHM= git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
git.tebibyte.media/tomo/x v0.6.0/go.mod h1:6INfDGlcPyoYVMem64ScD5AZb43PkXDGkfgaNa5GCqQ= git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/x v0.7.3 h1:9sjjYC7UMsrce6GsWdP/Jb2U6UEE3st4WiUvz9RhycI=
git.tebibyte.media/tomo/x v0.7.3/go.mod h1:8BLhXlFSTmn/y2FM+yrc6yLmMzqMhFQYYrN9SXMbmZM=
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,2 @@
// Package registrar provides platform-dependent components at compile time.
package registrar

View File

@@ -0,0 +1,13 @@
//go:build unix && (!darwin)
package registrar
import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/nasin/internal/theme/default"
func Init () error {
theme.SetTheme(defaultTheme.Theme())
tomo.Register(1, x.NewBackend)
return nil
}

View File

@@ -0,0 +1,41 @@
package theme
import "image"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo"
// Attr modifies one thing about an Objects's style.
type Attr interface { attr () int }
// AttrColor sets the background color of an Objects.
type AttrColor struct { color.Color }
// AttrTexture sets the texture of an Objects to a named texture.
type AttrTexture string
// AttrBorder sets the border of an Objects.
type AttrBorder []tomo.Border
// AttrMinimumSize sets the minimum size of an Objects.
type AttrMinimumSize image.Point
// AttrPadding sets the inner padding of an Objects.
type AttrPadding tomo.Inset
// AttrGap sets the gap between child Objects, if the Object is a ContainerBox.
type AttrGap image.Point
// AttrTextColor sets the text color, if the Object is a TextBox.
type AttrTextColor struct { color.Color }
// AttrDotColor sets the text selection color, if the Object is a TextBox.
type AttrDotColor struct { color.Color }
// AttrFace sets the font face, if the Object is a TextBox.
type AttrFace struct { font.Face }
// AttrAlign sets the alignment, if the Object is a ContentBox.
type AttrAlign struct { X, Y tomo.Align }
func (AttrColor) attr () int { return 0 }
func (AttrTexture) attr () int { return 1 }
func (AttrBorder) attr () int { return 2 }
func (AttrMinimumSize) attr () int { return 3 }
func (AttrPadding) attr () int { return 4 }
func (AttrGap) attr () int { return 5 }
func (AttrTextColor) attr () int { return 6 }
func (AttrDotColor) attr () int { return 7 }
func (AttrFace) attr () int { return 8 }
func (AttrAlign) attr () int { return 9 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

View File

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

View File

@@ -0,0 +1,228 @@
package defaultTheme
import "bytes"
import "image"
import _ "embed"
import _ "image/png"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/canvas"
//go:embed assets/icons-small.png
var atlasSmallBytes []byte
//go:embed assets/icons-large.png
var atlasLargeBytes []byte
func generateSource (data []byte, width int) map[theme.Icon] canvas.Texture {
atlasImage, _, err := image.Decode(bytes.NewReader(data))
if err != nil { panic(err) }
atlasTexture := tomo.NewTexture(atlasImage)
source := make(map[theme.Icon] canvas.Texture)
x := 0
y := 0
row := func () {
x = 0
y ++
}
col := func (id theme.Icon) {
source[id] = atlasTexture.Clip(image.Rect (
x * width,
y * width,
(x + 1) * width,
(y + 1) * width))
x++
}
// objects
col(theme.IconUnknown)
col(theme.IconFile)
col(theme.IconDirectory)
col(theme.IconStorage)
col(theme.IconApplication)
col(theme.IconNetwork)
col(theme.IconDevice)
col(theme.IconPeripheral)
col(theme.IconPort)
// actions: files
row()
col(theme.IconActionOpen)
col(theme.IconActionOpenIn)
col(theme.IconActionSave)
col(theme.IconActionSaveAs)
col(theme.IconActionPrint)
col(theme.IconActionNew)
col(theme.IconActionNewDirectory)
col(theme.IconActionDelete)
col(theme.IconActionRename)
col(theme.IconActionGetInformation)
col(theme.IconActionChangePermissions)
col(theme.IconActionRevert)
// actions: list management
row()
col(theme.IconActionAdd)
col(theme.IconActionRemove)
col(theme.IconActionAddBookmark)
col(theme.IconActionRemoveBookmark)
col(theme.IconActionAddFavorite)
col(theme.IconActionRemoveFavorite)
// actions: media
row()
col(theme.IconActionPlay)
col(theme.IconActionPause)
col(theme.IconActionStop)
col(theme.IconActionFastForward)
col(theme.IconActionRewind)
col(theme.IconActionToBeginning)
col(theme.IconActionToEnd)
col(theme.IconActionRecord)
col(theme.IconActionVolumeUp)
col(theme.IconActionVolumeDown)
col(theme.IconActionMute)
// actions: editing
row()
col(theme.IconActionUndo)
col(theme.IconActionRedo)
col(theme.IconActionCut)
col(theme.IconActionCopy)
col(theme.IconActionPaste)
col(theme.IconActionFind)
col(theme.IconActionReplace)
col(theme.IconActionSelectAll)
col(theme.IconActionSelectNone)
col(theme.IconActionIncrement)
col(theme.IconActionDecrement)
// actions: window management
row()
col(theme.IconActionClose)
col(theme.IconActionQuit)
col(theme.IconActionIconify)
col(theme.IconActionShade)
col(theme.IconActionMaximize)
col(theme.IconActionFullScreen)
col(theme.IconActionRestore)
// actions: view
row()
col(theme.IconActionExpand)
col(theme.IconActionContract)
col(theme.IconActionBack)
col(theme.IconActionForward)
col(theme.IconActionUp)
col(theme.IconActionDown)
col(theme.IconActionReload)
col(theme.IconActionZoomIn)
col(theme.IconActionZoomOut)
col(theme.IconActionZoomReset)
col(theme.IconActionMove)
col(theme.IconActionResize)
col(theme.IconActionGoTo)
// actions: tools
row()
col(theme.IconActionTransform)
col(theme.IconActionTranslate)
col(theme.IconActionRotate)
col(theme.IconActionScale)
col(theme.IconActionWarp)
col(theme.IconActionCornerPin)
col(theme.IconActionSelectRectangle)
col(theme.IconActionSelectEllipse)
col(theme.IconActionSelectLasso)
col(theme.IconActionSelectGeometric)
col(theme.IconActionSelectAuto)
col(theme.IconActionCrop)
col(theme.IconActionFill)
row()
col(theme.IconActionGradient)
col(theme.IconActionPencil)
col(theme.IconActionBrush)
col(theme.IconActionEraser)
col(theme.IconActionText)
col(theme.IconActionEyedropper)
// status: dialog
row()
col(theme.IconStatusInformation)
col(theme.IconStatusQuestion)
col(theme.IconStatusWarning)
col(theme.IconStatusError)
col(theme.IconStatusCancel)
col(theme.IconStatusOkay)
// status: network
row()
col(theme.IconStatusCellSignal0)
col(theme.IconStatusCellSignal1)
col(theme.IconStatusCellSignal2)
col(theme.IconStatusCellSignal3)
col(theme.IconStatusWirelessSignal0)
col(theme.IconStatusWirelessSignal1)
col(theme.IconStatusWirelessSignal2)
col(theme.IconStatusWirelessSignal3)
// status: power
row()
col(theme.IconStatusBattery0)
col(theme.IconStatusBattery1)
col(theme.IconStatusBattery2)
col(theme.IconStatusBattery3)
col(theme.IconStatusBrightness0)
col(theme.IconStatusBrightness1)
col(theme.IconStatusBrightness2)
col(theme.IconStatusBrightness3)
// status: media
row()
col(theme.IconStatusVolume0)
col(theme.IconStatusVolume1)
col(theme.IconStatusVolume2)
col(theme.IconStatusVolume3)
return source
}
type iconTheme struct {
texturesSmall map[theme.Icon] canvas.Texture
texturesLarge map[theme.Icon] canvas.Texture
}
func (this *iconTheme) ensure () {
if this.texturesSmall != nil { return }
this.texturesSmall = generateSource(atlasSmallBytes, 16)
this.texturesLarge = generateSource(atlasLargeBytes, 32)
}
func (this *iconTheme) selectSource (size theme.IconSize) map[theme.Icon] canvas.Texture {
if size == theme.IconSizeSmall {
return this.texturesSmall
} else {
return this.texturesLarge
}
}
func (this *iconTheme) Icon (icon theme.Icon, size theme.IconSize) canvas.Texture {
this.ensure()
source := this.selectSource(size)
if texture, ok := source[icon]; ok {
return texture
}
return source[theme.IconUnknown]
}
func (this *iconTheme) MimeIcon (mime data.Mime, size theme.IconSize) canvas.Texture {
this.ensure()
source := this.selectSource(size)
if mime == data.M("inode", "directory") {
return source[theme.IconDirectory]
} else {
return source[theme.IconFile]
}
}

View File

@@ -0,0 +1,251 @@
package defaultTheme
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/font/basicfont"
import "git.tebibyte.media/tomo/tomo/theme"
import dataTheme "git.tebibyte.media/tomo/nasin/internal/theme"
var colorFocus = color.RGBA { R: 61, G: 128, B: 143, A: 255 }
var colorInput = color.RGBA { R: 208, G: 203, B: 150, A: 255 }
var colorCarved = color.RGBA { R: 151, G: 160, B: 150, A: 255 }
var colorGutter = color.RGBA { R: 116, G: 132, B: 126, A: 255 }
var colorShadow = color.RGBA { R: 57, G: 59, B: 57, A: 255 }
var colorInputShadow = color.RGBA { R: 143, G: 146, B: 91, A: 255 }
var colorHighlight = color.RGBA { R: 207, G: 215, B: 210, A: 255 }
var colorBackground = color.RGBA { R: 169, G: 171, B: 168, A: 255 }
var colorCarvedPressed = color.RGBA { R: 129, G: 142, B: 137, A: 255 }
var colorForeground = color.Black
var colorOutline = color.Black
var outline = tomo.Border {
Width: tomo.I(1),
Color: [4]color.Color {
colorOutline,
colorOutline,
colorOutline,
colorOutline,
},
}
var borderColorEngraved = [4]color.Color { colorShadow, colorHighlight, colorHighlight, colorShadow }
var borderColorLifted = [4]color.Color { colorHighlight, colorShadow, colorShadow, colorHighlight }
var borderColorInput = [4]color.Color { colorInputShadow, colorInput, colorInput, colorInputShadow }
var borderColorFocused = [4]color.Color { colorFocus, colorFocus, colorFocus, colorFocus }
var rules = []dataTheme.Rule {
// *.*[*]
dataTheme.Rule {
Default: dataTheme.AS (
dataTheme.AttrFace { Face: basicfont.Face7x13 },
dataTheme.AttrTextColor { Color: theme.ColorForeground },
dataTheme.AttrDotColor { Color: theme.ColorAccent },
dataTheme.AttrGap { X: 8, Y: 8 },
),
},
// *.Button[*]
dataTheme.Rule {
Role: theme.R("", "Button", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
dataTheme.AttrPadding(tomo.I(4, 8)),
dataTheme.AttrColor { Color: theme.ColorRaised },
),
Pressed: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
dataTheme.AttrPadding(tomo.I(5, 8, 4, 9)),
dataTheme.AttrColor { Color: colorCarvedPressed },
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
dataTheme.AttrPadding(tomo.I(4, 8)),
),
},
// *.TextInput[*]
dataTheme.Rule {
Role: theme.R("", "TextInput", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorInput,
},
},
dataTheme.AttrColor { Color: colorInput },
dataTheme.AttrPadding(tomo.I(5, 4, 4, 5)),
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
),
},
// *.NumberInput[*]
dataTheme.Rule {
Role: theme.R("", "NumberInput", ""),
Default: dataTheme.AS (
dataTheme.AttrGap { },
),
},
// *.Container[sunken]
dataTheme.Rule {
Role: theme.R("", "Container", "sunken"),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
dataTheme.AttrColor { Color: theme.ColorSunken },
dataTheme.AttrPadding(tomo.I(8)),
),
},
// *.Container[outer]
dataTheme.Rule {
Role: theme.R("", "Container", "outer"),
Default: dataTheme.AS (
dataTheme.AttrColor { Color: theme.ColorBackground },
dataTheme.AttrPadding(tomo.I(8)),
),
},
// *.Heading[*]
dataTheme.Rule {
Role: theme.R("", "Heading", ""),
Default: dataTheme.AS (
dataTheme.AttrAlign { X: tomo.AlignMiddle, Y: tomo.AlignMiddle },
),
},
// *.Separator[*]
dataTheme.Rule {
Role: theme.R("", "Separator", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
tomo.Border {
Width: tomo.I(1),
Color: borderColorEngraved,
},
},
),
},
// *.Slider[*]
dataTheme.Rule {
Role: theme.R("", "Slider", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
dataTheme.AttrColor { Color: colorGutter },
dataTheme.AttrPadding(tomo.I(0, 1, 1, 0)),
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
dataTheme.AttrPadding(tomo.I(0)),
),
},
// *.Slider[horizontal]
dataTheme.Rule {
Role: theme.R("", "Slider", "horizontal"),
Default: dataTheme.AS(dataTheme.AttrMinimumSize { X: 48 }),
},
// *.Slider[vertical]
dataTheme.Rule {
Role: theme.R("", "Slider", "vertical"),
Default: dataTheme.AS(dataTheme.AttrMinimumSize { Y: 48 }),
},
// *.SliderHandle[*]
dataTheme.Rule {
Role: theme.R("", "SliderHandle", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
dataTheme.AttrColor { Color: theme.ColorRaised },
dataTheme.AttrMinimumSize { X: 12, Y: 12, },
),
},
// *.Checkbox[*]
dataTheme.Rule {
Role: theme.R("", "Checkbox", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
dataTheme.AttrColor { Color: theme.ColorSunken },
dataTheme.AttrPadding(tomo.I(0, 1, 1, 0)),
dataTheme.AttrMinimumSize { X: 19, Y: 19 },
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
dataTheme.AttrPadding(tomo.I(0)),
),
},
// *.LabelCheckbox[*]
dataTheme.Rule {
Role: theme.R("", "LabelCheckbox", ""),
Default: dataTheme.AS (
dataTheme.AttrGap { X: 8, Y: 8 },
),
},
}

24
internal/theme/missing.go Normal file
View File

@@ -0,0 +1,24 @@
package theme
import "image"
import "image/color"
type missingTexture int
func (texture missingTexture) ColorModel () color.Model {
return color.RGBAModel
}
func (texture missingTexture) Bounds () image.Rectangle {
return image.Rect(0, 0, int(texture), int(texture))
}
func (texture missingTexture) At (x, y int) color.Color {
x /= 8
y /= 8
if (x + y) % 2 == 0 {
return color.RGBA { R: 0xFF, B: 0xFF, A: 0xFF }
} else {
return color.RGBA { A: 0xFF }
}
}

268
internal/theme/theme.go Normal file
View File

@@ -0,0 +1,268 @@
// Package theme provides a data-driven theme implementation.
package theme
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/canvas"
// this is CSS's bastard child
// Theme allows the use of data to define a visual style.
type Theme struct {
// Textures maps texture names to image textures.
Textures map[string] image.Image
textures map[string] canvas.TextureCloser // private texture cache
missing canvas.TextureCloser // cache for "missing" texture
// Rules determines which styles get applied to which Objects.
Rules []Rule
// Colors maps theme.Color values to color.RGBA values.
Colors map[theme.Color] color.Color
// This type does not handle icons, and as such, a special icon theme
// must be separately specified.
IconTheme
}
// IconTheme implements the part of theme.Theme that handles icons.
type IconTheme interface {
// Icon returns a texture of the corresponding icon ID.
Icon (theme.Icon, theme.IconSize) canvas.Texture
// MimeIcon returns an icon corresponding to a MIME type.
MimeIcon (data.Mime, theme.IconSize) canvas.Texture
}
// Rule describes under what circumstances should certain style attributes be
// active.
type Rule struct {
Role theme.Role
Default AttrSet
Hovered AttrSet
Pressed AttrSet
Focused AttrSet
}
// AttrSet is a set of attributes wherein only one/zero of each attribute type
// can exist. I deserve to be imprisoned for the way I made this work (look in
// attribute.go). Its zero value can be used safely, and you can copy it if you
// want, but it will point to the same set of attributes.
type AttrSet struct {
set map[int] Attr
}
// AS builds an AttrSet out of a vararg list of Attr values.
func AS (attrs ...Attr) AttrSet {
set := AttrSet { }
set.Add(attrs...)
return set
}
// Add adds attributes to the set.
func (this *AttrSet) Add (attrs ...Attr) {
this.ensure()
for _, attr := range attrs {
this.set[attr.attr()] = attr
}
}
// MergeUnder takes attributes from another set and adds them if they don't
// already exist in this one.
func (this *AttrSet) MergeUnder (other AttrSet) {
this.ensure()
if other.set == nil { return }
for _, attr := range other.set {
if _, exists := this.set[attr.attr()]; !exists {
this.Add(attr)
}
}
}
// MergeOver takes attributes from another set and adds them, overriding this
// one.
func (this *AttrSet) MergeOver (other AttrSet) {
this.ensure()
if other.set == nil { return }
for _, attr := range other.set {
this.Add(attr)
}
}
func (this *AttrSet) ensure () {
if this.set == nil { this.set = make(map[int] Attr) }
}
func (this *Theme) execute (object tomo.Object, set AttrSet) {
box := object.GetBox()
for _, attr := range set.set {
switch attr := attr.(type) {
case AttrColor:
box.SetColor(attr.Color)
case AttrTexture:
box.SetTexture(this.texture(string(attr)))
case AttrBorder:
box.SetBorder([]tomo.Border(attr)...)
case AttrMinimumSize:
box.SetMinimumSize(image.Point(attr))
case AttrPadding:
box.SetPadding(tomo.Inset(attr))
case AttrGap:
if box, ok := box.(tomo.ContainerBox); ok {
box.SetGap(image.Point(attr))
}
case AttrTextColor:
if box, ok := box.(tomo.TextBox); ok {
box.SetTextColor(attr.Color)
}
case AttrDotColor:
if box, ok := box.(tomo.TextBox); ok {
box.SetDotColor(attr.Color)
}
case AttrFace:
if box, ok := box.(tomo.TextBox); ok {
box.SetFace(attr)
}
case AttrAlign:
if box, ok := box.(tomo.ContentBox); ok {
box.SetAlign(attr.X, attr.Y)
}
default:
panic("bug: nasin/internal/theme.Theme: unexpected attribute")
}
}
}
func (this *Theme) texture (name string) canvas.Texture {
this.ensureTextureCache()
if texture, ok := this.textures[name]; ok {
return texture
}
if this.Textures == nil {
if source, ok := this.Textures[name]; ok {
texture := tomo.NewTexture(source)
this.textures[name] = texture
return texture
}
}
return this.missingTexture()
}
func (this *Theme) missingTexture () canvas.Texture {
if this.missing == nil {
this.missing = tomo.NewTexture(missingTexture(16))
}
return this.missing
}
func (this *Theme) ensureTextureCache () {
if this.textures == nil { this.textures = make(map[string] canvas.TextureCloser) }
}
// setsFor builds flattened attr sets for a specific role based on the rules list
func (this *Theme) setsFor (role theme.Role) (defaul, hovered, pressed, focused AttrSet) {
for _, current := range this.Rules {
// check for a match
packageMatch := current.Role.Package == role.Package || current.Role.Package == ""
objectMatch := current.Role.Object == role.Object || current.Role.Object == ""
variantMatch := current.Role.Variant == role.Variant || current.Role.Variant == ""
if packageMatch && objectMatch && variantMatch {
// if found, merge and override
defaul.MergeOver(current.Default)
hovered.MergeOver(current.Hovered)
pressed.MergeOver(current.Pressed)
focused.MergeOver(current.Focused)
}
}
// hovered and pressed are mutually exclusive states, so we compress
// them with the default state.
hovered.MergeUnder(defaul)
pressed.MergeUnder(defaul)
return defaul, hovered, pressed, focused
}
func (this *Theme) Apply (object tomo.Object, role theme.Role) event.Cookie {
pressed := false
hovered := false
box := object.GetBox()
defaultSet, hoveredSet, pressedSet, focusedSet := this.setsFor(role)
updateStyle := func () {
if pressed {
this.execute(object, pressedSet)
} else if hovered {
this.execute(object, hoveredSet)
} else {
this.execute(object, defaultSet)
}
if box.Focused() && !pressed {
this.execute(object, focusedSet)
}
}
updateStyle()
return event.MultiCookie (
box.OnFocusEnter(updateStyle),
box.OnFocusLeave(updateStyle),
box.OnMouseDown(func (button input.Button) {
if button != input.ButtonLeft { return }
pressed = true
updateStyle()
}),
box.OnMouseUp(func (button input.Button) {
if button != input.ButtonLeft { return }
pressed = false
updateStyle()
}),
box.OnMouseEnter(func () {
hovered = true
updateStyle()
}),
box.OnMouseLeave(func () {
hovered = false
updateStyle()
}))
}
func (this *Theme) RGBA (c theme.Color) (r, g, b, a uint32) {
if this.Colors == nil { return 0xFFFF, 0, 0xFFFF, 0xFFFF }
color, ok := this.Colors[c]
if !ok { return 0xFFFF, 0, 0xFFFF, 0xFFFF }
return color.RGBA()
}
func (this *Theme) Icon (icon theme.Icon, size theme.IconSize) canvas.Texture {
if this.IconTheme == nil {
return this.missingTexture()
} else {
return this.IconTheme.Icon(icon, size)
}
}
func (this *Theme) MimeIcon (mime data.Mime, size theme.IconSize) canvas.Texture {
if this.IconTheme == nil {
return this.missingTexture()
} else {
return this.IconTheme.MimeIcon(mime, size)
}
}
// Close closes all cached textures this theme has open. Do not call this while
// the theme is in use.
func (this *Theme) Close () error {
this.missing.Close()
this.missing = nil
for _, texture := range this.textures {
texture.Close()
}
this.textures = nil
return nil
}