Merge pull request 'restructure-config' (#8) from restructure-config into main

Reviewed-on: sashakoshka/tomo#8
This commit is contained in:
Sasha Koshka 2023-02-09 02:07:08 +00:00
commit 5c7e243566
39 changed files with 1896 additions and 838 deletions

View File

@ -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 }

View File

@ -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

View File

@ -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)

View File

@ -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") }
} }

View File

@ -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
View 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
View 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)
}

View File

@ -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)
} }

View File

@ -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)
}
} }

View File

@ -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())
} }

View File

@ -1,11 +1,10 @@
package basicElements package basicElements
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var labelCase = theme.C("basic", "label")
// Label is a simple text box. // Label is a simple text box.
type Label struct { type Label struct {
*core.Core *core.Core
@ -15,6 +14,9 @@ type Label struct {
text string text string
drawer artist.TextDrawer drawer artist.TextDrawer
config config.Wrapped
theme theme.Wrapped
onFlexibleHeightChange func () onFlexibleHeightChange func ()
} }
@ -22,14 +24,28 @@ type Label struct {
// wrapped. // wrapped.
func NewLabel (text string, wrap bool) (element *Label) { func NewLabel (text string, wrap bool) (element *Label) {
element = &Label { } element = &Label { }
element.theme.Case = theme.C("basic", "label")
element.Core, element.core = core.NewCore(element.handleResize) element.Core, element.core = core.NewCore(element.handleResize)
face := theme.FontFaceRegular()
element.drawer.SetFace(face)
element.SetWrap(wrap) element.SetWrap(wrap)
element.SetText(text) element.SetText(text)
return return
} }
func (element *Label) redo () {
face := element.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal)
element.drawer.SetFace(face)
element.updateMinimumSize()
bounds := element.Bounds()
if element.wrap {
element.drawer.SetMaxWidth(bounds.Dx())
element.drawer.SetMaxHeight(bounds.Dy())
}
element.draw()
element.core.DamageAll()
}
func (element *Label) handleResize () { func (element *Label) handleResize () {
bounds := element.Bounds() bounds := element.Bounds()
if element.wrap { if element.wrap {
@ -90,10 +106,37 @@ func (element *Label) SetWrap (wrap bool) {
} }
} }
// SetTheme sets the element's theme.
func (element *Label) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal))
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
// SetConfig sets the element's configuration.
func (element *Label) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Label) updateMinimumSize () { func (element *Label) updateMinimumSize () {
if element.wrap { if element.wrap {
em := element.drawer.Em().Round() em := element.drawer.Em().Round()
if em < 1 { em = theme.Padding() } if em < 1 { em = element.config.Padding() }
element.core.SetMinimumSize ( element.core.SetMinimumSize (
em, element.drawer.LineHeight().Round()) em, element.drawer.LineHeight().Round())
if element.onFlexibleHeightChange != nil { if element.onFlexibleHeightChange != nil {
@ -108,15 +151,15 @@ func (element *Label) updateMinimumSize () {
func (element *Label) draw () { func (element *Label) draw () {
bounds := element.Bounds() bounds := element.Bounds()
pattern, _ := theme.BackgroundPattern(theme.PatternState { pattern := element.theme.Pattern (
Case: labelCase, theme.PatternBackground,
}) theme.PatternState { })
artist.FillRectangle(element, pattern, bounds) artist.FillRectangle(element, pattern, bounds)
textBounds := element.drawer.LayoutBounds() textBounds := element.drawer.LayoutBounds()
foreground, _ := theme.ForegroundPattern (theme.PatternState { foreground := element.theme.Pattern (
Case: labelCase, theme.PatternForeground,
}) theme.PatternState { })
element.drawer.Draw(element, foreground, bounds.Min.Sub(textBounds.Min)) element.drawer.Draw(element, foreground, bounds.Min.Sub(textBounds.Min))
} }

View File

@ -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(),
}) })

View File

@ -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,

View File

@ -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)
} }

View File

@ -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]

View File

@ -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)
} }
} }

View File

@ -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)
} }

View File

@ -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,

View File

@ -20,8 +20,15 @@ type Core struct {
} }
// NewCore creates a new element core and its corresponding control. // NewCore creates a new element core and its corresponding control.
func NewCore (drawSizeChange func ()) (core *Core, control CoreControl) { func NewCore (
core = &Core { drawSizeChange: drawSizeChange } drawSizeChange func (),
) (
core *Core,
control CoreControl,
) {
core = &Core {
drawSizeChange: drawSizeChange,
}
control = CoreControl { core: core } control = CoreControl { core: core }
return return
} }

View File

@ -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)
}

View File

@ -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 (

244
elements/fun/piano.go Normal file
View 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)
}

View File

@ -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

View File

@ -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,

View File

@ -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
View 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()
}

View File

@ -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
View 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
View 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 })

View File

@ -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
View 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,
}
}

View File

@ -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
View 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 { }
}

View File

@ -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 }
}

View File

@ -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
View 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
)

View File

@ -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
View 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
View File

@ -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") }
}