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,21 +14,38 @@ type Label struct {
 | 
				
			|||||||
	text   string
 | 
						text   string
 | 
				
			||||||
	drawer artist.TextDrawer
 | 
						drawer artist.TextDrawer
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
						config config.Wrapped
 | 
				
			||||||
 | 
						theme  theme.Wrapped
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	onFlexibleHeightChange func ()
 | 
						onFlexibleHeightChange func ()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewLabel creates a new label. If wrap is set to true, the text inside will be
 | 
					// NewLabel creates a new label. If wrap is set to true, the text inside will be
 | 
				
			||||||
// 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,
 | 
				
			||||||
 | 
				
			|||||||
@ -14,14 +14,21 @@ type Core struct {
 | 
				
			|||||||
		minimumHeight int
 | 
							minimumHeight int
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	drawSizeChange func ()
 | 
						drawSizeChange      func ()
 | 
				
			||||||
	onMinimumSizeChange func ()
 | 
						onMinimumSizeChange func ()
 | 
				
			||||||
	onDamage func (region canvas.Canvas)
 | 
						onDamage func (region canvas.Canvas)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 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