Merge pull request 'restructure-config' (#8) from restructure-config into main
Reviewed-on: sashakoshka/tomo#8
This commit is contained in:
commit
5c7e243566
@ -165,6 +165,7 @@ func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) {
|
|||||||
// have its maximum width set to the given width. This does not alter the
|
// have its maximum width set to the given width. This does not alter the
|
||||||
// drawer's state.
|
// drawer's state.
|
||||||
func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) {
|
func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) {
|
||||||
|
if drawer.face == nil { return }
|
||||||
if !drawer.layoutClean { drawer.recalculate() }
|
if !drawer.layoutClean { drawer.recalculate() }
|
||||||
metrics := drawer.face.Metrics()
|
metrics := drawer.face.Metrics()
|
||||||
dot := fixed.Point26_6 { 0, metrics.Height }
|
dot := fixed.Point26_6 { 0, metrics.Height }
|
||||||
|
@ -2,6 +2,8 @@ package tomo
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||||
|
|
||||||
// Backend represents a connection to a display server, or something similar.
|
// Backend represents a connection to a display server, or something similar.
|
||||||
@ -28,6 +30,12 @@ type Backend interface {
|
|||||||
|
|
||||||
// Paste returns the data currently in the clipboard.
|
// Paste returns the data currently in the clipboard.
|
||||||
Paste (accept []data.Mime) (data.Data)
|
Paste (accept []data.Mime) (data.Data)
|
||||||
|
|
||||||
|
// SetTheme sets the theme of all open windows.
|
||||||
|
SetTheme (theme.Theme)
|
||||||
|
|
||||||
|
// SetConfig sets the configuration of all open windows.
|
||||||
|
SetConfig (config.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackendFactory represents a function capable of constructing a backend
|
// BackendFactory represents a function capable of constructing a backend
|
||||||
|
@ -8,6 +8,8 @@ import "github.com/jezek/xgbutil/xevent"
|
|||||||
import "github.com/jezek/xgbutil/xwindow"
|
import "github.com/jezek/xgbutil/xwindow"
|
||||||
import "github.com/jezek/xgbutil/xgraphics"
|
import "github.com/jezek/xgbutil/xgraphics"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||||
|
|
||||||
@ -20,6 +22,9 @@ type Window struct {
|
|||||||
onClose func ()
|
onClose func ()
|
||||||
skipChildDrawCallback bool
|
skipChildDrawCallback bool
|
||||||
|
|
||||||
|
theme theme.Theme
|
||||||
|
config config.Config
|
||||||
|
|
||||||
metrics struct {
|
metrics struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
@ -70,6 +75,9 @@ func (backend *Backend) NewWindow (
|
|||||||
xevent.MotionNotifyFun(window.handleMotionNotify).
|
xevent.MotionNotifyFun(window.handleMotionNotify).
|
||||||
Connect(backend.connection, window.xWindow.Id)
|
Connect(backend.connection, window.xWindow.Id)
|
||||||
|
|
||||||
|
window.SetTheme(backend.theme)
|
||||||
|
window.SetConfig(backend.config)
|
||||||
|
|
||||||
window.metrics.width = width
|
window.metrics.width = width
|
||||||
window.metrics.height = height
|
window.metrics.height = height
|
||||||
window.childMinimumSizeChangeCallback(8, 8)
|
window.childMinimumSizeChangeCallback(8, 8)
|
||||||
@ -100,6 +108,12 @@ func (window *Window) Adopt (child elements.Element) {
|
|||||||
|
|
||||||
// adopt new child
|
// adopt new child
|
||||||
window.child = child
|
window.child = child
|
||||||
|
if newChild, ok := child.(elements.Themeable); ok {
|
||||||
|
newChild.SetTheme(window.theme)
|
||||||
|
}
|
||||||
|
if newChild, ok := child.(elements.Configurable); ok {
|
||||||
|
newChild.SetConfig(window.config)
|
||||||
|
}
|
||||||
if newChild, ok := child.(elements.Flexible); ok {
|
if newChild, ok := child.(elements.Flexible); ok {
|
||||||
newChild.OnFlexibleHeightChange(window.resizeChildToFit)
|
newChild.OnFlexibleHeightChange(window.resizeChildToFit)
|
||||||
}
|
}
|
||||||
@ -196,6 +210,20 @@ func (window *Window) OnClose (callback func ()) {
|
|||||||
window.onClose = callback
|
window.onClose = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (window *Window) SetTheme (theme theme.Theme) {
|
||||||
|
window.theme = theme
|
||||||
|
if child, ok := window.child.(elements.Themeable); ok {
|
||||||
|
child.SetTheme(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (window *Window) SetConfig (config config.Config) {
|
||||||
|
window.config = config
|
||||||
|
if child, ok := window.child.(elements.Configurable); ok {
|
||||||
|
child.SetConfig(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (window *Window) reallocateCanvas () {
|
func (window *Window) reallocateCanvas () {
|
||||||
window.canvas.Reallocate(window.metrics.width, window.metrics.height)
|
window.canvas.Reallocate(window.metrics.width, window.metrics.height)
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ package x
|
|||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
|
|
||||||
import "github.com/jezek/xgbutil"
|
import "github.com/jezek/xgbutil"
|
||||||
import "github.com/jezek/xgb/xproto"
|
import "github.com/jezek/xgb/xproto"
|
||||||
@ -25,6 +27,9 @@ type Backend struct {
|
|||||||
hyper uint16
|
hyper uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
theme theme.Theme
|
||||||
|
config config.Config
|
||||||
|
|
||||||
windows map[xproto.Window] *Window
|
windows map[xproto.Window] *Window
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +38,8 @@ func NewBackend () (output tomo.Backend, err error) {
|
|||||||
backend := &Backend {
|
backend := &Backend {
|
||||||
windows: map[xproto.Window] *Window { },
|
windows: map[xproto.Window] *Window { },
|
||||||
doChannel: make(chan func (), 0),
|
doChannel: make(chan func (), 0),
|
||||||
|
theme: theme.Default { },
|
||||||
|
config: config.Default { },
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect to X
|
// connect to X
|
||||||
@ -66,7 +73,11 @@ func (backend *Backend) Run () (err error) {
|
|||||||
// Stop gracefully closes the connection and stops the event loop.
|
// Stop gracefully closes the connection and stops the event loop.
|
||||||
func (backend *Backend) Stop () {
|
func (backend *Backend) Stop () {
|
||||||
backend.assert()
|
backend.assert()
|
||||||
|
toClose := []*Window { }
|
||||||
for _, window := range backend.windows {
|
for _, window := range backend.windows {
|
||||||
|
toClose = append(toClose, window)
|
||||||
|
}
|
||||||
|
for _, window := range toClose {
|
||||||
window.Close()
|
window.Close()
|
||||||
}
|
}
|
||||||
xevent.Quit(backend.connection)
|
xevent.Quit(backend.connection)
|
||||||
@ -95,6 +106,25 @@ func (backend *Backend) Paste (accept []data.Mime) (data data.Data) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// SetTheme sets the theme of all open windows.
|
||||||
|
func (backend *Backend) SetTheme (theme theme.Theme) {
|
||||||
|
backend.assert()
|
||||||
|
backend.theme = theme
|
||||||
|
for _, window := range backend.windows {
|
||||||
|
window.SetTheme(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the configuration of all open windows.
|
||||||
|
func (backend *Backend) SetConfig (config config.Config) {
|
||||||
|
backend.assert()
|
||||||
|
backend.config = config
|
||||||
|
for _, window := range backend.windows {
|
||||||
|
window.SetConfig(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (backend *Backend) assert () {
|
func (backend *Backend) assert () {
|
||||||
if backend == nil { panic("nil backend") }
|
if backend == nil { panic("nil backend") }
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,93 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
// Padding returns the amount of internal padding elements should have. An
|
// Config can return global configuration parameters.
|
||||||
// element's inner content (such as text) should be inset by this amount,
|
type Config interface {
|
||||||
// in addition to the inset returned by the pattern of its background. When
|
// Padding returns the amount of internal padding elements should have.
|
||||||
// using the aforementioned inset values to calculate the element's minimum size
|
// An element's inner content (such as text) should be inset by this
|
||||||
// or the position and alignment of its content, all parameters in the
|
// amount, in addition to the inset returned by the pattern of its
|
||||||
// PatternState should be unset except for Case.
|
// background.
|
||||||
func Padding () int {
|
Padding () int
|
||||||
|
|
||||||
|
// Margin returns how much space should be put in between elements.
|
||||||
|
Margin () int
|
||||||
|
|
||||||
|
// HandleWidth returns how large grab handles should typically be. This
|
||||||
|
// is important for accessibility reasons.
|
||||||
|
HandleWidth () int
|
||||||
|
|
||||||
|
// ScrollVelocity returns how many pixels should be scrolled every time
|
||||||
|
// a scroll button is pressed.
|
||||||
|
ScrollVelocity () int
|
||||||
|
|
||||||
|
// ThemePath returns the directory path to the theme.
|
||||||
|
ThemePath () string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default specifies default configuration values.
|
||||||
|
type Default struct { }
|
||||||
|
|
||||||
|
// Padding returns the default padding value.
|
||||||
|
func (Default) Padding () int {
|
||||||
return 7
|
return 7
|
||||||
}
|
}
|
||||||
|
|
||||||
// Margin returns how much space should be put in between elements.
|
// Margin returns the default margin value.
|
||||||
func Margin () int {
|
func (Default) Margin () int {
|
||||||
return 8
|
return 8
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleWidth returns how large grab handles should typically be. This is
|
// HandleWidth returns the default handle width value.
|
||||||
// important for accessibility reasons.
|
func (Default) HandleWidth () int {
|
||||||
func HandleWidth () int {
|
|
||||||
return 16
|
return 16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScrollVelocity returns the default scroll velocity value.
|
||||||
|
func (Default) ScrollVelocity () int {
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThemePath returns the default theme path.
|
||||||
|
func (Default) ThemePath () (string) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapped wraps a configuration and uses Default if it is nil.
|
||||||
|
type Wrapped struct {
|
||||||
|
Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding returns the amount of internal padding elements should have.
|
||||||
|
// An element's inner content (such as text) should be inset by this
|
||||||
|
// amount, in addition to the inset returned by the pattern of its
|
||||||
|
// background.
|
||||||
|
func (wrapped Wrapped) Padding () int {
|
||||||
|
return wrapped.ensure().Padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Margin returns how much space should be put in between elements.
|
||||||
|
func (wrapped Wrapped) Margin () int {
|
||||||
|
return wrapped.ensure().Margin()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleWidth returns how large grab handles should typically be. This
|
||||||
|
// is important for accessibility reasons.
|
||||||
|
func (wrapped Wrapped) HandleWidth () int {
|
||||||
|
return wrapped.ensure().HandleWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollVelocity returns how many pixels should be scrolled every time
|
||||||
|
// a scroll button is pressed.
|
||||||
|
func (wrapped Wrapped) ScrollVelocity () int {
|
||||||
|
return wrapped.ensure().ScrollVelocity()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThemePath returns the directory path to the theme.
|
||||||
|
func (wrapped Wrapped) ThemePath () string {
|
||||||
|
return wrapped.ensure().ThemePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wrapped Wrapped) ensure () (real Config) {
|
||||||
|
real = wrapped.Config
|
||||||
|
if real == nil { real = Default { } }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
9
config/parse.go
Normal file
9
config/parse.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// Parse parses one or more configuration files and returns them as a Config.
|
||||||
|
func Parse (sources ...io.Reader) (config Config) {
|
||||||
|
// TODO
|
||||||
|
return Default { }
|
||||||
|
}
|
82
dirs/dirs.go
Normal file
82
dirs/dirs.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package dirs
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
import "strings"
|
||||||
|
import "path/filepath"
|
||||||
|
|
||||||
|
var homeDirectory string
|
||||||
|
var configHome string
|
||||||
|
var configDirs []string
|
||||||
|
var dataHome string
|
||||||
|
var dataDirs []string
|
||||||
|
var cacheHome string
|
||||||
|
|
||||||
|
func init () {
|
||||||
|
var err error
|
||||||
|
homeDirectory, err = os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
panic("could not get user home directory: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
configHome = os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
if configHome == "" {
|
||||||
|
configHome = filepath.Join(homeDirectory, "/.config/")
|
||||||
|
}
|
||||||
|
|
||||||
|
configDirsString := os.Getenv("XDG_CONFIG_DIRS")
|
||||||
|
if configDirsString == "" {
|
||||||
|
configDirsString = "/etc/xdg/"
|
||||||
|
}
|
||||||
|
configDirs = append(strings.Split(configDirsString, ":"), configHome)
|
||||||
|
|
||||||
|
dataHome = os.Getenv("XDG_DATA_HOME")
|
||||||
|
if dataHome == "" {
|
||||||
|
dataHome = filepath.Join(homeDirectory, "/.local/share/")
|
||||||
|
}
|
||||||
|
|
||||||
|
dataDirsString := os.Getenv("XDG_CONFIG_DIRS")
|
||||||
|
if dataDirsString == "" {
|
||||||
|
dataDirsString = "/usr/local/share/:/usr/share/"
|
||||||
|
}
|
||||||
|
configDirs = append(strings.Split(configDirsString, ":"), configHome)
|
||||||
|
|
||||||
|
cacheHome = os.Getenv("XDG_CACHE_HOME")
|
||||||
|
if cacheHome == "" {
|
||||||
|
cacheHome = filepath.Join(homeDirectory, "/.cache/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigHome returns the path to the directory where user configuration files
|
||||||
|
// should be stored.
|
||||||
|
func ConfigHome (name string) (home string) {
|
||||||
|
return filepath.Join(configHome, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigDirs returns all paths where configuration files might exist.
|
||||||
|
func ConfigDirs (name string) (dirs []string) {
|
||||||
|
dirs = make([]string, len(configDirs))
|
||||||
|
for index, dir := range configDirs {
|
||||||
|
dirs[index] = filepath.Join(dir, name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataHome returns the path to the directory where user data should be stored.
|
||||||
|
func DataHome (name string) (home string) {
|
||||||
|
return filepath.Join(dataHome, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataDirs returns all paths where data files might exist.
|
||||||
|
func DataDirs (name string) (dirs []string) {
|
||||||
|
dirs = make([]string, len(dataDirs))
|
||||||
|
for index, dir := range dataDirs {
|
||||||
|
dirs[index] = filepath.Join(dir, name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheHome returns the path to the directory where user cache files should be
|
||||||
|
// stored.
|
||||||
|
func CacheHome (name string) (home string) {
|
||||||
|
return filepath.Join(cacheHome, name)
|
||||||
|
}
|
@ -3,11 +3,10 @@ package basicElements
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var buttonCase = theme.C("basic", "button")
|
|
||||||
|
|
||||||
// Button is a clickable button.
|
// Button is a clickable button.
|
||||||
type Button struct {
|
type Button struct {
|
||||||
*core.Core
|
*core.Core
|
||||||
@ -19,21 +18,19 @@ type Button struct {
|
|||||||
pressed bool
|
pressed bool
|
||||||
text string
|
text string
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
onClick func ()
|
onClick func ()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewButton creates a new button with the specified label text.
|
// NewButton creates a new button with the specified label text.
|
||||||
func NewButton (text string) (element *Button) {
|
func NewButton (text string) (element *Button) {
|
||||||
element = &Button { }
|
element = &Button { }
|
||||||
|
element.theme.Case = theme.C("basic", "button")
|
||||||
element.Core, element.core = core.NewCore(element.draw)
|
element.Core, element.core = core.NewCore(element.draw)
|
||||||
element.FocusableCore,
|
element.FocusableCore,
|
||||||
element.focusableControl = core.NewFocusableCore (func () {
|
element.focusableControl = core.NewFocusableCore(element.redo)
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
element.drawer.SetFace(theme.FontFaceRegular())
|
|
||||||
element.SetText(text)
|
element.SetText(text)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -43,19 +40,13 @@ func (element *Button) HandleMouseDown (x, y int, button input.Button) {
|
|||||||
if !element.Focused() { element.Focus() }
|
if !element.Focused() { element.Focus() }
|
||||||
if button != input.ButtonLeft { return }
|
if button != input.ButtonLeft { return }
|
||||||
element.pressed = true
|
element.pressed = true
|
||||||
if element.core.HasImage() {
|
element.redo()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Button) HandleMouseUp (x, y int, button input.Button) {
|
func (element *Button) HandleMouseUp (x, y int, button input.Button) {
|
||||||
if button != input.ButtonLeft { return }
|
if button != input.ButtonLeft { return }
|
||||||
element.pressed = false
|
element.pressed = false
|
||||||
if element.core.HasImage() {
|
element.redo()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
within := image.Point { x, y }.
|
within := image.Point { x, y }.
|
||||||
In(element.Bounds())
|
In(element.Bounds())
|
||||||
@ -73,20 +64,14 @@ func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers)
|
|||||||
if !element.Enabled() { return }
|
if !element.Enabled() { return }
|
||||||
if key == input.KeyEnter {
|
if key == input.KeyEnter {
|
||||||
element.pressed = true
|
element.pressed = true
|
||||||
if element.core.HasImage() {
|
element.redo()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||||
if key == input.KeyEnter && element.pressed {
|
if key == input.KeyEnter && element.pressed {
|
||||||
element.pressed = false
|
element.pressed = false
|
||||||
if element.core.HasImage() {
|
element.redo()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
if !element.Enabled() { return }
|
if !element.Enabled() { return }
|
||||||
if element.onClick != nil {
|
if element.onClick != nil {
|
||||||
element.onClick()
|
element.onClick()
|
||||||
@ -110,10 +95,36 @@ func (element *Button) SetText (text string) {
|
|||||||
|
|
||||||
element.text = text
|
element.text = text
|
||||||
element.drawer.SetText([]rune(text))
|
element.drawer.SetText([]rune(text))
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Button) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
element.drawer.SetFace (element.theme.FontFace (
|
||||||
|
theme.FontStyleRegular,
|
||||||
|
theme.FontSizeNormal))
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *Button) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Button) updateMinimumSize () {
|
||||||
textBounds := element.drawer.LayoutBounds()
|
textBounds := element.drawer.LayoutBounds()
|
||||||
_, inset := theme.ButtonPattern(theme.PatternState { Case: buttonCase })
|
minimumSize := textBounds.Inset(-element.config.Padding())
|
||||||
minimumSize := inset.Inverse().Apply(textBounds).Inset(-theme.Padding())
|
|
||||||
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Button) redo () {
|
||||||
if element.core.HasImage () {
|
if element.core.HasImage () {
|
||||||
element.draw()
|
element.draw()
|
||||||
element.core.DamageAll()
|
element.core.DamageAll()
|
||||||
@ -123,21 +134,20 @@ func (element *Button) SetText (text string) {
|
|||||||
func (element *Button) draw () {
|
func (element *Button) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
|
|
||||||
pattern, inset := theme.ButtonPattern(theme.PatternState {
|
state := theme.PatternState {
|
||||||
Case: buttonCase,
|
|
||||||
Disabled: !element.Enabled(),
|
Disabled: !element.Enabled(),
|
||||||
Focused: element.Focused(),
|
Focused: element.Focused(),
|
||||||
Pressed: element.pressed,
|
Pressed: element.pressed,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
pattern := element.theme.Pattern(theme.PatternButton, state)
|
||||||
|
|
||||||
artist.FillRectangle(element, pattern, bounds)
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
|
|
||||||
innerBounds := inset.Apply(bounds)
|
|
||||||
|
|
||||||
textBounds := element.drawer.LayoutBounds()
|
textBounds := element.drawer.LayoutBounds()
|
||||||
offset := image.Point {
|
offset := image.Point {
|
||||||
X: innerBounds.Min.X + (innerBounds.Dx() - textBounds.Dx()) / 2,
|
X: bounds.Min.X + (bounds.Dx() - textBounds.Dx()) / 2,
|
||||||
Y: innerBounds.Min.Y + (innerBounds.Dy() - textBounds.Dy()) / 2,
|
Y: bounds.Min.Y + (bounds.Dy() - textBounds.Dy()) / 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
// account for the fact that the bounding rectangle will be shifted over
|
// account for the fact that the bounding rectangle will be shifted over
|
||||||
@ -145,9 +155,10 @@ func (element *Button) draw () {
|
|||||||
offset.Y -= textBounds.Min.Y
|
offset.Y -= textBounds.Min.Y
|
||||||
offset.X -= textBounds.Min.X
|
offset.X -= textBounds.Min.X
|
||||||
|
|
||||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
if element.pressed {
|
||||||
Case: buttonCase,
|
offset = offset.Add(element.theme.Sink(theme.PatternButton))
|
||||||
Disabled: !element.Enabled(),
|
}
|
||||||
})
|
|
||||||
|
foreground := element.theme.Pattern(theme.PatternForeground, state)
|
||||||
element.drawer.Draw(element, foreground, offset)
|
element.drawer.Draw(element, foreground, offset)
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,10 @@ package basicElements
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var checkboxCase = theme.C("basic", "checkbox")
|
|
||||||
|
|
||||||
// Checkbox is a toggle-able checkbox with a label.
|
// Checkbox is a toggle-able checkbox with a label.
|
||||||
type Checkbox struct {
|
type Checkbox struct {
|
||||||
*core.Core
|
*core.Core
|
||||||
@ -20,21 +19,19 @@ type Checkbox struct {
|
|||||||
checked bool
|
checked bool
|
||||||
text string
|
text string
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
onToggle func ()
|
onToggle func ()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCheckbox creates a new cbeckbox with the specified label text.
|
// NewCheckbox creates a new cbeckbox with the specified label text.
|
||||||
func NewCheckbox (text string, checked bool) (element *Checkbox) {
|
func NewCheckbox (text string, checked bool) (element *Checkbox) {
|
||||||
element = &Checkbox { checked: checked }
|
element = &Checkbox { checked: checked }
|
||||||
|
element.theme.Case = theme.C("basic", "checkbox")
|
||||||
element.Core, element.core = core.NewCore(element.draw)
|
element.Core, element.core = core.NewCore(element.draw)
|
||||||
element.FocusableCore,
|
element.FocusableCore,
|
||||||
element.focusableControl = core.NewFocusableCore (func () {
|
element.focusableControl = core.NewFocusableCore(element.redo)
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
element.drawer.SetFace(theme.FontFaceRegular())
|
|
||||||
element.SetText(text)
|
element.SetText(text)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -116,16 +113,45 @@ func (element *Checkbox) SetText (text string) {
|
|||||||
|
|
||||||
element.text = text
|
element.text = text
|
||||||
element.drawer.SetText([]rune(text))
|
element.drawer.SetText([]rune(text))
|
||||||
textBounds := element.drawer.LayoutBounds()
|
element.updateMinimumSize()
|
||||||
|
|
||||||
if text == "" {
|
if element.core.HasImage () {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Checkbox) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
element.drawer.SetFace (element.theme.FontFace (
|
||||||
|
theme.FontStyleRegular,
|
||||||
|
theme.FontSizeNormal))
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *Checkbox) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Checkbox) updateMinimumSize () {
|
||||||
|
textBounds := element.drawer.LayoutBounds()
|
||||||
|
if element.text == "" {
|
||||||
element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
|
element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
|
||||||
} else {
|
} else {
|
||||||
element.core.SetMinimumSize (
|
element.core.SetMinimumSize (
|
||||||
textBounds.Dy() + theme.Padding() + textBounds.Dx(),
|
textBounds.Dy() + element.config.Padding() + textBounds.Dx(),
|
||||||
textBounds.Dy())
|
textBounds.Dy())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Checkbox) redo () {
|
||||||
if element.core.HasImage () {
|
if element.core.HasImage () {
|
||||||
element.draw()
|
element.draw()
|
||||||
element.core.DamageAll()
|
element.core.DamageAll()
|
||||||
@ -136,35 +162,28 @@ func (element *Checkbox) draw () {
|
|||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
||||||
|
|
||||||
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState {
|
state := theme.PatternState {
|
||||||
Case: checkboxCase,
|
|
||||||
})
|
|
||||||
artist.FillRectangle(element, backgroundPattern, bounds)
|
|
||||||
|
|
||||||
pattern, inset := theme.ButtonPattern(theme.PatternState {
|
|
||||||
Case: checkboxCase,
|
|
||||||
Disabled: !element.Enabled(),
|
Disabled: !element.Enabled(),
|
||||||
Focused: element.Focused(),
|
Focused: element.Focused(),
|
||||||
Pressed: element.pressed,
|
Pressed: element.pressed,
|
||||||
})
|
On: element.checked,
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundPattern := element.theme.Pattern (
|
||||||
|
theme.PatternBackground, state)
|
||||||
|
artist.FillRectangle(element, backgroundPattern, bounds)
|
||||||
|
|
||||||
|
pattern := element.theme.Pattern(theme.PatternButton, state)
|
||||||
artist.FillRectangle(element, pattern, boxBounds)
|
artist.FillRectangle(element, pattern, boxBounds)
|
||||||
|
|
||||||
textBounds := element.drawer.LayoutBounds()
|
textBounds := element.drawer.LayoutBounds()
|
||||||
offset := bounds.Min.Add(image.Point {
|
offset := bounds.Min.Add(image.Point {
|
||||||
X: bounds.Dy() + theme.Padding(),
|
X: bounds.Dy() + element.config.Padding(),
|
||||||
})
|
})
|
||||||
|
|
||||||
offset.Y -= textBounds.Min.Y
|
offset.Y -= textBounds.Min.Y
|
||||||
offset.X -= textBounds.Min.X
|
offset.X -= textBounds.Min.X
|
||||||
|
|
||||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
foreground := element.theme.Pattern(theme.PatternForeground, state)
|
||||||
Case: checkboxCase,
|
|
||||||
Disabled: !element.Enabled(),
|
|
||||||
})
|
|
||||||
element.drawer.Draw(element, foreground, offset)
|
element.drawer.Draw(element, foreground, offset)
|
||||||
|
|
||||||
if element.checked {
|
|
||||||
checkBounds := inset.Apply(boxBounds).Inset(2)
|
|
||||||
artist.FillRectangle(element, foreground, checkBounds)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,13 @@ package basicElements
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/layouts"
|
import "git.tebibyte.media/sashakoshka/tomo/layouts"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var containerCase = theme.C("basic", "container")
|
|
||||||
|
|
||||||
// Container is an element capable of containg other elements, and arranging
|
// Container is an element capable of containg other elements, and arranging
|
||||||
// them in a layout.
|
// them in a layout.
|
||||||
type Container struct {
|
type Container struct {
|
||||||
@ -25,6 +24,9 @@ type Container struct {
|
|||||||
focusable bool
|
focusable bool
|
||||||
flexible bool
|
flexible bool
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
onFocusRequest func () (granted bool)
|
onFocusRequest func () (granted bool)
|
||||||
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
|
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
|
||||||
onFlexibleHeightChange func ()
|
onFlexibleHeightChange func ()
|
||||||
@ -33,6 +35,7 @@ type Container struct {
|
|||||||
// NewContainer creates a new container.
|
// NewContainer creates a new container.
|
||||||
func NewContainer (layout layouts.Layout) (element *Container) {
|
func NewContainer (layout layouts.Layout) (element *Container) {
|
||||||
element = &Container { }
|
element = &Container { }
|
||||||
|
element.theme.Case = theme.C("basic", "container")
|
||||||
element.Core, element.core = core.NewCore(element.redoAll)
|
element.Core, element.core = core.NewCore(element.redoAll)
|
||||||
element.SetLayout(layout)
|
element.SetLayout(layout)
|
||||||
return
|
return
|
||||||
@ -52,6 +55,12 @@ func (element *Container) SetLayout (layout layouts.Layout) {
|
|||||||
// whatever way is defined by the current layout.
|
// whatever way is defined by the current layout.
|
||||||
func (element *Container) Adopt (child elements.Element, expand bool) {
|
func (element *Container) Adopt (child elements.Element, expand bool) {
|
||||||
// set event handlers
|
// set event handlers
|
||||||
|
if child0, ok := child.(elements.Themeable); ok {
|
||||||
|
child0.SetTheme(element.theme.Theme)
|
||||||
|
}
|
||||||
|
if child0, ok := child.(elements.Configurable); ok {
|
||||||
|
child0.SetConfig(element.config.Config)
|
||||||
|
}
|
||||||
child.OnDamage (func (region canvas.Canvas) {
|
child.OnDamage (func (region canvas.Canvas) {
|
||||||
element.core.DamageRegion(region.Bounds())
|
element.core.DamageRegion(region.Bounds())
|
||||||
})
|
})
|
||||||
@ -205,9 +214,9 @@ func (element *Container) redoAll () {
|
|||||||
|
|
||||||
// draw a background
|
// draw a background
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
pattern, _ := theme.BackgroundPattern (theme.PatternState {
|
pattern := element.theme.Pattern (
|
||||||
Case: containerCase,
|
theme.PatternBackground,
|
||||||
})
|
theme.PatternState { })
|
||||||
artist.FillRectangle(element, pattern, bounds)
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
|
|
||||||
// cut our canvas up and give peices to child elements
|
// cut our canvas up and give peices to child elements
|
||||||
@ -216,6 +225,33 @@ func (element *Container) redoAll () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Container) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
for _, child := range element.children {
|
||||||
|
if child0, ok := child.Element.(elements.Themeable); ok {
|
||||||
|
child0.SetTheme(element.theme.Theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redoAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *Container) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
for _, child := range element.children {
|
||||||
|
if child0, ok := child.Element.(elements.Configurable); ok {
|
||||||
|
child0.SetConfig(element.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redoAll()
|
||||||
|
}
|
||||||
|
|
||||||
func (element *Container) HandleMouseDown (x, y int, button input.Button) {
|
func (element *Container) HandleMouseDown (x, y int, button input.Button) {
|
||||||
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget)
|
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget)
|
||||||
if !handlesMouse { return }
|
if !handlesMouse { return }
|
||||||
@ -266,7 +302,7 @@ func (element *Container) HandleKeyUp (key input.Key, modifiers input.Modifiers)
|
|||||||
func (element *Container) FlexibleHeightFor (width int) (height int) {
|
func (element *Container) FlexibleHeightFor (width int) (height int) {
|
||||||
return element.layout.FlexibleHeightFor (
|
return element.layout.FlexibleHeightFor (
|
||||||
element.children,
|
element.children,
|
||||||
theme.Margin(), width)
|
element.config.Margin(), width)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Container) OnFlexibleHeightChange (callback func ()) {
|
func (element *Container) OnFlexibleHeightChange (callback func ()) {
|
||||||
@ -469,15 +505,15 @@ func (element *Container) childFocusRequestCallback (
|
|||||||
|
|
||||||
func (element *Container) updateMinimumSize () {
|
func (element *Container) updateMinimumSize () {
|
||||||
width, height := element.layout.MinimumSize (
|
width, height := element.layout.MinimumSize (
|
||||||
element.children, theme.Margin())
|
element.children, element.config.Margin())
|
||||||
if element.flexible {
|
if element.flexible {
|
||||||
height = element.layout.FlexibleHeightFor (
|
height = element.layout.FlexibleHeightFor (
|
||||||
element.children, theme.Margin(), width)
|
element.children, element.config.Margin(), width)
|
||||||
}
|
}
|
||||||
element.core.SetMinimumSize(width, height)
|
element.core.SetMinimumSize(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Container) recalculate () {
|
func (element *Container) recalculate () {
|
||||||
element.layout.Arrange (
|
element.layout.Arrange (
|
||||||
element.children, theme.Margin(), element.Bounds())
|
element.children, element.config.Margin(), element.Bounds())
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
package basicElements
|
package basicElements
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var labelCase = theme.C("basic", "label")
|
|
||||||
|
|
||||||
// Label is a simple text box.
|
// Label is a simple text box.
|
||||||
type Label struct {
|
type Label struct {
|
||||||
*core.Core
|
*core.Core
|
||||||
@ -15,6 +14,9 @@ type Label struct {
|
|||||||
text string
|
text string
|
||||||
drawer artist.TextDrawer
|
drawer artist.TextDrawer
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
onFlexibleHeightChange func ()
|
onFlexibleHeightChange func ()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,14 +24,28 @@ type Label struct {
|
|||||||
// wrapped.
|
// wrapped.
|
||||||
func NewLabel (text string, wrap bool) (element *Label) {
|
func NewLabel (text string, wrap bool) (element *Label) {
|
||||||
element = &Label { }
|
element = &Label { }
|
||||||
|
element.theme.Case = theme.C("basic", "label")
|
||||||
element.Core, element.core = core.NewCore(element.handleResize)
|
element.Core, element.core = core.NewCore(element.handleResize)
|
||||||
face := theme.FontFaceRegular()
|
|
||||||
element.drawer.SetFace(face)
|
|
||||||
element.SetWrap(wrap)
|
element.SetWrap(wrap)
|
||||||
element.SetText(text)
|
element.SetText(text)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (element *Label) redo () {
|
||||||
|
face := element.theme.FontFace (
|
||||||
|
theme.FontStyleRegular,
|
||||||
|
theme.FontSizeNormal)
|
||||||
|
element.drawer.SetFace(face)
|
||||||
|
element.updateMinimumSize()
|
||||||
|
bounds := element.Bounds()
|
||||||
|
if element.wrap {
|
||||||
|
element.drawer.SetMaxWidth(bounds.Dx())
|
||||||
|
element.drawer.SetMaxHeight(bounds.Dy())
|
||||||
|
}
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
|
||||||
func (element *Label) handleResize () {
|
func (element *Label) handleResize () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
if element.wrap {
|
if element.wrap {
|
||||||
@ -90,10 +106,37 @@ func (element *Label) SetWrap (wrap bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Label) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
element.drawer.SetFace (element.theme.FontFace (
|
||||||
|
theme.FontStyleRegular,
|
||||||
|
theme.FontSizeNormal))
|
||||||
|
element.updateMinimumSize()
|
||||||
|
|
||||||
|
if element.core.HasImage () {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *Label) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
|
||||||
|
if element.core.HasImage () {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (element *Label) updateMinimumSize () {
|
func (element *Label) updateMinimumSize () {
|
||||||
if element.wrap {
|
if element.wrap {
|
||||||
em := element.drawer.Em().Round()
|
em := element.drawer.Em().Round()
|
||||||
if em < 1 { em = theme.Padding() }
|
if em < 1 { em = element.config.Padding() }
|
||||||
element.core.SetMinimumSize (
|
element.core.SetMinimumSize (
|
||||||
em, element.drawer.LineHeight().Round())
|
em, element.drawer.LineHeight().Round())
|
||||||
if element.onFlexibleHeightChange != nil {
|
if element.onFlexibleHeightChange != nil {
|
||||||
@ -108,15 +151,15 @@ func (element *Label) updateMinimumSize () {
|
|||||||
func (element *Label) draw () {
|
func (element *Label) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
|
|
||||||
pattern, _ := theme.BackgroundPattern(theme.PatternState {
|
pattern := element.theme.Pattern (
|
||||||
Case: labelCase,
|
theme.PatternBackground,
|
||||||
})
|
theme.PatternState { })
|
||||||
artist.FillRectangle(element, pattern, bounds)
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
|
|
||||||
textBounds := element.drawer.LayoutBounds()
|
textBounds := element.drawer.LayoutBounds()
|
||||||
|
|
||||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
foreground := element.theme.Pattern (
|
||||||
Case: labelCase,
|
theme.PatternForeground,
|
||||||
})
|
theme.PatternState { })
|
||||||
element.drawer.Draw (element, foreground, bounds.Min.Sub(textBounds.Min))
|
element.drawer.Draw(element, foreground, bounds.Min.Sub(textBounds.Min))
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,11 @@ import "fmt"
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var listCase = theme.C("basic", "list")
|
|
||||||
|
|
||||||
// List is an element that contains several objects that a user can select.
|
// List is an element that contains several objects that a user can select.
|
||||||
type List struct {
|
type List struct {
|
||||||
*core.Core
|
*core.Core
|
||||||
@ -27,6 +26,9 @@ type List struct {
|
|||||||
scroll int
|
scroll int
|
||||||
entries []ListEntry
|
entries []ListEntry
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
onScrollBoundsChange func ()
|
onScrollBoundsChange func ()
|
||||||
onNoEntrySelected func ()
|
onNoEntrySelected func ()
|
||||||
}
|
}
|
||||||
@ -34,6 +36,7 @@ type List struct {
|
|||||||
// NewList creates a new list element with the specified entries.
|
// NewList creates a new list element with the specified entries.
|
||||||
func NewList (entries ...ListEntry) (element *List) {
|
func NewList (entries ...ListEntry) (element *List) {
|
||||||
element = &List { selectedEntry: -1 }
|
element = &List { selectedEntry: -1 }
|
||||||
|
element.theme.Case = theme.C("basic", "list")
|
||||||
element.Core, element.core = core.NewCore(element.handleResize)
|
element.Core, element.core = core.NewCore(element.handleResize)
|
||||||
element.FocusableCore,
|
element.FocusableCore,
|
||||||
element.focusableControl = core.NewFocusableCore (func () {
|
element.focusableControl = core.NewFocusableCore (func () {
|
||||||
@ -63,6 +66,44 @@ func (element *List) handleResize () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *List) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
for index, entry := range element.entries {
|
||||||
|
entry.SetTheme(element.theme.Theme)
|
||||||
|
element.entries[index] = entry
|
||||||
|
}
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *List) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
for index, entry := range element.entries {
|
||||||
|
entry.SetConfig(element.config)
|
||||||
|
element.entries[index] = entry
|
||||||
|
}
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) redo () {
|
||||||
|
for index, entry := range element.entries {
|
||||||
|
element.entries[index] = element.resizeEntryToFit(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
if element.core.HasImage() {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
if element.onScrollBoundsChange != nil {
|
||||||
|
element.onScrollBoundsChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Collapse forces a minimum width and height upon the list. If a zero value is
|
// Collapse forces a minimum width and height upon the list. If a zero value is
|
||||||
// given for a dimension, its minimum will be determined by the list's content.
|
// given for a dimension, its minimum will be determined by the list's content.
|
||||||
// If the list's height goes beyond the forced size, it will need to be accessed
|
// If the list's height goes beyond the forced size, it will need to be accessed
|
||||||
@ -164,9 +205,7 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *List) scrollViewportHeight () (height int) {
|
func (element *List) scrollViewportHeight () (height int) {
|
||||||
_, inset := theme.ListPattern(theme.PatternState {
|
inset := element.theme.Inset(theme.PatternSunken)
|
||||||
Case: listCase,
|
|
||||||
})
|
|
||||||
return element.Bounds().Dy() - inset[0] - inset[2]
|
return element.Bounds().Dy() - inset[0] - inset[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +237,8 @@ func (element *List) CountEntries () (count int) {
|
|||||||
func (element *List) Append (entry ListEntry) {
|
func (element *List) Append (entry ListEntry) {
|
||||||
// append
|
// append
|
||||||
entry.Collapse(element.forcedMinimumWidth)
|
entry.Collapse(element.forcedMinimumWidth)
|
||||||
|
entry.SetTheme(element.theme.Theme)
|
||||||
|
entry.SetConfig(element.config)
|
||||||
element.entries = append(element.entries, entry)
|
element.entries = append(element.entries, entry)
|
||||||
|
|
||||||
// recalculate, redraw, notify
|
// recalculate, redraw, notify
|
||||||
@ -290,7 +331,7 @@ func (element *List) Replace (index int, entry ListEntry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *List) selectUnderMouse (x, y int) (updated bool) {
|
func (element *List) selectUnderMouse (x, y int) (updated bool) {
|
||||||
_, inset := theme.ListPattern(theme.PatternState { })
|
inset := element.theme.Inset(theme.PatternSunken)
|
||||||
bounds := inset.Apply(element.Bounds())
|
bounds := inset.Apply(element.Bounds())
|
||||||
mousePoint := image.Pt(x, y)
|
mousePoint := image.Pt(x, y)
|
||||||
dot := image.Pt (
|
dot := image.Pt (
|
||||||
@ -332,9 +373,7 @@ func (element *List) changeSelectionBy (delta int) (updated bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
|
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
|
||||||
_, inset := theme.ListPattern(theme.PatternState {
|
inset := element.theme.Inset(theme.PatternSunken)
|
||||||
Case: listCase,
|
|
||||||
})
|
|
||||||
entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1])
|
entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1])
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
@ -361,9 +400,7 @@ func (element *List) updateMinimumSize () {
|
|||||||
minimumHeight = element.contentHeight
|
minimumHeight = element.contentHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
_, inset := theme.ListPattern(theme.PatternState {
|
inset := element.theme.Inset(theme.PatternSunken)
|
||||||
Case: listCase,
|
|
||||||
})
|
|
||||||
minimumHeight += inset[0] + inset[2]
|
minimumHeight += inset[0] + inset[2]
|
||||||
|
|
||||||
element.core.SetMinimumSize(minimumWidth, minimumHeight)
|
element.core.SetMinimumSize(minimumWidth, minimumHeight)
|
||||||
@ -372,8 +409,8 @@ func (element *List) updateMinimumSize () {
|
|||||||
func (element *List) draw () {
|
func (element *List) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
|
|
||||||
pattern, inset := theme.ListPattern(theme.PatternState {
|
inset := element.theme.Inset(theme.PatternSunken)
|
||||||
Case: listCase,
|
pattern := element.theme.Pattern (theme.PatternSunken, theme.PatternState {
|
||||||
Disabled: !element.Enabled(),
|
Disabled: !element.Enabled(),
|
||||||
Focused: element.Focused(),
|
Focused: element.Focused(),
|
||||||
})
|
})
|
||||||
|
@ -2,11 +2,10 @@ package basicElements
|
|||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
|
||||||
var listEntryCase = theme.C("basic", "listEntry")
|
|
||||||
|
|
||||||
// ListEntry is an item that can be added to a list.
|
// ListEntry is an item that can be added to a list.
|
||||||
type ListEntry struct {
|
type ListEntry struct {
|
||||||
drawer artist.TextDrawer
|
drawer artist.TextDrawer
|
||||||
@ -14,6 +13,10 @@ type ListEntry struct {
|
|||||||
textPoint image.Point
|
textPoint image.Point
|
||||||
text string
|
text string
|
||||||
forcedMinimumWidth int
|
forcedMinimumWidth int
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
onSelect func ()
|
onSelect func ()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,8 +25,8 @@ func NewListEntry (text string, onSelect func ()) (entry ListEntry) {
|
|||||||
text: text,
|
text: text,
|
||||||
onSelect: onSelect,
|
onSelect: onSelect,
|
||||||
}
|
}
|
||||||
|
entry.theme.Case = theme.C("basic", "listEntry")
|
||||||
entry.drawer.SetText([]rune(text))
|
entry.drawer.SetText([]rune(text))
|
||||||
entry.drawer.SetFace(theme.FontFaceRegular())
|
|
||||||
entry.updateBounds()
|
entry.updateBounds()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -34,6 +37,20 @@ func (entry *ListEntry) Collapse (width int) {
|
|||||||
entry.updateBounds()
|
entry.updateBounds()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (entry *ListEntry) SetTheme (new theme.Theme) {
|
||||||
|
if new == entry.theme.Theme { return }
|
||||||
|
entry.theme.Theme = new
|
||||||
|
entry.drawer.SetFace (entry.theme.FontFace (
|
||||||
|
theme.FontStyleRegular,
|
||||||
|
theme.FontSizeNormal))
|
||||||
|
entry.updateBounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *ListEntry) SetConfig (new config.Config) {
|
||||||
|
if new == entry.config.Config { return }
|
||||||
|
entry.config.Config = new
|
||||||
|
}
|
||||||
|
|
||||||
func (entry *ListEntry) updateBounds () {
|
func (entry *ListEntry) updateBounds () {
|
||||||
entry.bounds = image.Rectangle { }
|
entry.bounds = image.Rectangle { }
|
||||||
entry.bounds.Max.Y = entry.drawer.LineHeight().Round()
|
entry.bounds.Max.Y = entry.drawer.LineHeight().Round()
|
||||||
@ -43,8 +60,7 @@ func (entry *ListEntry) updateBounds () {
|
|||||||
entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx()
|
entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx()
|
||||||
}
|
}
|
||||||
|
|
||||||
_, inset := theme.ItemPattern(theme.PatternState {
|
inset := entry.theme.Inset(theme.PatternRaised)
|
||||||
})
|
|
||||||
entry.bounds.Max.Y += inset[0] + inset[2]
|
entry.bounds.Max.Y += inset[0] + inset[2]
|
||||||
|
|
||||||
entry.textPoint =
|
entry.textPoint =
|
||||||
@ -60,20 +76,16 @@ func (entry *ListEntry) Draw (
|
|||||||
) (
|
) (
|
||||||
updatedRegion image.Rectangle,
|
updatedRegion image.Rectangle,
|
||||||
) {
|
) {
|
||||||
pattern, _ := theme.ItemPattern(theme.PatternState {
|
state := theme.PatternState {
|
||||||
Case: listEntryCase,
|
|
||||||
Focused: focused,
|
Focused: focused,
|
||||||
On: on,
|
On: on,
|
||||||
})
|
}
|
||||||
|
pattern := entry.theme.Pattern (theme.PatternRaised, state)
|
||||||
artist.FillRectangle (
|
artist.FillRectangle (
|
||||||
destination,
|
destination,
|
||||||
pattern,
|
pattern,
|
||||||
entry.Bounds().Add(offset))
|
entry.Bounds().Add(offset))
|
||||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
foreground := entry.theme.Pattern (theme.PatternForeground, state)
|
||||||
Case: listEntryCase,
|
|
||||||
Focused: focused,
|
|
||||||
On: on,
|
|
||||||
})
|
|
||||||
return entry.drawer.Draw (
|
return entry.drawer.Draw (
|
||||||
destination,
|
destination,
|
||||||
foreground,
|
foreground,
|
||||||
|
@ -2,6 +2,7 @@ package basicElements
|
|||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
@ -10,14 +11,17 @@ type ProgressBar struct {
|
|||||||
*core.Core
|
*core.Core
|
||||||
core core.CoreControl
|
core core.CoreControl
|
||||||
progress float64
|
progress float64
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProgressBar creates a new progress bar displaying the given progress
|
// NewProgressBar creates a new progress bar displaying the given progress
|
||||||
// level.
|
// level.
|
||||||
func NewProgressBar (progress float64) (element *ProgressBar) {
|
func NewProgressBar (progress float64) (element *ProgressBar) {
|
||||||
element = &ProgressBar { progress: progress }
|
element = &ProgressBar { progress: progress }
|
||||||
|
element.theme.Case = theme.C("basic", "progressBar")
|
||||||
element.Core, element.core = core.NewCore(element.draw)
|
element.Core, element.core = core.NewCore(element.draw)
|
||||||
element.core.SetMinimumSize(theme.Padding() * 2, theme.Padding() * 2)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,16 +35,50 @@ func (element *ProgressBar) SetProgress (progress float64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *ProgressBar) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *ProgressBar) SetConfig (new config.Config) {
|
||||||
|
if new == nil || new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element (ProgressBar)) updateMinimumSize() {
|
||||||
|
element.core.SetMinimumSize (
|
||||||
|
element.config.Padding() * 2,
|
||||||
|
element.config.Padding() * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *ProgressBar) redo () {
|
||||||
|
if element.core.HasImage() {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (element *ProgressBar) draw () {
|
func (element *ProgressBar) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
|
|
||||||
pattern, inset := theme.SunkenPattern(theme.PatternState { })
|
pattern := element.theme.Pattern (
|
||||||
|
theme.PatternSunken,
|
||||||
|
theme.PatternState { })
|
||||||
|
inset := element.theme.Inset(theme.PatternSunken)
|
||||||
artist.FillRectangle(element, pattern, bounds)
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
bounds = inset.Apply(bounds)
|
bounds = inset.Apply(bounds)
|
||||||
meterBounds := image.Rect (
|
meterBounds := image.Rect (
|
||||||
bounds.Min.X, bounds.Min.Y,
|
bounds.Min.X, bounds.Min.Y,
|
||||||
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
|
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
|
||||||
bounds.Max.Y)
|
bounds.Max.Y)
|
||||||
accent, _ := theme.AccentPattern(theme.PatternState { })
|
accent := element.theme.Pattern (
|
||||||
|
theme.PatternAccent,
|
||||||
|
theme.PatternState { })
|
||||||
artist.FillRectangle(element, accent, meterBounds)
|
artist.FillRectangle(element, accent, meterBounds)
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,12 @@ package basicElements
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var scrollContainerCase = theme.C("basic", "scrollContainer")
|
|
||||||
var scrollBarHorizontalCase = theme.C("basic", "scrollBarHorizontal")
|
|
||||||
var scrollBarVerticalCase = theme.C("basic", "scrollBarVertical")
|
|
||||||
|
|
||||||
// ScrollContainer is a container that is capable of holding a scrollable
|
// ScrollContainer is a container that is capable of holding a scrollable
|
||||||
// element.
|
// element.
|
||||||
type ScrollContainer struct {
|
type ScrollContainer struct {
|
||||||
@ -23,6 +20,7 @@ type ScrollContainer struct {
|
|||||||
childWidth, childHeight int
|
childWidth, childHeight int
|
||||||
|
|
||||||
horizontal struct {
|
horizontal struct {
|
||||||
|
theme theme.Wrapped
|
||||||
exists bool
|
exists bool
|
||||||
enabled bool
|
enabled bool
|
||||||
dragging bool
|
dragging bool
|
||||||
@ -33,6 +31,7 @@ type ScrollContainer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vertical struct {
|
vertical struct {
|
||||||
|
theme theme.Wrapped
|
||||||
exists bool
|
exists bool
|
||||||
enabled bool
|
enabled bool
|
||||||
dragging bool
|
dragging bool
|
||||||
@ -42,6 +41,9 @@ type ScrollContainer struct {
|
|||||||
bar image.Rectangle
|
bar image.Rectangle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
onFocusRequest func () (granted bool)
|
onFocusRequest func () (granted bool)
|
||||||
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
|
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
|
||||||
}
|
}
|
||||||
@ -50,8 +52,11 @@ type ScrollContainer struct {
|
|||||||
// bars.
|
// bars.
|
||||||
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
|
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
|
||||||
element = &ScrollContainer { }
|
element = &ScrollContainer { }
|
||||||
|
element.theme.Case = theme.C("basic", "scrollContainer")
|
||||||
|
element.horizontal.theme.Case = theme.C("basic", "scrollBarHorizontal")
|
||||||
|
element.vertical.theme.Case = theme.C("basic", "scrollBarVertical")
|
||||||
|
|
||||||
element.Core, element.core = core.NewCore(element.handleResize)
|
element.Core, element.core = core.NewCore(element.handleResize)
|
||||||
element.updateMinimumSize()
|
|
||||||
element.horizontal.exists = horizontal
|
element.horizontal.exists = horizontal
|
||||||
element.vertical.exists = vertical
|
element.vertical.exists = vertical
|
||||||
return
|
return
|
||||||
@ -75,6 +80,12 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) {
|
|||||||
// adopt new child
|
// adopt new child
|
||||||
element.child = child
|
element.child = child
|
||||||
if child != nil {
|
if child != nil {
|
||||||
|
if child0, ok := child.(elements.Themeable); ok {
|
||||||
|
child0.SetTheme(element.theme.Theme)
|
||||||
|
}
|
||||||
|
if child0, ok := child.(elements.Configurable); ok {
|
||||||
|
child0.SetConfig(element.config.Config)
|
||||||
|
}
|
||||||
child.OnDamage(element.childDamageCallback)
|
child.OnDamage(element.childDamageCallback)
|
||||||
child.OnMinimumSizeChange(element.updateMinimumSize)
|
child.OnMinimumSizeChange(element.updateMinimumSize)
|
||||||
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
|
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
|
||||||
@ -85,8 +96,6 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) {
|
|||||||
element.childFocusMotionRequestCallback)
|
element.childFocusMotionRequestCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: somehow inform the core that we do not in fact want to
|
|
||||||
// redraw the element.
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
|
|
||||||
element.horizontal.enabled,
|
element.horizontal.enabled,
|
||||||
@ -98,6 +107,34 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *ScrollContainer) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
if child, ok := element.child.(elements.Themeable); ok {
|
||||||
|
child.SetTheme(element.theme.Theme)
|
||||||
|
}
|
||||||
|
if element.core.HasImage() {
|
||||||
|
element.recalculate()
|
||||||
|
element.resizeChildToFit()
|
||||||
|
element.draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *ScrollContainer) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
if child, ok := element.child.(elements.Configurable); ok {
|
||||||
|
child.SetConfig(element.config.Config)
|
||||||
|
}
|
||||||
|
if element.core.HasImage() {
|
||||||
|
element.recalculate()
|
||||||
|
element.resizeChildToFit()
|
||||||
|
element.draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||||
if child, ok := element.child.(elements.KeyboardTarget); ok {
|
if child, ok := element.child.(elements.KeyboardTarget); ok {
|
||||||
child.HandleKeyDown(key, modifiers)
|
child.HandleKeyDown(key, modifiers)
|
||||||
@ -111,6 +148,7 @@ func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) {
|
func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) {
|
||||||
|
velocity := element.config.ScrollVelocity()
|
||||||
point := image.Pt(x, y)
|
point := image.Pt(x, y)
|
||||||
if point.In(element.horizontal.bar) {
|
if point.In(element.horizontal.bar) {
|
||||||
element.horizontal.dragging = true
|
element.horizontal.dragging = true
|
||||||
@ -123,9 +161,9 @@ func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button)
|
|||||||
// FIXME: x backend and scroll container should pull these
|
// FIXME: x backend and scroll container should pull these
|
||||||
// values from the same place
|
// values from the same place
|
||||||
if x > element.horizontal.bar.Min.X {
|
if x > element.horizontal.bar.Min.X {
|
||||||
element.scrollChildBy(16, 0)
|
element.scrollChildBy(velocity, 0)
|
||||||
} else {
|
} else {
|
||||||
element.scrollChildBy(-16, 0)
|
element.scrollChildBy(-velocity, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if point.In(element.vertical.bar) {
|
} else if point.In(element.vertical.bar) {
|
||||||
@ -137,9 +175,9 @@ func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button)
|
|||||||
|
|
||||||
} else if point.In(element.vertical.gutter) {
|
} else if point.In(element.vertical.gutter) {
|
||||||
if y > element.vertical.bar.Min.Y {
|
if y > element.vertical.bar.Min.Y {
|
||||||
element.scrollChildBy(0, 16)
|
element.scrollChildBy(0, velocity)
|
||||||
} else {
|
} else {
|
||||||
element.scrollChildBy(0, -16)
|
element.scrollChildBy(0, -velocity)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if child, ok := element.child.(elements.MouseTarget); ok {
|
} else if child, ok := element.child.(elements.MouseTarget); ok {
|
||||||
@ -281,22 +319,19 @@ func (element *ScrollContainer) resizeChildToFit () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *ScrollContainer) recalculate () {
|
func (element *ScrollContainer) recalculate () {
|
||||||
_, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState {
|
|
||||||
Case: scrollBarHorizontalCase,
|
|
||||||
})
|
|
||||||
_, gutterInsetVertical := theme.GutterPattern(theme.PatternState {
|
|
||||||
Case: scrollBarHorizontalCase,
|
|
||||||
})
|
|
||||||
|
|
||||||
horizontal := &element.horizontal
|
horizontal := &element.horizontal
|
||||||
vertical := &element.vertical
|
vertical := &element.vertical
|
||||||
|
|
||||||
|
gutterInsetHorizontal := horizontal.theme.Inset(theme.PatternGutter)
|
||||||
|
gutterInsetVertical := vertical.theme.Inset(theme.PatternGutter)
|
||||||
|
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
thicknessHorizontal :=
|
thicknessHorizontal :=
|
||||||
theme.HandleWidth() +
|
element.config.HandleWidth() +
|
||||||
gutterInsetHorizontal[3] +
|
gutterInsetHorizontal[3] +
|
||||||
gutterInsetHorizontal[1]
|
gutterInsetHorizontal[1]
|
||||||
thicknessVertical :=
|
thicknessVertical :=
|
||||||
theme.HandleWidth() +
|
element.config.HandleWidth() +
|
||||||
gutterInsetVertical[3] +
|
gutterInsetVertical[3] +
|
||||||
gutterInsetVertical[1]
|
gutterInsetVertical[1]
|
||||||
|
|
||||||
@ -373,9 +408,8 @@ func (element *ScrollContainer) recalculate () {
|
|||||||
|
|
||||||
func (element *ScrollContainer) draw () {
|
func (element *ScrollContainer) draw () {
|
||||||
artist.Paste(element, element.child, image.Point { })
|
artist.Paste(element, element.child, image.Point { })
|
||||||
deadPattern, _ := theme.DeadPattern(theme.PatternState {
|
deadPattern := element.theme.Pattern (
|
||||||
Case: scrollContainerCase,
|
theme.PatternDead, theme.PatternState { })
|
||||||
})
|
|
||||||
artist.FillRectangle (
|
artist.FillRectangle (
|
||||||
element, deadPattern,
|
element, deadPattern,
|
||||||
image.Rect (
|
image.Rect (
|
||||||
@ -388,32 +422,26 @@ func (element *ScrollContainer) draw () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *ScrollContainer) drawHorizontalBar () {
|
func (element *ScrollContainer) drawHorizontalBar () {
|
||||||
gutterPattern, _ := theme.GutterPattern (theme.PatternState {
|
state := theme.PatternState {
|
||||||
Case: scrollBarHorizontalCase,
|
|
||||||
Disabled: !element.horizontal.enabled,
|
|
||||||
})
|
|
||||||
artist.FillRectangle(element, gutterPattern, element.horizontal.gutter)
|
|
||||||
|
|
||||||
handlePattern, _ := theme.HandlePattern (theme.PatternState {
|
|
||||||
Case: scrollBarHorizontalCase,
|
|
||||||
Disabled: !element.horizontal.enabled,
|
Disabled: !element.horizontal.enabled,
|
||||||
Pressed: element.horizontal.dragging,
|
Pressed: element.horizontal.dragging,
|
||||||
})
|
}
|
||||||
|
gutterPattern := element.horizontal.theme.Pattern(theme.PatternGutter, state)
|
||||||
|
artist.FillRectangle(element, gutterPattern, element.horizontal.gutter)
|
||||||
|
|
||||||
|
handlePattern := element.horizontal.theme.Pattern(theme.PatternHandle, state)
|
||||||
artist.FillRectangle(element, handlePattern, element.horizontal.bar)
|
artist.FillRectangle(element, handlePattern, element.horizontal.bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *ScrollContainer) drawVerticalBar () {
|
func (element *ScrollContainer) drawVerticalBar () {
|
||||||
gutterPattern, _ := theme.GutterPattern (theme.PatternState {
|
state := theme.PatternState {
|
||||||
Case: scrollBarVerticalCase,
|
|
||||||
Disabled: !element.vertical.enabled,
|
|
||||||
})
|
|
||||||
artist.FillRectangle(element, gutterPattern, element.vertical.gutter)
|
|
||||||
|
|
||||||
handlePattern, _ := theme.HandlePattern (theme.PatternState {
|
|
||||||
Case: scrollBarVerticalCase,
|
|
||||||
Disabled: !element.vertical.enabled,
|
Disabled: !element.vertical.enabled,
|
||||||
Pressed: element.vertical.dragging,
|
Pressed: element.vertical.dragging,
|
||||||
})
|
}
|
||||||
|
gutterPattern := element.vertical.theme.Pattern(theme.PatternGutter, state)
|
||||||
|
artist.FillRectangle(element, gutterPattern, element.vertical.gutter)
|
||||||
|
|
||||||
|
handlePattern := element.vertical.theme.Pattern(theme.PatternHandle, state)
|
||||||
artist.FillRectangle(element, handlePattern, element.vertical.bar)
|
artist.FillRectangle(element, handlePattern, element.vertical.bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,19 +464,15 @@ func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *ScrollContainer) updateMinimumSize () {
|
func (element *ScrollContainer) updateMinimumSize () {
|
||||||
_, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState {
|
gutterInsetHorizontal := element.horizontal.theme.Inset(theme.PatternGutter)
|
||||||
Case: scrollBarHorizontalCase,
|
gutterInsetVertical := element.vertical.theme.Inset(theme.PatternGutter)
|
||||||
})
|
|
||||||
_, gutterInsetVertical := theme.GutterPattern(theme.PatternState {
|
|
||||||
Case: scrollBarHorizontalCase,
|
|
||||||
})
|
|
||||||
|
|
||||||
thicknessHorizontal :=
|
thicknessHorizontal :=
|
||||||
theme.HandleWidth() +
|
element.config.HandleWidth() +
|
||||||
gutterInsetHorizontal[3] +
|
gutterInsetHorizontal[3] +
|
||||||
gutterInsetHorizontal[1]
|
gutterInsetHorizontal[1]
|
||||||
thicknessVertical :=
|
thicknessVertical :=
|
||||||
theme.HandleWidth() +
|
element.config.HandleWidth() +
|
||||||
gutterInsetVertical[3] +
|
gutterInsetVertical[3] +
|
||||||
gutterInsetVertical[1]
|
gutterInsetVertical[1]
|
||||||
|
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
package basicElements
|
package basicElements
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var spacerCase = theme.C("basic", "spacer")
|
|
||||||
|
|
||||||
// Spacer can be used to put space between two elements..
|
// Spacer can be used to put space between two elements..
|
||||||
type Spacer struct {
|
type Spacer struct {
|
||||||
*core.Core
|
*core.Core
|
||||||
core core.CoreControl
|
core core.CoreControl
|
||||||
line bool
|
line bool
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSpacer creates a new spacer. If line is set to true, the spacer will be
|
// NewSpacer creates a new spacer. If line is set to true, the spacer will be
|
||||||
@ -18,6 +20,7 @@ type Spacer struct {
|
|||||||
// will appear as a line.
|
// will appear as a line.
|
||||||
func NewSpacer (line bool) (element *Spacer) {
|
func NewSpacer (line bool) (element *Spacer) {
|
||||||
element = &Spacer { line: line }
|
element = &Spacer { line: line }
|
||||||
|
element.theme.Case = theme.C("basic", "spacer")
|
||||||
element.Core, element.core = core.NewCore(element.draw)
|
element.Core, element.core = core.NewCore(element.draw)
|
||||||
element.core.SetMinimumSize(1, 1)
|
element.core.SetMinimumSize(1, 1)
|
||||||
return
|
return
|
||||||
@ -33,20 +36,39 @@ func (element *Spacer) SetLine (line bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Spacer) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *Spacer) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Spacer) redo () {
|
||||||
|
if !element.core.HasImage() {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (element *Spacer) draw () {
|
func (element *Spacer) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
|
|
||||||
if element.line {
|
if element.line {
|
||||||
pattern, _ := theme.ForegroundPattern(theme.PatternState {
|
pattern := element.theme.Pattern (
|
||||||
Case: spacerCase,
|
theme.PatternForeground,
|
||||||
Disabled: true,
|
theme.PatternState { })
|
||||||
})
|
|
||||||
artist.FillRectangle(element, pattern, bounds)
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
} else {
|
} else {
|
||||||
pattern, _ := theme.BackgroundPattern(theme.PatternState {
|
pattern := element.theme.Pattern (
|
||||||
Case: spacerCase,
|
theme.PatternBackground,
|
||||||
Disabled: true,
|
theme.PatternState { })
|
||||||
})
|
|
||||||
artist.FillRectangle(element, pattern, bounds)
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,10 @@ package basicElements
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var switchCase = theme.C("basic", "switch")
|
|
||||||
|
|
||||||
// Switch is a toggle-able on/off switch with an optional label. It is
|
// Switch is a toggle-able on/off switch with an optional label. It is
|
||||||
// functionally identical to Checkbox, but plays a different semantic role.
|
// functionally identical to Checkbox, but plays a different semantic role.
|
||||||
type Switch struct {
|
type Switch struct {
|
||||||
@ -21,23 +20,24 @@ type Switch struct {
|
|||||||
checked bool
|
checked bool
|
||||||
text string
|
text string
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
onToggle func ()
|
onToggle func ()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSwitch creates a new switch with the specified label text.
|
// NewSwitch creates a new switch with the specified label text.
|
||||||
func NewSwitch (text string, on bool) (element *Switch) {
|
func NewSwitch (text string, on bool) (element *Switch) {
|
||||||
element = &Switch { checked: on, text: text }
|
element = &Switch {
|
||||||
|
checked: on,
|
||||||
|
text: text,
|
||||||
|
}
|
||||||
|
element.theme.Case = theme.C("basic", "switch")
|
||||||
element.Core, element.core = core.NewCore(element.draw)
|
element.Core, element.core = core.NewCore(element.draw)
|
||||||
element.FocusableCore,
|
element.FocusableCore,
|
||||||
element.focusableControl = core.NewFocusableCore (func () {
|
element.focusableControl = core.NewFocusableCore(element.redo)
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
element.drawer.SetFace(theme.FontFaceRegular())
|
|
||||||
element.drawer.SetText([]rune(text))
|
element.drawer.SetText([]rune(text))
|
||||||
element.calculateMinimumSize()
|
element.updateMinimumSize()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,10 +45,7 @@ func (element *Switch) HandleMouseDown (x, y int, button input.Button) {
|
|||||||
if !element.Enabled() { return }
|
if !element.Enabled() { return }
|
||||||
element.Focus()
|
element.Focus()
|
||||||
element.pressed = true
|
element.pressed = true
|
||||||
if element.core.HasImage() {
|
element.redo()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Switch) HandleMouseUp (x, y int, button input.Button) {
|
func (element *Switch) HandleMouseUp (x, y int, button input.Button) {
|
||||||
@ -76,10 +73,7 @@ func (element *Switch) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
|||||||
func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||||
if key == input.KeyEnter {
|
if key == input.KeyEnter {
|
||||||
element.pressed = true
|
element.pressed = true
|
||||||
if element.core.HasImage() {
|
element.redo()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,10 +81,7 @@ func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
|||||||
if key == input.KeyEnter && element.pressed {
|
if key == input.KeyEnter && element.pressed {
|
||||||
element.pressed = false
|
element.pressed = false
|
||||||
element.checked = !element.checked
|
element.checked = !element.checked
|
||||||
if element.core.HasImage() {
|
element.redo()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
if element.onToggle != nil {
|
if element.onToggle != nil {
|
||||||
element.onToggle()
|
element.onToggle()
|
||||||
}
|
}
|
||||||
@ -118,15 +109,37 @@ func (element *Switch) SetText (text string) {
|
|||||||
|
|
||||||
element.text = text
|
element.text = text
|
||||||
element.drawer.SetText([]rune(text))
|
element.drawer.SetText([]rune(text))
|
||||||
element.calculateMinimumSize()
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Switch) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
element.drawer.SetFace (element.theme.FontFace (
|
||||||
|
theme.FontStyleRegular,
|
||||||
|
theme.FontSizeNormal))
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *Switch) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Switch) redo () {
|
||||||
if element.core.HasImage () {
|
if element.core.HasImage () {
|
||||||
element.draw()
|
element.draw()
|
||||||
element.core.DamageAll()
|
element.core.DamageAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Switch) calculateMinimumSize () {
|
func (element *Switch) updateMinimumSize () {
|
||||||
textBounds := element.drawer.LayoutBounds()
|
textBounds := element.drawer.LayoutBounds()
|
||||||
lineHeight := element.drawer.LineHeight().Round()
|
lineHeight := element.drawer.LineHeight().Round()
|
||||||
|
|
||||||
@ -134,7 +147,9 @@ func (element *Switch) calculateMinimumSize () {
|
|||||||
element.core.SetMinimumSize(lineHeight * 2, lineHeight)
|
element.core.SetMinimumSize(lineHeight * 2, lineHeight)
|
||||||
} else {
|
} else {
|
||||||
element.core.SetMinimumSize (
|
element.core.SetMinimumSize (
|
||||||
lineHeight * 2 + theme.Padding() + textBounds.Dx(),
|
lineHeight * 2 +
|
||||||
|
element.config.Padding() +
|
||||||
|
textBounds.Dx(),
|
||||||
lineHeight)
|
lineHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,9 +158,14 @@ func (element *Switch) draw () {
|
|||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
||||||
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
|
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
|
||||||
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState {
|
|
||||||
Case: switchCase,
|
state := theme.PatternState {
|
||||||
})
|
Disabled: !element.Enabled(),
|
||||||
|
Focused: element.Focused(),
|
||||||
|
Pressed: element.pressed,
|
||||||
|
}
|
||||||
|
backgroundPattern := element.theme.Pattern (
|
||||||
|
theme.PatternBackground, state)
|
||||||
artist.FillRectangle (element, backgroundPattern, bounds)
|
artist.FillRectangle (element, backgroundPattern, bounds)
|
||||||
|
|
||||||
if element.checked {
|
if element.checked {
|
||||||
@ -162,33 +182,23 @@ func (element *Switch) draw () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gutterPattern, _ := theme.GutterPattern(theme.PatternState {
|
gutterPattern := element.theme.Pattern (
|
||||||
Case: switchCase,
|
theme.PatternGutter, state)
|
||||||
Disabled: !element.Enabled(),
|
|
||||||
Focused: element.Focused(),
|
|
||||||
Pressed: element.pressed,
|
|
||||||
})
|
|
||||||
artist.FillRectangle(element, gutterPattern, gutterBounds)
|
artist.FillRectangle(element, gutterPattern, gutterBounds)
|
||||||
|
|
||||||
handlePattern, _ := theme.HandlePattern(theme.PatternState {
|
handlePattern := element.theme.Pattern (
|
||||||
Case: switchCase,
|
theme.PatternHandle, state)
|
||||||
Disabled: !element.Enabled(),
|
|
||||||
Focused: element.Focused(),
|
|
||||||
Pressed: element.pressed,
|
|
||||||
})
|
|
||||||
artist.FillRectangle(element, handlePattern, handleBounds)
|
artist.FillRectangle(element, handlePattern, handleBounds)
|
||||||
|
|
||||||
textBounds := element.drawer.LayoutBounds()
|
textBounds := element.drawer.LayoutBounds()
|
||||||
offset := bounds.Min.Add(image.Point {
|
offset := bounds.Min.Add(image.Point {
|
||||||
X: bounds.Dy() * 2 + theme.Padding(),
|
X: bounds.Dy() * 2 + element.config.Padding(),
|
||||||
})
|
})
|
||||||
|
|
||||||
offset.Y -= textBounds.Min.Y
|
offset.Y -= textBounds.Min.Y
|
||||||
offset.X -= textBounds.Min.X
|
offset.X -= textBounds.Min.X
|
||||||
|
|
||||||
foreground, _ := theme.ForegroundPattern (theme.PatternState {
|
foreground := element.theme.Pattern (
|
||||||
Case: switchCase,
|
theme.PatternForeground, state)
|
||||||
Disabled: !element.Enabled(),
|
|
||||||
})
|
|
||||||
element.drawer.Draw(element, foreground, offset)
|
element.drawer.Draw(element, foreground, offset)
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,11 @@ package basicElements
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
|
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var textBoxCase = theme.C("basic", "textBox")
|
|
||||||
|
|
||||||
// TextBox is a single-line text input.
|
// TextBox is a single-line text input.
|
||||||
type TextBox struct {
|
type TextBox struct {
|
||||||
*core.Core
|
*core.Core
|
||||||
@ -24,6 +23,9 @@ type TextBox struct {
|
|||||||
placeholderDrawer artist.TextDrawer
|
placeholderDrawer artist.TextDrawer
|
||||||
valueDrawer artist.TextDrawer
|
valueDrawer artist.TextDrawer
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
|
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
|
||||||
onChange func ()
|
onChange func ()
|
||||||
onScrollBoundsChange func ()
|
onScrollBoundsChange func ()
|
||||||
@ -34,6 +36,7 @@ type TextBox struct {
|
|||||||
// text.
|
// text.
|
||||||
func NewTextBox (placeholder, value string) (element *TextBox) {
|
func NewTextBox (placeholder, value string) (element *TextBox) {
|
||||||
element = &TextBox { }
|
element = &TextBox { }
|
||||||
|
element.theme.Case = theme.C("basic", "textBox")
|
||||||
element.Core, element.core = core.NewCore(element.handleResize)
|
element.Core, element.core = core.NewCore(element.handleResize)
|
||||||
element.FocusableCore,
|
element.FocusableCore,
|
||||||
element.focusableControl = core.NewFocusableCore (func () {
|
element.focusableControl = core.NewFocusableCore (func () {
|
||||||
@ -42,8 +45,6 @@ func NewTextBox (placeholder, value string) (element *TextBox) {
|
|||||||
element.core.DamageAll()
|
element.core.DamageAll()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
element.placeholderDrawer.SetFace(theme.FontFaceRegular())
|
|
||||||
element.valueDrawer.SetFace(theme.FontFaceRegular())
|
|
||||||
element.placeholder = placeholder
|
element.placeholder = placeholder
|
||||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
@ -130,9 +131,8 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers)
|
|||||||
element.onScrollBoundsChange()
|
element.onScrollBoundsChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
if altered && element.core.HasImage () {
|
if altered {
|
||||||
element.draw()
|
element.redo()
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,10 +145,7 @@ func (element *TextBox) SetPlaceholder (placeholder string) {
|
|||||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||||
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
if element.core.HasImage () {
|
element.redo()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) SetValue (text string) {
|
func (element *TextBox) SetValue (text string) {
|
||||||
@ -161,11 +158,7 @@ func (element *TextBox) SetValue (text string) {
|
|||||||
element.cursor = element.valueDrawer.Length()
|
element.cursor = element.valueDrawer.Length()
|
||||||
}
|
}
|
||||||
element.scrollToCursor()
|
element.scrollToCursor()
|
||||||
|
element.redo()
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) Value () (value string) {
|
func (element *TextBox) Value () (value string) {
|
||||||
@ -203,7 +196,7 @@ func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) scrollViewportWidth () (width int) {
|
func (element *TextBox) scrollViewportWidth () (width int) {
|
||||||
return element.Bounds().Inset(theme.Padding()).Dx()
|
return element.Bounds().Inset(element.config.Padding()).Dx()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScrollTo scrolls the viewport to the specified point relative to
|
// ScrollTo scrolls the viewport to the specified point relative to
|
||||||
@ -218,10 +211,7 @@ func (element *TextBox) ScrollTo (position image.Point) {
|
|||||||
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
|
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
|
||||||
if element.scroll > maxPosition { element.scroll = maxPosition }
|
if element.scroll > maxPosition { element.scroll = maxPosition }
|
||||||
|
|
||||||
if element.core.HasImage () {
|
element.redo()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
if element.onScrollBoundsChange != nil {
|
if element.onScrollBoundsChange != nil {
|
||||||
element.onScrollBoundsChange()
|
element.onScrollBoundsChange()
|
||||||
}
|
}
|
||||||
@ -236,18 +226,6 @@ func (element *TextBox) OnScrollBoundsChange (callback func ()) {
|
|||||||
element.onScrollBoundsChange = callback
|
element.onScrollBoundsChange = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) updateMinimumSize () {
|
|
||||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
|
||||||
_, inset := theme.InputPattern(theme.PatternState {
|
|
||||||
Case: textBoxCase,
|
|
||||||
})
|
|
||||||
element.core.SetMinimumSize (
|
|
||||||
textBounds.Dx() +
|
|
||||||
theme.Padding() * 2 + inset[3] + inset[1],
|
|
||||||
element.placeholderDrawer.LineHeight().Round() +
|
|
||||||
theme.Padding() * 2 + inset[0] + inset[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *TextBox) runOnChange () {
|
func (element *TextBox) runOnChange () {
|
||||||
if element.onChange != nil {
|
if element.onChange != nil {
|
||||||
element.onChange()
|
element.onChange()
|
||||||
@ -257,7 +235,7 @@ func (element *TextBox) runOnChange () {
|
|||||||
func (element *TextBox) scrollToCursor () {
|
func (element *TextBox) scrollToCursor () {
|
||||||
if !element.core.HasImage() { return }
|
if !element.core.HasImage() { return }
|
||||||
|
|
||||||
bounds := element.Bounds().Inset(theme.Padding())
|
bounds := element.Bounds().Inset(element.config.Padding())
|
||||||
bounds = bounds.Sub(bounds.Min)
|
bounds = bounds.Sub(bounds.Min)
|
||||||
bounds.Max.X -= element.valueDrawer.Em().Round()
|
bounds.Max.X -= element.valueDrawer.Em().Round()
|
||||||
cursorPosition := element.valueDrawer.PositionOf(element.cursor)
|
cursorPosition := element.valueDrawer.PositionOf(element.cursor)
|
||||||
@ -272,28 +250,64 @@ func (element *TextBox) scrollToCursor () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *TextBox) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
face := element.theme.FontFace (
|
||||||
|
theme.FontStyleRegular,
|
||||||
|
theme.FontSizeNormal)
|
||||||
|
element.placeholderDrawer.SetFace(face)
|
||||||
|
element.valueDrawer.SetFace(face)
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *TextBox) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *TextBox) updateMinimumSize () {
|
||||||
|
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||||
|
element.core.SetMinimumSize (
|
||||||
|
textBounds.Dx() +
|
||||||
|
element.config.Padding() * 2,
|
||||||
|
element.placeholderDrawer.LineHeight().Round() +
|
||||||
|
element.config.Padding() * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *TextBox) redo () {
|
||||||
|
if element.core.HasImage () {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (element *TextBox) draw () {
|
func (element *TextBox) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
|
|
||||||
// FIXME: take index into account
|
// FIXME: take index into account
|
||||||
pattern, inset := theme.InputPattern(theme.PatternState {
|
state := theme.PatternState {
|
||||||
Case: textBoxCase,
|
|
||||||
Disabled: !element.Enabled(),
|
Disabled: !element.Enabled(),
|
||||||
Focused: element.Focused(),
|
Focused: element.Focused(),
|
||||||
})
|
}
|
||||||
|
pattern := element.theme.Pattern(theme.PatternSunken, state)
|
||||||
artist.FillRectangle(element, pattern, bounds)
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
|
|
||||||
if len(element.text) == 0 && !element.Focused() {
|
if len(element.text) == 0 && !element.Focused() {
|
||||||
// draw placeholder
|
// draw placeholder
|
||||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||||
offset := bounds.Min.Add (image.Point {
|
offset := bounds.Min.Add (image.Point {
|
||||||
X: theme.Padding() + inset[3],
|
X: element.config.Padding(),
|
||||||
Y: theme.Padding() + inset[0],
|
Y: element.config.Padding(),
|
||||||
})
|
|
||||||
foreground, _ := theme.ForegroundPattern(theme.PatternState {
|
|
||||||
Case: textBoxCase,
|
|
||||||
Disabled: true,
|
|
||||||
})
|
})
|
||||||
|
foreground := element.theme.Pattern (
|
||||||
|
theme.PatternForeground,
|
||||||
|
theme.PatternState { Disabled: true })
|
||||||
element.placeholderDrawer.Draw (
|
element.placeholderDrawer.Draw (
|
||||||
element,
|
element,
|
||||||
foreground,
|
foreground,
|
||||||
@ -302,13 +316,11 @@ func (element *TextBox) draw () {
|
|||||||
// draw input value
|
// draw input value
|
||||||
textBounds := element.valueDrawer.LayoutBounds()
|
textBounds := element.valueDrawer.LayoutBounds()
|
||||||
offset := bounds.Min.Add (image.Point {
|
offset := bounds.Min.Add (image.Point {
|
||||||
X: theme.Padding() + inset[3] - element.scroll,
|
X: element.config.Padding() - element.scroll,
|
||||||
Y: theme.Padding() + inset[0],
|
Y: element.config.Padding(),
|
||||||
})
|
|
||||||
foreground, _ := theme.ForegroundPattern(theme.PatternState {
|
|
||||||
Case: textBoxCase,
|
|
||||||
Disabled: !element.Enabled(),
|
|
||||||
})
|
})
|
||||||
|
foreground := element.theme.Pattern (
|
||||||
|
theme.PatternForeground, state)
|
||||||
element.valueDrawer.Draw (
|
element.valueDrawer.Draw (
|
||||||
element,
|
element,
|
||||||
foreground,
|
foreground,
|
||||||
@ -318,9 +330,6 @@ func (element *TextBox) draw () {
|
|||||||
// cursor
|
// cursor
|
||||||
cursorPosition := element.valueDrawer.PositionOf (
|
cursorPosition := element.valueDrawer.PositionOf (
|
||||||
element.cursor)
|
element.cursor)
|
||||||
foreground, _ := theme.ForegroundPattern(theme.PatternState {
|
|
||||||
Case: textBoxCase,
|
|
||||||
})
|
|
||||||
artist.Line (
|
artist.Line (
|
||||||
element,
|
element,
|
||||||
foreground, 1,
|
foreground, 1,
|
||||||
|
@ -20,8 +20,15 @@ type Core struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCore creates a new element core and its corresponding control.
|
// NewCore creates a new element core and its corresponding control.
|
||||||
func NewCore (drawSizeChange func ()) (core *Core, control CoreControl) {
|
func NewCore (
|
||||||
core = &Core { drawSizeChange: drawSizeChange }
|
drawSizeChange func (),
|
||||||
|
) (
|
||||||
|
core *Core,
|
||||||
|
control CoreControl,
|
||||||
|
) {
|
||||||
|
core = &Core {
|
||||||
|
drawSizeChange: drawSizeChange,
|
||||||
|
}
|
||||||
control = CoreControl { core: core }
|
control = CoreControl { core: core }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ package elements
|
|||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
|
|
||||||
// Element represents a basic on-screen object.
|
// Element represents a basic on-screen object.
|
||||||
type Element interface {
|
type Element interface {
|
||||||
@ -37,7 +39,8 @@ type Element interface {
|
|||||||
type Focusable interface {
|
type Focusable interface {
|
||||||
Element
|
Element
|
||||||
|
|
||||||
// Focused returns whether or not this element is currently focused.
|
// Focused returns whether or not this element or any of its children
|
||||||
|
// are currently focused.
|
||||||
Focused () (selected bool)
|
Focused () (selected bool)
|
||||||
|
|
||||||
// Focus focuses this element, if its parent element grants the
|
// Focus focuses this element, if its parent element grants the
|
||||||
@ -157,3 +160,23 @@ type Scrollable interface {
|
|||||||
// ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed.
|
// ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed.
|
||||||
OnScrollBoundsChange (callback func ())
|
OnScrollBoundsChange (callback func ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Themeable represents an element that can modify its appearance to fit within
|
||||||
|
// a theme.
|
||||||
|
type Themeable interface {
|
||||||
|
Element
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme to something fulfilling the
|
||||||
|
// theme.Theme interface.
|
||||||
|
SetTheme (theme.Theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurable represents an element that can modify its behavior to fit within
|
||||||
|
// a set of configuration parameters.
|
||||||
|
type Configurable interface {
|
||||||
|
Element
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration to something fulfilling
|
||||||
|
// the config.Config interface.
|
||||||
|
SetConfig (config.Config)
|
||||||
|
}
|
||||||
|
@ -4,21 +4,24 @@ import "time"
|
|||||||
import "math"
|
import "math"
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
var clockCase = theme.C("fun", "clock")
|
|
||||||
|
|
||||||
// AnalogClock can display the time of day in an analog format.
|
// AnalogClock can display the time of day in an analog format.
|
||||||
type AnalogClock struct {
|
type AnalogClock struct {
|
||||||
*core.Core
|
*core.Core
|
||||||
core core.CoreControl
|
core core.CoreControl
|
||||||
time time.Time
|
time time.Time
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAnalogClock creates a new analog clock that displays the specified time.
|
// NewAnalogClock creates a new analog clock that displays the specified time.
|
||||||
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
|
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
|
||||||
element = &AnalogClock { }
|
element = &AnalogClock { }
|
||||||
|
element.theme.Case = theme.C("fun", "clock")
|
||||||
element.Core, element.core = core.NewCore(element.draw)
|
element.Core, element.core = core.NewCore(element.draw)
|
||||||
element.core.SetMinimumSize(64, 64)
|
element.core.SetMinimumSize(64, 64)
|
||||||
return
|
return
|
||||||
@ -28,6 +31,24 @@ func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
|
|||||||
func (element *AnalogClock) SetTime (newTime time.Time) {
|
func (element *AnalogClock) SetTime (newTime time.Time) {
|
||||||
if newTime == element.time { return }
|
if newTime == element.time { return }
|
||||||
element.time = newTime
|
element.time = newTime
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *AnalogClock) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *AnalogClock) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *AnalogClock) redo () {
|
||||||
if element.core.HasImage() {
|
if element.core.HasImage() {
|
||||||
element.draw()
|
element.draw()
|
||||||
element.core.DamageAll()
|
element.core.DamageAll()
|
||||||
@ -37,19 +58,15 @@ func (element *AnalogClock) SetTime (newTime time.Time) {
|
|||||||
func (element *AnalogClock) draw () {
|
func (element *AnalogClock) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
|
|
||||||
pattern, inset := theme.SunkenPattern(theme.PatternState {
|
state := theme.PatternState { }
|
||||||
Case: clockCase,
|
pattern := element.theme.Pattern(theme.PatternSunken, state)
|
||||||
})
|
inset := element.theme.Inset(theme.PatternSunken)
|
||||||
artist.FillRectangle(element, pattern, bounds)
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
|
|
||||||
bounds = inset.Apply(bounds)
|
bounds = inset.Apply(bounds)
|
||||||
|
|
||||||
foreground, _ := theme.ForegroundPattern(theme.PatternState {
|
foreground := element.theme.Pattern(theme.PatternForeground, state)
|
||||||
Case: clockCase,
|
accent := element.theme.Pattern(theme.PatternAccent, state)
|
||||||
})
|
|
||||||
accent, _ := theme.AccentPattern(theme.PatternState {
|
|
||||||
Case: clockCase,
|
|
||||||
})
|
|
||||||
|
|
||||||
for hour := 0; hour < 12; hour ++ {
|
for hour := 0; hour < 12; hour ++ {
|
||||||
element.radialLine (
|
element.radialLine (
|
||||||
@ -71,7 +88,7 @@ func (element *AnalogClock) FlexibleHeightFor (width int) (height int) {
|
|||||||
return width
|
return width
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnFlexibleHeightChange sets a function to be calle dwhen the parameters
|
// OnFlexibleHeightChange sets a function to be called when the parameters
|
||||||
// affecting the clock's flexible height change.
|
// affecting the clock's flexible height change.
|
||||||
func (element *AnalogClock) OnFlexibleHeightChange (func ()) { }
|
func (element *AnalogClock) OnFlexibleHeightChange (func ()) { }
|
||||||
|
|
||||||
|
244
elements/fun/piano.go
Normal file
244
elements/fun/piano.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package fun
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
|
// Octave represents a MIDI octave.
|
||||||
|
type Octave int
|
||||||
|
|
||||||
|
// Note returns the note at the specified scale degree in the chromatic scale.
|
||||||
|
func (octave Octave) Note (degree int) Note {
|
||||||
|
return Note(int(octave + 1) * 12 + degree)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note represents a MIDI note.
|
||||||
|
type Note int
|
||||||
|
|
||||||
|
// Octave returns the octave of the note
|
||||||
|
func (note Note) Octave () int {
|
||||||
|
return int(note / 12 - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Degree returns the scale degree of the note in the chromatic scale.
|
||||||
|
func (note Note) Degree () int {
|
||||||
|
mod := note % 12
|
||||||
|
if mod < 0 { mod += 12 }
|
||||||
|
return int(mod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSharp returns whether or not the note is a sharp.
|
||||||
|
func (note Note) IsSharp () bool {
|
||||||
|
degree := note.Degree()
|
||||||
|
return degree == 1 ||
|
||||||
|
degree == 3 ||
|
||||||
|
degree == 6 ||
|
||||||
|
degree == 8 ||
|
||||||
|
degree == 10
|
||||||
|
}
|
||||||
|
|
||||||
|
const pianoKeyWidth = 18
|
||||||
|
|
||||||
|
type pianoKey struct {
|
||||||
|
image.Rectangle
|
||||||
|
Note
|
||||||
|
}
|
||||||
|
|
||||||
|
type Piano struct {
|
||||||
|
*core.Core
|
||||||
|
core core.CoreControl
|
||||||
|
low, high Octave
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
|
flatKeys []pianoKey
|
||||||
|
sharpKeys []pianoKey
|
||||||
|
|
||||||
|
pressed *pianoKey
|
||||||
|
|
||||||
|
onPress func (Note)
|
||||||
|
onRelease func (Note)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPiano (low, high Octave) (element *Piano) {
|
||||||
|
element = &Piano {
|
||||||
|
low: low,
|
||||||
|
high: high,
|
||||||
|
}
|
||||||
|
element.theme.Case = theme.C("fun", "piano")
|
||||||
|
element.Core, element.core = core.NewCore (func () {
|
||||||
|
element.recalculate()
|
||||||
|
element.draw()
|
||||||
|
})
|
||||||
|
element.updateMinimumSize()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPress sets a function to be called when a key is pressed.
|
||||||
|
func (element *Piano) OnPress (callback func (note Note)) {
|
||||||
|
element.onPress = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRelease sets a function to be called when a key is released.
|
||||||
|
func (element *Piano) OnRelease (callback func (note Note)) {
|
||||||
|
element.onRelease = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) HandleMouseDown (x, y int, button input.Button) {
|
||||||
|
if button != input.ButtonLeft { return }
|
||||||
|
element.pressUnderMouseCursor(image.Pt(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) HandleMouseUp (x, y int, button input.Button) {
|
||||||
|
if button != input.ButtonLeft { return }
|
||||||
|
if element.onRelease != nil {
|
||||||
|
element.onRelease((*element.pressed).Note)
|
||||||
|
}
|
||||||
|
element.pressed = nil
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) HandleMouseMove (x, y int) {
|
||||||
|
if element.pressed == nil { return }
|
||||||
|
element.pressUnderMouseCursor(image.Pt(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||||
|
|
||||||
|
func (element *Piano) pressUnderMouseCursor (point image.Point) {
|
||||||
|
// release previous note
|
||||||
|
if element.pressed != nil && element.onRelease != nil {
|
||||||
|
element.onRelease((*element.pressed).Note)
|
||||||
|
}
|
||||||
|
|
||||||
|
// find out which note is being pressed
|
||||||
|
newKey := (*pianoKey)(nil)
|
||||||
|
for index, key := range element.flatKeys {
|
||||||
|
if point.In(key.Rectangle) {
|
||||||
|
newKey = &element.flatKeys[index]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index, key := range element.sharpKeys {
|
||||||
|
if point.In(key.Rectangle) {
|
||||||
|
newKey = &element.sharpKeys[index]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if newKey == nil { return }
|
||||||
|
|
||||||
|
if newKey != element.pressed {
|
||||||
|
// press new note
|
||||||
|
element.pressed = newKey
|
||||||
|
if element.onPress != nil {
|
||||||
|
element.onPress((*element.pressed).Note)
|
||||||
|
}
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Piano) SetTheme (new theme.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.recalculate()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *Piano) SetConfig (new config.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.recalculate()
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) updateMinimumSize () {
|
||||||
|
element.core.SetMinimumSize (
|
||||||
|
pianoKeyWidth * 7 * element.countOctaves(), 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) countOctaves () int {
|
||||||
|
return int(element.high - element.low + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) countFlats () int {
|
||||||
|
return element.countOctaves() * 8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) countSharps () int {
|
||||||
|
return element.countOctaves() * 5
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) redo () {
|
||||||
|
if element.core.HasImage() {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) recalculate () {
|
||||||
|
element.flatKeys = make([]pianoKey, element.countFlats())
|
||||||
|
element.sharpKeys = make([]pianoKey, element.countSharps())
|
||||||
|
|
||||||
|
bounds := element.Bounds()
|
||||||
|
dot := bounds.Min
|
||||||
|
note := element.low.Note(0)
|
||||||
|
limit := element.high.Note(12)
|
||||||
|
flatIndex := 0
|
||||||
|
sharpIndex := 0
|
||||||
|
for note < limit {
|
||||||
|
if note.IsSharp() {
|
||||||
|
element.sharpKeys[sharpIndex].Rectangle = image.Rect (
|
||||||
|
-(pianoKeyWidth * 3) / 7, 0,
|
||||||
|
(pianoKeyWidth * 3) / 7,
|
||||||
|
bounds.Dy() / 2).Add(dot)
|
||||||
|
element.sharpKeys[sharpIndex].Note = note
|
||||||
|
sharpIndex ++
|
||||||
|
} else {
|
||||||
|
element.flatKeys[flatIndex].Rectangle = image.Rect (
|
||||||
|
0, 0, pianoKeyWidth, bounds.Dy()).Add(dot)
|
||||||
|
dot.X += pianoKeyWidth
|
||||||
|
element.flatKeys[flatIndex].Note = note
|
||||||
|
flatIndex ++
|
||||||
|
}
|
||||||
|
note ++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) draw () {
|
||||||
|
for _, key := range element.flatKeys {
|
||||||
|
element.drawFlat (
|
||||||
|
key.Rectangle,
|
||||||
|
element.pressed != nil &&
|
||||||
|
(*element.pressed).Note == key.Note)
|
||||||
|
}
|
||||||
|
for _, key := range element.sharpKeys {
|
||||||
|
element.drawSharp (
|
||||||
|
key.Rectangle,
|
||||||
|
element.pressed != nil &&
|
||||||
|
(*element.pressed).Note == key.Note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) drawFlat (bounds image.Rectangle, pressed bool) {
|
||||||
|
state := theme.PatternState {
|
||||||
|
Pressed: pressed,
|
||||||
|
}
|
||||||
|
pattern := element.theme.Pattern(theme.PatternButton, state)
|
||||||
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Piano) drawSharp (bounds image.Rectangle, pressed bool) {
|
||||||
|
state := theme.PatternState {
|
||||||
|
Pressed: pressed,
|
||||||
|
}
|
||||||
|
pattern := element.theme.Pattern(theme.PatternButton, state)
|
||||||
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
|
}
|
@ -26,6 +26,8 @@ func NewArtist () (element *Artist) {
|
|||||||
|
|
||||||
func (element *Artist) draw () {
|
func (element *Artist) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
|
artist.FillRectangle(element, artist.NewUniform(hex(0)), bounds)
|
||||||
|
|
||||||
element.cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
|
element.cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
|
||||||
element.cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 8
|
element.cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 8
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package testing
|
|||||||
import "image"
|
import "image"
|
||||||
import "image/color"
|
import "image/color"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
@ -15,20 +16,45 @@ type Mouse struct {
|
|||||||
drawing bool
|
drawing bool
|
||||||
color artist.Pattern
|
color artist.Pattern
|
||||||
lastMousePos image.Point
|
lastMousePos image.Point
|
||||||
|
|
||||||
|
config config.Config
|
||||||
|
theme theme.Theme
|
||||||
|
c theme.Case
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMouse creates a new mouse test element.
|
// NewMouse creates a new mouse test element.
|
||||||
func NewMouse () (element *Mouse) {
|
func NewMouse () (element *Mouse) {
|
||||||
element = &Mouse { }
|
element = &Mouse { c: theme.C("testing", "mouse") }
|
||||||
element.Core, element.core = core.NewCore(element.draw)
|
element.Core, element.core = core.NewCore(element.draw)
|
||||||
element.core.SetMinimumSize(32, 32)
|
element.core.SetMinimumSize(32, 32)
|
||||||
element.color = artist.NewUniform(color.Black)
|
element.color = artist.NewUniform(color.Black)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Mouse) SetTheme (new theme.Theme) {
|
||||||
|
element.theme = new
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *Mouse) SetConfig (new config.Config) {
|
||||||
|
element.config = new
|
||||||
|
element.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Mouse) redo () {
|
||||||
|
if !element.core.HasImage() { return }
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
|
||||||
func (element *Mouse) draw () {
|
func (element *Mouse) draw () {
|
||||||
bounds := element.Bounds()
|
bounds := element.Bounds()
|
||||||
pattern, _ := theme.AccentPattern(theme.PatternState { })
|
pattern := element.theme.Pattern (
|
||||||
|
theme.PatternAccent,
|
||||||
|
element.c,
|
||||||
|
theme.PatternState { })
|
||||||
artist.FillRectangle(element, pattern, bounds)
|
artist.FillRectangle(element, pattern, bounds)
|
||||||
artist.StrokeRectangle (
|
artist.StrokeRectangle (
|
||||||
element,
|
element,
|
||||||
|
@ -15,7 +15,7 @@ func main () {
|
|||||||
|
|
||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(2, 2)
|
window, _ := tomo.NewWindow(2, 2)
|
||||||
window.SetTitle("clock")
|
window.SetTitle("Clock")
|
||||||
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
|
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
|
26
examples/piano/main.go
Normal file
26
examples/piano/main.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||||
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||||
|
|
||||||
|
func main () {
|
||||||
|
tomo.Run(run)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run () {
|
||||||
|
window, _ := tomo.NewWindow(2, 2)
|
||||||
|
window.SetTitle("Piano")
|
||||||
|
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
|
||||||
|
window.Adopt(container)
|
||||||
|
|
||||||
|
label := basicElements.NewLabel("Play a song!", false)
|
||||||
|
container.Adopt(label, false)
|
||||||
|
piano := fun.NewPiano(3, 5)
|
||||||
|
container.Adopt(piano, true)
|
||||||
|
|
||||||
|
window.OnClose(tomo.Stop)
|
||||||
|
window.Show()
|
||||||
|
}
|
@ -1,48 +0,0 @@
|
|||||||
package theme
|
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
|
||||||
|
|
||||||
var buttonPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0xCCD5D2FF)),
|
|
||||||
artist.NewUniform(hex(0x4B5B59FF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
|
||||||
var selectedButtonPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0xCCD5D2FF)),
|
|
||||||
artist.NewUniform(hex(0x4B5B59FF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
|
||||||
var pressedButtonPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0x4B5B59FF)),
|
|
||||||
artist.NewUniform(hex(0x8D9894FF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
|
||||||
var pressedSelectedButtonPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0x4B5B59FF)),
|
|
||||||
artist.NewUniform(hex(0x8D9894FF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
|
||||||
var disabledButtonPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: weakForegroundPattern },
|
|
||||||
artist.Stroke { Pattern: backgroundPattern })
|
|
179
theme/default.go
Normal file
179
theme/default.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package theme
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "golang.org/x/image/font"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
|
||||||
|
|
||||||
|
// Default is the default theme.
|
||||||
|
type Default struct { }
|
||||||
|
|
||||||
|
// FontFace returns the default font face.
|
||||||
|
func (Default) FontFace (style FontStyle, size FontSize, c Case) font.Face {
|
||||||
|
switch style {
|
||||||
|
case FontStyleBold:
|
||||||
|
return defaultfont.FaceBold
|
||||||
|
case FontStyleItalic:
|
||||||
|
return defaultfont.FaceItalic
|
||||||
|
case FontStyleBoldItalic:
|
||||||
|
return defaultfont.FaceBoldItalic
|
||||||
|
default:
|
||||||
|
return defaultfont.FaceRegular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon returns an icon from the default set corresponding to the given name.
|
||||||
|
func (Default) Icon (string, Case) artist.Pattern {
|
||||||
|
// TODO
|
||||||
|
return uhex(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern returns a pattern from the default theme corresponding to the given
|
||||||
|
// pattern ID.
|
||||||
|
func (Default) Pattern (
|
||||||
|
pattern Pattern,
|
||||||
|
c Case,
|
||||||
|
state PatternState,
|
||||||
|
) artist.Pattern {
|
||||||
|
switch pattern {
|
||||||
|
case PatternAccent:
|
||||||
|
return accentPattern
|
||||||
|
case PatternBackground:
|
||||||
|
return backgroundPattern
|
||||||
|
case PatternForeground:
|
||||||
|
if state.Disabled || c == C("basic", "spacer") {
|
||||||
|
return weakForegroundPattern
|
||||||
|
} else {
|
||||||
|
return foregroundPattern
|
||||||
|
}
|
||||||
|
case PatternDead:
|
||||||
|
return deadPattern
|
||||||
|
case PatternRaised:
|
||||||
|
if c == C("basic", "listEntry") {
|
||||||
|
if state.Focused {
|
||||||
|
if state.On {
|
||||||
|
return focusedOnListEntryPattern
|
||||||
|
} else {
|
||||||
|
return focusedListEntryPattern
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if state.On {
|
||||||
|
return onListEntryPattern
|
||||||
|
} else {
|
||||||
|
return listEntryPattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if state.Focused {
|
||||||
|
return selectedRaisedPattern
|
||||||
|
} else {
|
||||||
|
return raisedPattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PatternSunken:
|
||||||
|
if c == C("basic", "list") {
|
||||||
|
if state.Focused {
|
||||||
|
return focusedListPattern
|
||||||
|
} else {
|
||||||
|
return listPattern
|
||||||
|
}
|
||||||
|
} else if c == C("basic", "textBox") {
|
||||||
|
if state.Disabled {
|
||||||
|
return disabledInputPattern
|
||||||
|
} else {
|
||||||
|
if state.Focused {
|
||||||
|
return selectedInputPattern
|
||||||
|
} else {
|
||||||
|
return inputPattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return sunkenPattern
|
||||||
|
}
|
||||||
|
case PatternPinboard:
|
||||||
|
return texturedSunkenPattern
|
||||||
|
case PatternButton:
|
||||||
|
if state.Disabled {
|
||||||
|
return disabledButtonPattern
|
||||||
|
} else {
|
||||||
|
if state.Pressed || state.On && c == C("basic", "checkbox") {
|
||||||
|
if state.Focused {
|
||||||
|
return pressedSelectedButtonPattern
|
||||||
|
} else {
|
||||||
|
return pressedButtonPattern
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if state.Focused {
|
||||||
|
return selectedButtonPattern
|
||||||
|
} else {
|
||||||
|
return buttonPattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PatternInput:
|
||||||
|
if state.Disabled {
|
||||||
|
return disabledInputPattern
|
||||||
|
} else {
|
||||||
|
if state.Focused {
|
||||||
|
return selectedInputPattern
|
||||||
|
} else {
|
||||||
|
return inputPattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PatternGutter:
|
||||||
|
if state.Disabled {
|
||||||
|
return disabledScrollGutterPattern
|
||||||
|
} else {
|
||||||
|
return scrollGutterPattern
|
||||||
|
}
|
||||||
|
case PatternHandle:
|
||||||
|
if state.Disabled {
|
||||||
|
return disabledScrollBarPattern
|
||||||
|
} else {
|
||||||
|
if state.Focused {
|
||||||
|
if state.Pressed {
|
||||||
|
return pressedSelectedScrollBarPattern
|
||||||
|
} else {
|
||||||
|
return selectedScrollBarPattern
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if state.Pressed {
|
||||||
|
return pressedScrollBarPattern
|
||||||
|
} else {
|
||||||
|
return scrollBarPattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return uhex(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inset returns the default inset value for the given pattern.
|
||||||
|
func (Default) Inset (pattern Pattern, c Case) Inset {
|
||||||
|
switch pattern {
|
||||||
|
case PatternRaised:
|
||||||
|
if c == C("basic", "listEntry") {
|
||||||
|
return Inset { 4, 6, 4, 6 }
|
||||||
|
} else {
|
||||||
|
return Inset { 1, 1, 1, 1 }
|
||||||
|
}
|
||||||
|
case PatternSunken:
|
||||||
|
if c == C("basic", "list") {
|
||||||
|
return Inset { 2, 1, 2, 1 }
|
||||||
|
} else if c == C("basic", "progressBar") {
|
||||||
|
return Inset { 2, 1, 1, 2 }
|
||||||
|
} else {
|
||||||
|
return Inset { 1, 1, 1, 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
case PatternInput, PatternButton, PatternHandle, PatternPinboard:
|
||||||
|
return Inset { 1, 1, 1, 1}
|
||||||
|
default: return Inset { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sink returns the default sink vector for the given pattern.
|
||||||
|
func (Default) Sink (pattern Pattern, c Case) image.Point {
|
||||||
|
return image.Point { 1, 1 }
|
||||||
|
}
|
237
theme/defaultpatterns.go
Normal file
237
theme/defaultpatterns.go
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
package theme
|
||||||
|
|
||||||
|
import "image/color"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
|
||||||
|
var accentPattern = artist.NewUniform(hex(0x408090FF))
|
||||||
|
var backgroundPattern = artist.NewUniform(color.Gray16 { 0xAAAA })
|
||||||
|
var foregroundPattern = artist.NewUniform(color.Gray16 { 0x0000 })
|
||||||
|
var weakForegroundPattern = artist.NewUniform(color.Gray16 { 0x4444 })
|
||||||
|
var strokePattern = artist.NewUniform(color.Gray16 { 0x0000 })
|
||||||
|
|
||||||
|
var sunkenPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0x3b534eFF)),
|
||||||
|
artist.NewUniform(hex(0x97a09cFF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) })
|
||||||
|
|
||||||
|
var texturedSunkenPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0x3b534eFF)),
|
||||||
|
artist.NewUniform(hex(0x97a09cFF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// artist.Stroke { Pattern: artist.Striped {
|
||||||
|
// First: artist.Stroke {
|
||||||
|
// Weight: 2,
|
||||||
|
// Pattern: artist.NewUniform(hex(0x97a09cFF)),
|
||||||
|
// },
|
||||||
|
// Second: artist.Stroke {
|
||||||
|
// Weight: 1,
|
||||||
|
// Pattern: artist.NewUniform(hex(0x6e8079FF)),
|
||||||
|
// },
|
||||||
|
// }})
|
||||||
|
|
||||||
|
artist.Stroke { Pattern: artist.Noisy {
|
||||||
|
Low: artist.NewUniform(hex(0x97a09cFF)),
|
||||||
|
High: artist.NewUniform(hex(0x6e8079FF)),
|
||||||
|
}})
|
||||||
|
|
||||||
|
var raisedPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0xDBDBDBFF)),
|
||||||
|
artist.NewUniform(hex(0x383C3AFF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) })
|
||||||
|
|
||||||
|
var selectedRaisedPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0xDBDBDBFF)),
|
||||||
|
artist.NewUniform(hex(0x383C3AFF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) })
|
||||||
|
|
||||||
|
var deadPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) })
|
||||||
|
|
||||||
|
var buttonPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0xCCD5D2FF)),
|
||||||
|
artist.NewUniform(hex(0x4B5B59FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
||||||
|
var selectedButtonPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0xCCD5D2FF)),
|
||||||
|
artist.NewUniform(hex(0x4B5B59FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
||||||
|
|
||||||
|
var pressedButtonPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0x4B5B59FF)),
|
||||||
|
artist.NewUniform(hex(0x8D9894FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
||||||
|
var pressedSelectedButtonPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0x4B5B59FF)),
|
||||||
|
artist.NewUniform(hex(0x8D9894FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
||||||
|
var disabledButtonPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: weakForegroundPattern },
|
||||||
|
artist.Stroke { Pattern: backgroundPattern })
|
||||||
|
|
||||||
|
var inputPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0x89925AFF)),
|
||||||
|
artist.NewUniform(hex(0xD2CB9AFF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) })
|
||||||
|
var selectedInputPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) })
|
||||||
|
var disabledInputPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: weakForegroundPattern },
|
||||||
|
artist.Stroke { Pattern: backgroundPattern })
|
||||||
|
|
||||||
|
var listPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
uhex(0x383C3AFF),
|
||||||
|
uhex(0x999C99FF),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: uhex(0x999C99FF) })
|
||||||
|
|
||||||
|
var focusedListPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
||||||
|
artist.Stroke { Pattern: uhex(0x999C99FF) })
|
||||||
|
|
||||||
|
var listEntryPattern = artist.Padded {
|
||||||
|
Stroke: uhex(0x383C3AFF),
|
||||||
|
Fill: uhex(0x999C99FF),
|
||||||
|
Sides: []int { 0, 0, 0, 1 },
|
||||||
|
}
|
||||||
|
|
||||||
|
var onListEntryPattern = artist.Padded {
|
||||||
|
Stroke: uhex(0x383C3AFF),
|
||||||
|
Fill: uhex(0x6e8079FF),
|
||||||
|
Sides: []int { 0, 0, 0, 1 },
|
||||||
|
}
|
||||||
|
|
||||||
|
var focusedListEntryPattern = artist.Padded {
|
||||||
|
Stroke: accentPattern,
|
||||||
|
Fill: uhex(0x999C99FF),
|
||||||
|
Sides: []int { 0, 1, 0, 1 },
|
||||||
|
}
|
||||||
|
|
||||||
|
var focusedOnListEntryPattern = artist.Padded {
|
||||||
|
Stroke: accentPattern,
|
||||||
|
Fill: uhex(0x6e8079FF),
|
||||||
|
Sides: []int { 0, 1, 0, 1 },
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollGutterPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0x3b534eFF)),
|
||||||
|
artist.NewUniform(hex(0x6e8079FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x6e8079FF)) })
|
||||||
|
var disabledScrollGutterPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: weakForegroundPattern },
|
||||||
|
artist.Stroke { Pattern: backgroundPattern })
|
||||||
|
var scrollBarPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0xCCD5D2FF)),
|
||||||
|
artist.NewUniform(hex(0x4B5B59FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
||||||
|
var selectedScrollBarPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0xCCD5D2FF)),
|
||||||
|
artist.NewUniform(hex(0x4B5B59FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
||||||
|
var pressedScrollBarPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0xCCD5D2FF)),
|
||||||
|
artist.NewUniform(hex(0x4B5B59FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Weight: 1, Pattern: artist.NewUniform(hex(0x8D9894FF)) },
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) })
|
||||||
|
var pressedSelectedScrollBarPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
artist.NewUniform(hex(0xCCD5D2FF)),
|
||||||
|
artist.NewUniform(hex(0x4B5B59FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) })
|
||||||
|
var disabledScrollBarPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: weakForegroundPattern },
|
||||||
|
artist.Stroke { Pattern: backgroundPattern })
|
@ -1,21 +0,0 @@
|
|||||||
package theme
|
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
|
||||||
|
|
||||||
var inputPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0x89925AFF)),
|
|
||||||
artist.NewUniform(hex(0xD2CB9AFF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) })
|
|
||||||
var selectedInputPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) })
|
|
||||||
var disabledInputPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: weakForegroundPattern },
|
|
||||||
artist.Stroke { Pattern: backgroundPattern })
|
|
42
theme/inset.go
Normal file
42
theme/inset.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package theme
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
// Inset represents an inset amount for all four sides of a rectangle. The top
|
||||||
|
// side is at index zero, the right at index one, the bottom at index two, and
|
||||||
|
// the left at index three. These values may be negative.
|
||||||
|
type Inset [4]int
|
||||||
|
|
||||||
|
// Apply returns the given rectangle, shrunk on all four sides by the given
|
||||||
|
// inset. If a measurment of the inset is negative, that side will instead be
|
||||||
|
// expanded outward. If the rectangle's dimensions cannot be reduced any
|
||||||
|
// further, an empty rectangle near its center will be returned.
|
||||||
|
func (inset Inset) Apply (bigger image.Rectangle) (smaller image.Rectangle) {
|
||||||
|
smaller = bigger
|
||||||
|
if smaller.Dx() < inset[3] + inset[1] {
|
||||||
|
smaller.Min.X = (smaller.Min.X + smaller.Max.X) / 2
|
||||||
|
smaller.Max.X = smaller.Min.X
|
||||||
|
} else {
|
||||||
|
smaller.Min.X += inset[3]
|
||||||
|
smaller.Max.X -= inset[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if smaller.Dy() < inset[0] + inset[2] {
|
||||||
|
smaller.Min.Y = (smaller.Min.Y + smaller.Max.Y) / 2
|
||||||
|
smaller.Max.Y = smaller.Min.Y
|
||||||
|
} else {
|
||||||
|
smaller.Min.Y += inset[0]
|
||||||
|
smaller.Max.Y -= inset[2]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse returns a negated version of the inset.
|
||||||
|
func (inset Inset) Inverse () (prime Inset) {
|
||||||
|
return Inset {
|
||||||
|
inset[0] * -1,
|
||||||
|
inset[1] * -1,
|
||||||
|
inset[2] * -1,
|
||||||
|
inset[3] * -1,
|
||||||
|
}
|
||||||
|
}
|
@ -1,43 +0,0 @@
|
|||||||
package theme
|
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
|
||||||
|
|
||||||
var listPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
uhex(0x383C3AFF),
|
|
||||||
uhex(0x999C99FF),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Pattern: uhex(0x999C99FF) })
|
|
||||||
|
|
||||||
var focusedListPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
|
||||||
artist.Stroke { Pattern: uhex(0x999C99FF) })
|
|
||||||
|
|
||||||
var listEntryPattern = artist.Padded {
|
|
||||||
Stroke: uhex(0x383C3AFF),
|
|
||||||
Fill: uhex(0x999C99FF),
|
|
||||||
Sides: []int { 0, 0, 0, 1 },
|
|
||||||
}
|
|
||||||
|
|
||||||
var onListEntryPattern = artist.Padded {
|
|
||||||
Stroke: uhex(0x383C3AFF),
|
|
||||||
Fill: uhex(0x6e8079FF),
|
|
||||||
Sides: []int { 0, 0, 0, 1 },
|
|
||||||
}
|
|
||||||
|
|
||||||
var focusedListEntryPattern = artist.Padded {
|
|
||||||
Stroke: accentPattern,
|
|
||||||
Fill: uhex(0x999C99FF),
|
|
||||||
Sides: []int { 0, 1, 0, 1 },
|
|
||||||
}
|
|
||||||
|
|
||||||
var focusedOnListEntryPattern = artist.Padded {
|
|
||||||
Stroke: accentPattern,
|
|
||||||
Fill: uhex(0x6e8079FF),
|
|
||||||
Sides: []int { 0, 1, 0, 1 },
|
|
||||||
}
|
|
9
theme/parse.go
Normal file
9
theme/parse.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package theme
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// Parse parses one or more theme files and returns them as a Theme.
|
||||||
|
func Parse (sources ...io.Reader) (Theme) {
|
||||||
|
// TODO
|
||||||
|
return Default { }
|
||||||
|
}
|
@ -1,239 +0,0 @@
|
|||||||
package theme
|
|
||||||
|
|
||||||
import "image"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
|
||||||
|
|
||||||
// Case sepecifies what kind of element is using a pattern. It contains a
|
|
||||||
// namespace parameter and an element parameter. The element parameter does not
|
|
||||||
// necissarily need to match an element name, but if it can, it should. Both
|
|
||||||
// parameters should be written in camel case. Themes can change their styling
|
|
||||||
// based on this parameter for fine-grained control over the look and feel of
|
|
||||||
// specific elements.
|
|
||||||
type Case struct { Namespace, Element string }
|
|
||||||
|
|
||||||
// C can be used as shorthand to generate a case struct as used in PatternState.
|
|
||||||
func C (namespace, element string) (c Case) {
|
|
||||||
return Case {
|
|
||||||
Namespace: namespace,
|
|
||||||
Element: element,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PatternState lists parameters which can change the appearance of some
|
|
||||||
// patterns. For example, passing a PatternState with Selected set to true may
|
|
||||||
// result in a pattern that has a colored border within it.
|
|
||||||
type PatternState struct {
|
|
||||||
Case
|
|
||||||
|
|
||||||
// On should be set to true if the element that is using this pattern is
|
|
||||||
// in some sort of "on" state, such as if a checkbox is checked or a
|
|
||||||
// switch is toggled on. This is only necessary if the element in
|
|
||||||
// question is capable of being toggled.
|
|
||||||
On bool
|
|
||||||
|
|
||||||
// Focused should be set to true if the element that is using this
|
|
||||||
// pattern is currently focused.
|
|
||||||
Focused bool
|
|
||||||
|
|
||||||
// Pressed should be set to true if the element that is using this
|
|
||||||
// pattern is being pressed down by the mouse. This is only necessary if
|
|
||||||
// the element in question processes mouse button events.
|
|
||||||
Pressed bool
|
|
||||||
|
|
||||||
// Disabled should be set to true if the element that is using this
|
|
||||||
// pattern is locked and cannot be interacted with. Disabled variations
|
|
||||||
// of patterns are typically flattened and greyed-out.
|
|
||||||
Disabled bool
|
|
||||||
|
|
||||||
// Invalid should be set to true if th element that is using this
|
|
||||||
// pattern wants to warn the user of an invalid interaction or data
|
|
||||||
// entry. Invalid variations typically have some sort of reddish tint
|
|
||||||
// or outline.
|
|
||||||
Invalid bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inset represents an inset amount for all four sides of a rectangle. The top
|
|
||||||
// side is at index zero, the right at index one, the bottom at index two, and
|
|
||||||
// the left at index three. These values may be negative.
|
|
||||||
type Inset [4]int
|
|
||||||
|
|
||||||
// Apply returns the given rectangle, shrunk on all four sides by the given
|
|
||||||
// inset. If a measurment of the inset is negative, that side will instead be
|
|
||||||
// expanded outward. If the rectangle's dimensions cannot be reduced any
|
|
||||||
// further, an empty rectangle near its center will be returned.
|
|
||||||
func (inset Inset) Apply (bigger image.Rectangle) (smaller image.Rectangle) {
|
|
||||||
smaller = bigger
|
|
||||||
if smaller.Dx() < inset[3] + inset[1] {
|
|
||||||
smaller.Min.X = (smaller.Min.X + smaller.Max.X) / 2
|
|
||||||
smaller.Max.X = smaller.Min.X
|
|
||||||
} else {
|
|
||||||
smaller.Min.X += inset[3]
|
|
||||||
smaller.Max.X -= inset[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if smaller.Dy() < inset[0] + inset[2] {
|
|
||||||
smaller.Min.Y = (smaller.Min.Y + smaller.Max.Y) / 2
|
|
||||||
smaller.Max.Y = smaller.Min.Y
|
|
||||||
} else {
|
|
||||||
smaller.Min.Y += inset[0]
|
|
||||||
smaller.Max.Y -= inset[2]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inverse returns a negated version of the inset.
|
|
||||||
func (inset Inset) Inverse () (prime Inset) {
|
|
||||||
return Inset {
|
|
||||||
inset[0] * -1,
|
|
||||||
inset[1] * -1,
|
|
||||||
inset[2] * -1,
|
|
||||||
inset[3] * -1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccentPattern returns the accent pattern, which is usually just a solid
|
|
||||||
// color.
|
|
||||||
func AccentPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
return accentPattern, Inset { }
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackgroundPattern returns the main background pattern.
|
|
||||||
func BackgroundPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
return backgroundPattern, Inset { }
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeadPattern returns a pattern that can be used to mark an area or gap that
|
|
||||||
// serves no purpose, but still needs aesthetic structure.
|
|
||||||
func DeadPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
return deadPattern, Inset { }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForegroundPattern returns the color text should be.
|
|
||||||
func ForegroundPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
if state.Disabled {
|
|
||||||
return weakForegroundPattern, Inset { }
|
|
||||||
} else {
|
|
||||||
return foregroundPattern, Inset { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InputPattern returns a background pattern for any input field that can be
|
|
||||||
// edited by typing with the keyboard.
|
|
||||||
func InputPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
if state.Disabled {
|
|
||||||
return disabledInputPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
} else {
|
|
||||||
if state.Focused {
|
|
||||||
return selectedInputPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
} else {
|
|
||||||
return inputPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPattern returns a background pattern for a list of things.
|
|
||||||
func ListPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
if state.Focused {
|
|
||||||
pattern = focusedListPattern
|
|
||||||
inset = Inset { 2, 1, 2, 1 }
|
|
||||||
} else {
|
|
||||||
pattern = listPattern
|
|
||||||
inset = Inset { 2, 1, 1, 1 }
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ItemPattern returns a background pattern for a list item.
|
|
||||||
func ItemPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
if state.Focused {
|
|
||||||
if state.On {
|
|
||||||
pattern = focusedOnListEntryPattern
|
|
||||||
} else {
|
|
||||||
pattern = focusedListEntryPattern
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if state.On {
|
|
||||||
pattern = onListEntryPattern
|
|
||||||
} else {
|
|
||||||
pattern = listEntryPattern
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inset = Inset { 4, 6, 4, 6 }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ButtonPattern returns a pattern to be displayed on buttons.
|
|
||||||
func ButtonPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
if state.Disabled {
|
|
||||||
return disabledButtonPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
} else {
|
|
||||||
if state.Pressed {
|
|
||||||
if state.Focused {
|
|
||||||
return pressedSelectedButtonPattern, Inset {
|
|
||||||
2, 0, 0, 2 }
|
|
||||||
} else {
|
|
||||||
return pressedButtonPattern, Inset { 2, 0, 0, 2 }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if state.Focused {
|
|
||||||
return selectedButtonPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
} else {
|
|
||||||
return buttonPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GutterPattern returns a pattern to be used to mark a track along which
|
|
||||||
// something slides.
|
|
||||||
func GutterPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
if state.Disabled {
|
|
||||||
return disabledScrollGutterPattern, Inset { 0, 0, 0, 0 }
|
|
||||||
} else {
|
|
||||||
return scrollGutterPattern, Inset { 0, 0, 0, 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandlePattern returns a pattern to be displayed on a grab handle that slides
|
|
||||||
// along a gutter.
|
|
||||||
func HandlePattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
if state.Disabled {
|
|
||||||
return disabledScrollBarPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
} else {
|
|
||||||
if state.Focused {
|
|
||||||
if state.Pressed {
|
|
||||||
return pressedSelectedScrollBarPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
} else {
|
|
||||||
return selectedScrollBarPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if state.Pressed {
|
|
||||||
return pressedScrollBarPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
} else {
|
|
||||||
return scrollBarPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SunkenPattern returns a general purpose pattern that is sunken/engraved into
|
|
||||||
// the background.
|
|
||||||
func SunkenPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
return sunkenPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// RaisedPattern returns a general purpose pattern that is raised up out of the
|
|
||||||
// background.
|
|
||||||
func RaisedPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
if state.Focused {
|
|
||||||
return selectedRaisedPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
} else {
|
|
||||||
return raisedPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PinboardPattern returns a textured backdrop pattern. Anything drawn within it
|
|
||||||
// should have its own background pattern.
|
|
||||||
func PinboardPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
|
|
||||||
return texturedSunkenPattern, Inset { 1, 1, 1, 1 }
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
package theme
|
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
|
||||||
|
|
||||||
var scrollGutterPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0x3b534eFF)),
|
|
||||||
artist.NewUniform(hex(0x6e8079FF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x6e8079FF)) })
|
|
||||||
var disabledScrollGutterPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: weakForegroundPattern },
|
|
||||||
artist.Stroke { Pattern: backgroundPattern })
|
|
||||||
var scrollBarPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0xCCD5D2FF)),
|
|
||||||
artist.NewUniform(hex(0x4B5B59FF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
|
||||||
var selectedScrollBarPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0xCCD5D2FF)),
|
|
||||||
artist.NewUniform(hex(0x4B5B59FF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) })
|
|
||||||
var pressedScrollBarPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0xCCD5D2FF)),
|
|
||||||
artist.NewUniform(hex(0x4B5B59FF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Weight: 1, Pattern: artist.NewUniform(hex(0x8D9894FF)) },
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) })
|
|
||||||
var pressedSelectedScrollBarPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0xCCD5D2FF)),
|
|
||||||
artist.NewUniform(hex(0x4B5B59FF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) })
|
|
||||||
var disabledScrollBarPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: weakForegroundPattern },
|
|
||||||
artist.Stroke { Pattern: backgroundPattern })
|
|
78
theme/state.go
Normal file
78
theme/state.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package theme
|
||||||
|
|
||||||
|
// Case sepecifies what kind of element is using a pattern. It contains a
|
||||||
|
// namespace parameter and an element parameter. The element parameter does not
|
||||||
|
// necissarily need to match an element name, but if it can, it should. Both
|
||||||
|
// parameters should be written in camel case. Themes can change their styling
|
||||||
|
// based on this parameter for fine-grained control over the look and feel of
|
||||||
|
// specific elements.
|
||||||
|
type Case struct { Namespace, Element string }
|
||||||
|
|
||||||
|
// C can be used as shorthand to generate a case struct as used in PatternState.
|
||||||
|
func C (namespace, element string) (c Case) {
|
||||||
|
return Case {
|
||||||
|
Namespace: namespace,
|
||||||
|
Element: element,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatternState lists parameters which can change the appearance of some
|
||||||
|
// patterns. For example, passing a PatternState with Selected set to true may
|
||||||
|
// result in a pattern that has a colored border within it.
|
||||||
|
type PatternState struct {
|
||||||
|
// On should be set to true if the element that is using this pattern is
|
||||||
|
// in some sort of "on" state, such as if a checkbox is checked or a
|
||||||
|
// switch is toggled on. This is only necessary if the element in
|
||||||
|
// question is capable of being toggled.
|
||||||
|
On bool
|
||||||
|
|
||||||
|
// Focused should be set to true if the element that is using this
|
||||||
|
// pattern is currently focused.
|
||||||
|
Focused bool
|
||||||
|
|
||||||
|
// Pressed should be set to true if the element that is using this
|
||||||
|
// pattern is being pressed down by the mouse. This is only necessary if
|
||||||
|
// the element in question processes mouse button events.
|
||||||
|
Pressed bool
|
||||||
|
|
||||||
|
// Disabled should be set to true if the element that is using this
|
||||||
|
// pattern is locked and cannot be interacted with. Disabled variations
|
||||||
|
// of patterns are typically flattened and greyed-out.
|
||||||
|
Disabled bool
|
||||||
|
|
||||||
|
// Invalid should be set to true if th element that is using this
|
||||||
|
// pattern wants to warn the user of an invalid interaction or data
|
||||||
|
// entry. Invalid variations typically have some sort of reddish tint
|
||||||
|
// or outline.
|
||||||
|
Invalid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontStyle specifies stylistic alterations to a font face.
|
||||||
|
type FontStyle int; const (
|
||||||
|
FontStyleRegular FontStyle = 0
|
||||||
|
FontStyleBold FontStyle = 1
|
||||||
|
FontStyleItalic FontStyle = 2
|
||||||
|
FontStyleBoldItalic FontStyle = 1 | 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// FontSize specifies the general size of a font face in a semantic way.
|
||||||
|
type FontSize int; const (
|
||||||
|
// FontSizeNormal is the default font size that should be used for most
|
||||||
|
// things.
|
||||||
|
FontSizeNormal FontSize = iota
|
||||||
|
|
||||||
|
// FontSizeLarge is a larger font size suitable for things like section
|
||||||
|
// headings.
|
||||||
|
FontSizeLarge
|
||||||
|
|
||||||
|
// FontSizeHuge is a very large font size suitable for things like
|
||||||
|
// titles, wizard step names, digital clocks, etc.
|
||||||
|
FontSizeHuge
|
||||||
|
|
||||||
|
// FontSizeSmall is a smaller font size. Try not to use this unless it
|
||||||
|
// makes a lot of sense to do so, because it can negatively impact
|
||||||
|
// accessibility. It is useful for things like copyright notices at the
|
||||||
|
// bottom of some window that the average user doesn't actually care
|
||||||
|
// about.
|
||||||
|
FontSizeSmall
|
||||||
|
)
|
213
theme/theme.go
213
theme/theme.go
@ -1,113 +1,116 @@
|
|||||||
package theme
|
package theme
|
||||||
|
|
||||||
import "image/color"
|
import "image"
|
||||||
import "golang.org/x/image/font"
|
import "golang.org/x/image/font"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
|
|
||||||
|
|
||||||
// none of these colors are final! TODO: generate these values from a theme
|
// Pattern lists a number of cannonical pattern types, each with its own ID.
|
||||||
// file at startup.
|
// This allows custom elements to follow themes, even those that do not
|
||||||
|
// explicitly support them.
|
||||||
|
type Pattern int; const (
|
||||||
|
// PatternAccent is the accent color of the theme. It is safe to assume
|
||||||
|
// that this is, by default, a solid color.
|
||||||
|
PatternAccent Pattern = iota
|
||||||
|
|
||||||
func hex (color uint32) (c color.RGBA) {
|
// PatternBackground is the background color of the theme. It is safe to
|
||||||
c.A = uint8(color)
|
// assume that this is, by default, a solid color.
|
||||||
c.B = uint8(color >> 8)
|
PatternBackground
|
||||||
c.G = uint8(color >> 16)
|
|
||||||
c.R = uint8(color >> 24)
|
// PatternForeground is the foreground text color of the theme. It is
|
||||||
|
// safe to assume that this is, by default, a solid color.
|
||||||
|
PatternForeground
|
||||||
|
|
||||||
|
// PatternDead is a pattern that is displayed on a "dead area" where no
|
||||||
|
// controls exist, but there still must be some indication of visual
|
||||||
|
// structure (such as in the corner between two scroll bars).
|
||||||
|
PatternDead
|
||||||
|
|
||||||
|
// PatternRaised is a generic raised pattern.
|
||||||
|
PatternRaised
|
||||||
|
|
||||||
|
// PatternSunken is a generic sunken pattern.
|
||||||
|
PatternSunken
|
||||||
|
|
||||||
|
// PatternPinboard is similar to PatternSunken, but it is textured.
|
||||||
|
PatternPinboard
|
||||||
|
|
||||||
|
// PatternButton is a button pattern.
|
||||||
|
PatternButton
|
||||||
|
|
||||||
|
// PatternInput is a pattern for input fields, editable text areas, etc.
|
||||||
|
PatternInput
|
||||||
|
|
||||||
|
// PatternGutter is a track for things to slide on.
|
||||||
|
PatternGutter
|
||||||
|
|
||||||
|
// PatternHandle is a handle that slides along a gutter.
|
||||||
|
PatternHandle
|
||||||
|
)
|
||||||
|
|
||||||
|
// Theme represents a visual style configuration,
|
||||||
|
type Theme interface {
|
||||||
|
// FontFace returns the proper font for a given style, size, and case.
|
||||||
|
FontFace (FontStyle, FontSize, Case) font.Face
|
||||||
|
|
||||||
|
// Icon returns an appropriate icon given an icon name and case.
|
||||||
|
Icon (string, Case) artist.Pattern
|
||||||
|
|
||||||
|
// Pattern returns an appropriate pattern given a pattern name, case,
|
||||||
|
// and state.
|
||||||
|
Pattern (Pattern, Case, PatternState) artist.Pattern
|
||||||
|
|
||||||
|
// Inset returns the area on all sides of a given pattern that is not
|
||||||
|
// meant to be drawn on.
|
||||||
|
Inset (Pattern, Case) Inset
|
||||||
|
|
||||||
|
// Sink returns a vector that should be added to an element's inner
|
||||||
|
// content when it is pressed down (if applicable) to simulate a 3D
|
||||||
|
// sinking effect.
|
||||||
|
Sink (Pattern, Case) image.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapped wraps any theme and injects a case into it automatically so that it
|
||||||
|
// doesn't need to be specified for each query. Additionally, if the underlying
|
||||||
|
// theme is nil, it just uses the default theme instead.
|
||||||
|
type Wrapped struct {
|
||||||
|
Theme
|
||||||
|
Case
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontFace returns the proper font for a given style and size.
|
||||||
|
func (wrapped Wrapped) FontFace (style FontStyle, size FontSize) font.Face {
|
||||||
|
real := wrapped.ensure()
|
||||||
|
return real.FontFace(style, size, wrapped.Case)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon returns an appropriate icon given an icon name.
|
||||||
|
func (wrapped Wrapped) Icon (name string) artist.Pattern {
|
||||||
|
real := wrapped.ensure()
|
||||||
|
return real.Icon(name, wrapped.Case)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern returns an appropriate pattern given a pattern name and state.
|
||||||
|
func (wrapped Wrapped) Pattern (id Pattern, state PatternState) artist.Pattern {
|
||||||
|
real := wrapped.ensure()
|
||||||
|
return real.Pattern(id, wrapped.Case, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inset returns the area on all sides of a given pattern that is not meant to
|
||||||
|
// be drawn on.
|
||||||
|
func (wrapped Wrapped) Inset (id Pattern) Inset {
|
||||||
|
real := wrapped.ensure()
|
||||||
|
return real.Inset(id, wrapped.Case)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sink returns a vector that should be added to an element's inner content when
|
||||||
|
// it is pressed down (if applicable) to simulate a 3D sinking effect.
|
||||||
|
func (wrapped Wrapped) Sink (id Pattern) image.Point {
|
||||||
|
real := wrapped.ensure()
|
||||||
|
return real.Sink(id, wrapped.Case)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wrapped Wrapped) ensure () (real Theme) {
|
||||||
|
real = wrapped.Theme
|
||||||
|
if real == nil { real = Default { } }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func uhex (color uint32) (pattern artist.Pattern) {
|
|
||||||
return artist.NewUniform(hex(color))
|
|
||||||
}
|
|
||||||
|
|
||||||
var accentPattern = artist.NewUniform(hex(0x408090FF))
|
|
||||||
var backgroundPattern = artist.NewUniform(color.Gray16 { 0xAAAA })
|
|
||||||
var foregroundPattern = artist.NewUniform(color.Gray16 { 0x0000 })
|
|
||||||
var weakForegroundPattern = artist.NewUniform(color.Gray16 { 0x4444 })
|
|
||||||
var strokePattern = artist.NewUniform(color.Gray16 { 0x0000 })
|
|
||||||
|
|
||||||
var sunkenPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0x3b534eFF)),
|
|
||||||
artist.NewUniform(hex(0x97a09cFF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) })
|
|
||||||
|
|
||||||
var texturedSunkenPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0x3b534eFF)),
|
|
||||||
artist.NewUniform(hex(0x97a09cFF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// artist.Stroke { Pattern: artist.Striped {
|
|
||||||
// First: artist.Stroke {
|
|
||||||
// Weight: 2,
|
|
||||||
// Pattern: artist.NewUniform(hex(0x97a09cFF)),
|
|
||||||
// },
|
|
||||||
// Second: artist.Stroke {
|
|
||||||
// Weight: 1,
|
|
||||||
// Pattern: artist.NewUniform(hex(0x6e8079FF)),
|
|
||||||
// },
|
|
||||||
// }})
|
|
||||||
|
|
||||||
artist.Stroke { Pattern: artist.Noisy {
|
|
||||||
Low: artist.NewUniform(hex(0x97a09cFF)),
|
|
||||||
High: artist.NewUniform(hex(0x6e8079FF)),
|
|
||||||
}})
|
|
||||||
|
|
||||||
var raisedPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0xDBDBDBFF)),
|
|
||||||
artist.NewUniform(hex(0x383C3AFF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) })
|
|
||||||
|
|
||||||
var selectedRaisedPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke {
|
|
||||||
Weight: 1,
|
|
||||||
Pattern: artist.Beveled {
|
|
||||||
artist.NewUniform(hex(0xDBDBDBFF)),
|
|
||||||
artist.NewUniform(hex(0x383C3AFF)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
artist.Stroke { Weight: 1, Pattern: accentPattern },
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) })
|
|
||||||
|
|
||||||
var deadPattern = artist.NewMultiBordered (
|
|
||||||
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
|
||||||
artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) })
|
|
||||||
|
|
||||||
// TODO: load fonts from an actual source instead of using defaultfont
|
|
||||||
|
|
||||||
// FontFaceRegular returns the font face to be used for normal text.
|
|
||||||
func FontFaceRegular () font.Face {
|
|
||||||
return defaultfont.FaceRegular
|
|
||||||
}
|
|
||||||
|
|
||||||
// FontFaceBold returns the font face to be used for bolded text.
|
|
||||||
func FontFaceBold () font.Face {
|
|
||||||
return defaultfont.FaceBold
|
|
||||||
}
|
|
||||||
|
|
||||||
// FontFaceItalic returns the font face to be used for italicized text.
|
|
||||||
func FontFaceItalic () font.Face {
|
|
||||||
return defaultfont.FaceItalic
|
|
||||||
}
|
|
||||||
|
|
||||||
// FontFaceBoldItalic returns the font face to be used for text that is both
|
|
||||||
// bolded and italicized.
|
|
||||||
func FontFaceBoldItalic () font.Face {
|
|
||||||
return defaultfont.FaceBoldItalic
|
|
||||||
}
|
|
||||||
|
16
theme/util.go
Normal file
16
theme/util.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package theme
|
||||||
|
|
||||||
|
import "image/color"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
|
||||||
|
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 uhex (color uint32) (pattern artist.Pattern) {
|
||||||
|
return artist.NewUniform(hex(color))
|
||||||
|
}
|
89
tomo.go
89
tomo.go
@ -1,7 +1,12 @@
|
|||||||
package tomo
|
package tomo
|
||||||
|
|
||||||
import "errors"
|
import "os"
|
||||||
|
import "io"
|
||||||
|
import "path/filepath"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/dirs"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||||
|
|
||||||
var backend Backend
|
var backend Backend
|
||||||
@ -11,6 +16,9 @@ var backend Backend
|
|||||||
// the backend experiences a fatal error.
|
// the backend experiences a fatal error.
|
||||||
func Run (callback func ()) (err error) {
|
func Run (callback func ()) (err error) {
|
||||||
backend, err = instantiateBackend()
|
backend, err = instantiateBackend()
|
||||||
|
config := parseConfig()
|
||||||
|
backend.SetConfig(config)
|
||||||
|
backend.SetTheme(parseTheme(config.ThemePath()))
|
||||||
if callback != nil { callback() }
|
if callback != nil { callback() }
|
||||||
err = backend.Run()
|
err = backend.Run()
|
||||||
backend = nil
|
backend = nil
|
||||||
@ -26,7 +34,7 @@ func Stop () {
|
|||||||
// Do executes the specified callback within the main thread as soon as
|
// Do executes the specified callback within the main thread as soon as
|
||||||
// possible. This function can be safely called from other threads.
|
// possible. This function can be safely called from other threads.
|
||||||
func Do (callback func ()) {
|
func Do (callback func ()) {
|
||||||
if backend == nil { panic("no backend is running") }
|
assertBackend()
|
||||||
backend.Do(callback)
|
backend.Do(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,22 +43,87 @@ func Do (callback func ()) {
|
|||||||
// why. If this function is called without a running backend, an error is
|
// why. If this function is called without a running backend, an error is
|
||||||
// returned as well.
|
// returned as well.
|
||||||
func NewWindow (width, height int) (window elements.Window, err error) {
|
func NewWindow (width, height int) (window elements.Window, err error) {
|
||||||
if backend == nil {
|
assertBackend()
|
||||||
err = errors.New("no backend is running.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return backend.NewWindow(width, height)
|
return backend.NewWindow(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy puts data into the clipboard.
|
// Copy puts data into the clipboard.
|
||||||
func Copy (data data.Data) {
|
func Copy (data data.Data) {
|
||||||
if backend == nil { panic("no backend is running") }
|
assertBackend()
|
||||||
backend.Copy(data)
|
backend.Copy(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paste returns the data currently in the clipboard. This method may
|
// Paste returns the data currently in the clipboard. This method may
|
||||||
// return nil.
|
// return nil.
|
||||||
func Paste (accept []data.Mime) (data.Data) {
|
func Paste (accept []data.Mime) (data.Data) {
|
||||||
if backend == nil { panic("no backend is running") }
|
assertBackend()
|
||||||
return backend.Paste(accept)
|
return backend.Paste(accept)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the theme of all open windows.
|
||||||
|
func SetTheme (theme theme.Theme) {
|
||||||
|
backend.SetTheme(theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the configuration of all open windows.
|
||||||
|
func SetConfig (config config.Config) {
|
||||||
|
backend.SetConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig () (config.Config) {
|
||||||
|
return parseMany [config.Config] (
|
||||||
|
dirs.ConfigDirs("tomo/tomo.conf"),
|
||||||
|
config.Parse,
|
||||||
|
config.Default { })
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTheme (path string) (theme.Theme) {
|
||||||
|
if path == "" { return theme.Default { } }
|
||||||
|
path = filepath.Join(path, "tomo")
|
||||||
|
|
||||||
|
// find all tomo pattern graph files in the directory
|
||||||
|
directory, err := os.Open(path)
|
||||||
|
if err != nil { return theme.Default { } }
|
||||||
|
names, _ := directory.Readdirnames(0)
|
||||||
|
paths := []string { }
|
||||||
|
for _, name := range names {
|
||||||
|
if filepath.Ext(name) == ".tpg" {
|
||||||
|
paths = append(paths, filepath.Join(path, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse them
|
||||||
|
return parseMany [theme.Theme] (
|
||||||
|
paths,
|
||||||
|
theme.Parse,
|
||||||
|
theme.Default { })
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMany [OBJECT any] (
|
||||||
|
paths []string,
|
||||||
|
parser func (...io.Reader) OBJECT,
|
||||||
|
fallback OBJECT,
|
||||||
|
) (
|
||||||
|
object OBJECT,
|
||||||
|
) {
|
||||||
|
// convert all paths into readers
|
||||||
|
sources := []io.Reader { }
|
||||||
|
for _, path := range paths {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil { continue }
|
||||||
|
sources = append(sources, file)
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if sources == nil {
|
||||||
|
// if there are no readers, return the fallback object
|
||||||
|
return fallback
|
||||||
|
} else {
|
||||||
|
// if there are readers, parse them
|
||||||
|
return parser(sources...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertBackend () {
|
||||||
|
if backend == nil { panic("no backend is running") }
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user