Merge pull request 'reorganize' (#17) from reorganize into main

Reviewed-on: sashakoshka/tomo#17
This commit is contained in:
Sasha Koshka 2023-05-03 19:42:22 +00:00
commit ae12945676
108 changed files with 1715 additions and 2974 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -6,11 +6,19 @@ Please note: Tomo is in early development. Some features may not work properly,
and its API may change without notice.
Tomo is a GUI toolkit written in pure Go with minimal external dependencies. It
makes use of Go's unique language features to do more with less. It is also
easily extendable with custom backends and elements.
makes use of Go's unique language features to do more with less.
You can find out more about how to use it by visiting the examples directory,
or pull up its documentation by running `godoc` within the repository. You can
also view it on the web on
[pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/sashakoshka/tomo) (although
Nasin is an application framework that runs on top of Tomo. It supports plugins
which can extend any application with backends, themes, etc.
## Usage
Before you start using Tomo, you need to install a backend plugin. Currently,
there is only an X backend. You can run ./scripts/install-backends.sh to install
it. It will be placed in `~/.local/lib/nasin/plugins`.
You can find out more about how to use Tomo and Nasin by visiting the examples
directory, or pull up the documentation by running `godoc` within the
repository. You can also view it on the web on
[pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/sashakoshka/tomo) (although
it may be slightly out of date).

241
ability/element.go Normal file
View File

@ -0,0 +1,241 @@
// Package ability defines extended interfaces that elements can support.
package ability
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// Layoutable represents an element that needs to perform layout calculations
// before it can draw itself.
type Layoutable interface {
tomo.Element
// Layout causes this element to perform a layout operation.
Layout ()
}
// Container represents an element capable of containing child elements.
type Container interface {
tomo.Element
Layoutable
// DrawBackground causes the element to draw its background pattern to
// the specified canvas. The bounds of this canvas specify the area that
// is actually drawn to, while the Entity bounds specify the actual area
// of the element.
DrawBackground (artist.Canvas)
// HandleChildMinimumSizeChange is called when a child's minimum size is
// changed.
HandleChildMinimumSizeChange (child tomo.Element)
}
// Enableable represents an element that can be enabled and disabled. Disabled
// elements typically appear greyed out.
type Enableable interface {
tomo.Element
// Enabled returns whether or not the element is enabled.
Enabled () bool
// SetEnabled sets whether or not the element is enabled.
SetEnabled (bool)
}
// Focusable represents an element that has keyboard navigation support.
type Focusable interface {
tomo.Element
Enableable
// HandleFocusChange is called when the element is focused or unfocused.
HandleFocusChange ()
}
// Selectable represents an element that can be selected. This includes things
// like list items, files, etc. The difference between this and Focusable is
// that multiple Selectable elements may be selected at the same time, whereas
// only one Focusable element may be focused at the same time. Containers who's
// purpose is to contain selectable elements can determine when to select them
// by implementing MouseTargetContainer and listening for HandleChildMouseDown
// events.
type Selectable interface {
tomo.Element
Enableable
// HandleSelectionChange is called when the element is selected or
// deselected.
HandleSelectionChange ()
}
// KeyboardTarget represents an element that can receive keyboard input.
type KeyboardTarget interface {
tomo.Element
// HandleKeyDown is called when a key is pressed down or repeated while
// this element has keyboard focus. It is important to note that not
// every key down event is guaranteed to be paired with exactly one key
// up event. This is the reason a list of modifier keys held down at the
// time of the key press is given.
HandleKeyDown (key input.Key, modifiers input.Modifiers)
// HandleKeyUp is called when a key is released while this element has
// keyboard focus.
HandleKeyUp (key input.Key, modifiers input.Modifiers)
}
// MouseTarget represents an element that can receive mouse events.
type MouseTarget interface {
tomo.Element
// HandleMouseDown is called when a mouse button is pressed down on this
// element.
HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on this element.
HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers)
}
// MouseTargetContainer represents an element that wants to know when one
// of its children is clicked. Children do not have to implement MouseTarget for
// a container satisfying MouseTargetContainer to be notified that they have
// been clicked.
type MouseTargetContainer interface {
Container
// HandleMouseDown is called when a mouse button is pressed down on a
// child element.
HandleChildMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child tomo.Element)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on a child element.
HandleChildMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child tomo.Element)
}
// MotionTarget represents an element that can receive mouse motion events.
type MotionTarget interface {
tomo.Element
// HandleMotion is called when the mouse is moved over this element,
// or the mouse is moving while being held down and originally pressed
// down on this element.
HandleMotion (position image.Point)
}
// ScrollTarget represents an element that can receive mouse scroll events.
type ScrollTarget interface {
tomo.Element
// HandleScroll is called when the mouse is scrolled. The X and Y
// direction of the scroll event are passed as deltaX and deltaY.
HandleScroll (
position image.Point,
deltaX, deltaY float64,
modifiers input.Modifiers)
}
// Flexible represents an element who's preferred minimum height can change in
// response to its width.
type Flexible interface {
tomo.Element
// FlexibleHeightFor returns what the element's minimum height would be
// if resized to a specified width. This does not actually alter the
// state of the element in any way, but it may perform significant work,
// so it should be called sparingly.
//
// It is reccomended that parent containers check for this interface and
// take this method's value into account in order to support things like
// flow layouts and text wrapping, but it is not absolutely necessary.
// The element's MinimumSize method will still return the absolute
// minimum size that the element may be resized to.
//
// It is important to note that if a parent container checks for
// flexible chilren, it itself will likely need to be either scrollable
// or flexible.
FlexibleHeightFor (width int) int
}
// FlexibleContainer represents an element that is capable of containing
// flexible children.
type FlexibleContainer interface {
Container
// HandleChildFlexibleHeightChange is called when the parameters
// affecting a child's flexible height are changed.
HandleChildFlexibleHeightChange (child Flexible)
}
// Scrollable represents an element that can be scrolled. It acts as a viewport
// through which its contents can be observed.
type Scrollable interface {
tomo.Element
// ScrollContentBounds returns the full content size of the element.
ScrollContentBounds () image.Rectangle
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
ScrollViewportBounds () image.Rectangle
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
ScrollTo (position image.Point)
// ScrollAxes returns the supported axes for scrolling.
ScrollAxes () (horizontal, vertical bool)
}
// ScrollableContainer represents an element that is capable of containing
// scrollable children.
type ScrollableContainer interface {
Container
// HandleChildScrollBoundsChange is called when the content bounds,
// viewport bounds, or scroll axes of a child are changed.
HandleChildScrollBoundsChange (child Scrollable)
}
// Collapsible represents an element who's minimum width and height can be
// manually resized. Scrollable elements should implement this if possible.
type Collapsible interface {
tomo.Element
// Collapse collapses the element's minimum width and height. A value of
// zero for either means that the element's normal value is used.
Collapse (width, height int)
}
// Themeable represents an element that can modify its appearance to fit within
// a theme.
type Themeable interface {
tomo.Element
// HandleThemeChange is called whenever the theme is changed.
HandleThemeChange ()
}
// Configurable represents an element that can modify its behavior to fit within
// a set of configuration parameters.
type Configurable interface {
tomo.Element
// HandleConfigChange is called whenever configuration parameters are
// changed.
HandleConfigChange ()
}

63
artist/artutil/util.go Normal file
View File

@ -0,0 +1,63 @@
// Package artutil provides utility functions for working with graphical types
// defined in artist, canvas, and image.
package artutil
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
// Fill fills the destination canvas with the given pattern.
func Fill (destination artist.Canvas, source artist.Pattern) (updated image.Rectangle) {
source.Draw(destination, destination.Bounds())
return destination.Bounds()
}
// DrawClip lets you draw several subsets of a pattern at once.
func DrawClip (
destination artist.Canvas,
source artist.Pattern,
bounds image.Rectangle,
subsets ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
for _, subset := range subsets {
source.Draw(artist.Cut(destination, subset), bounds)
updatedRegion = updatedRegion.Union(subset)
}
return
}
// DrawShatter is like an inverse of DrawClip, drawing nothing in the areas
// specified by "rocks".
func DrawShatter (
destination artist.Canvas,
source artist.Pattern,
bounds image.Rectangle,
rocks ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
tiles := shatter.Shatter(bounds, rocks...)
return DrawClip(destination, source, bounds, tiles...)
}
// AllocateSample returns a new canvas containing the result of a pattern. The
// resulting canvas can be sourced from shape drawing functions. I beg of you
// please do not call this every time you need to draw a shape with a pattern on
// it because that is horrible and cruel to the computer.
func AllocateSample (source artist.Pattern, width, height int) artist.Canvas {
allocated := artist.NewBasicCanvas(width, height)
Fill(allocated, source)
return allocated
}
// Hex creates a color.RGBA value from an RGBA integer value.
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
}

View File

@ -1,4 +1,4 @@
package canvas
package artist
import "image"
import "image/draw"

View File

@ -1,12 +0,0 @@
package artist
import "image/color"
// Hex creates a color.RGBA value from an RGBA integer value.
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
}

View File

@ -2,12 +2,11 @@ package artist
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
type Icon interface {
// Draw draws the icon to the destination canvas at the specified point,
// using the specified color (if the icon is monochrome).
Draw (destination canvas.Canvas, color color.RGBA, at image.Point)
Draw (destination Canvas, color color.RGBA, at image.Point)
// Bounds returns the bounds of the icon.
Bounds () image.Rectangle

View File

@ -1,8 +1,6 @@
package artist
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
// Pattern is capable of drawing to a canvas within the bounds of a given
// clipping rectangle.
@ -11,51 +9,5 @@ type Pattern interface {
// specified bounds. The given bounds can be smaller or larger than the
// bounds of the destination canvas. The destination canvas can be cut
// using canvas.Cut() to draw only a specific subset of a pattern.
Draw (destination canvas.Canvas, bounds image.Rectangle)
Draw (destination Canvas, bounds image.Rectangle)
}
// Fill fills the destination canvas with the given pattern.
func Fill (destination canvas.Canvas, source Pattern) (updated image.Rectangle) {
source.Draw(destination, destination.Bounds())
return destination.Bounds()
}
// DrawClip lets you draw several subsets of a pattern at once.
func DrawClip (
destination canvas.Canvas,
source Pattern,
bounds image.Rectangle,
subsets ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
for _, subset := range subsets {
source.Draw(canvas.Cut(destination, subset), bounds)
updatedRegion = updatedRegion.Union(subset)
}
return
}
// DrawShatter is like an inverse of DrawClip, drawing nothing in the areas
// specified by "rocks".
func DrawShatter (
destination canvas.Canvas,
source Pattern,
bounds image.Rectangle,
rocks ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
tiles := shatter.Shatter(bounds, rocks...)
return DrawClip(destination, source, bounds, tiles...)
}
// AllocateSample returns a new canvas containing the result of a pattern. The
// resulting canvas can be sourced from shape drawing functions. I beg of you
// please do not call this every time you need to draw a shape with a pattern on
// it because that is horrible and cruel to the computer.
func AllocateSample (source Pattern, width, height int) canvas.Canvas {
allocated := canvas.NewBasicCanvas(width, height)
Fill(allocated, source)
return allocated
}

View File

@ -1,7 +1,6 @@
package patterns
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// Border is a pattern that behaves similarly to border-image in CSS. It divides
@ -31,20 +30,20 @@ import "git.tebibyte.media/sashakoshka/tomo/artist"
// This pattern can be used to make a static image texture into something that
// responds well to being resized.
type Border struct {
canvas.Canvas
artist.Canvas
artist.Inset
}
// Draw draws the border pattern onto the destination canvas within the given
// bounds.
func (pattern Border) Draw (destination canvas.Canvas, bounds image.Rectangle) {
func (pattern Border) Draw (destination artist.Canvas, bounds image.Rectangle) {
drawBounds := bounds.Canon().Intersect(destination.Bounds())
if drawBounds.Empty() { return }
srcSections := nonasect(pattern.Bounds(), pattern.Inset)
srcTextures := [9]Texture { }
for index, section := range srcSections {
srcTextures[index].Canvas = canvas.Cut(pattern, section)
srcTextures[index].Canvas = artist.Cut(pattern, section)
}
dstSections := nonasect(bounds, pattern.Inset)

View File

@ -1,18 +1,18 @@
package patterns
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// Texture is a pattern that tiles the content of a canvas both horizontally and
// vertically.
type Texture struct {
canvas.Canvas
artist.Canvas
}
// Draw tiles the pattern's canvas within the given bounds. The minimum
// point of the pattern's canvas will be lined up with the minimum point of the
// bounding rectangle.
func (pattern Texture) Draw (destination canvas.Canvas, bounds image.Rectangle) {
func (pattern Texture) Draw (destination artist.Canvas, bounds image.Rectangle) {
dstBounds := bounds.Canon().Intersect(destination.Bounds())
if dstBounds.Empty() { return }

View File

@ -2,19 +2,19 @@ package patterns
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
// Uniform is a pattern that draws a solid color.
type Uniform color.RGBA
// Draw fills the bounding rectangle with the pattern's color.
func (pattern Uniform) Draw (destination canvas.Canvas, bounds image.Rectangle) {
func (pattern Uniform) Draw (destination artist.Canvas, bounds image.Rectangle) {
shapes.FillColorRectangle(destination, color.RGBA(pattern), bounds)
}
// Uhex creates a new Uniform pattern from an RGBA integer value.
func Uhex (color uint32) (uniform Uniform) {
return Uniform(artist.Hex(color))
return Uniform(artutil.Hex(color))
}

View File

@ -3,7 +3,7 @@ package shapes
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// TODO: redo fill ellipse, stroke ellipse, etc. so that it only takes in
// destination and source, using the bounds of destination as the bounds of the
@ -11,8 +11,8 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas"
// of both canvases.
func FillEllipse (
destination canvas.Canvas,
source canvas.Canvas,
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
@ -42,8 +42,8 @@ func FillEllipse (
}
func StrokeEllipse (
destination canvas.Canvas,
source canvas.Canvas,
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
weight int,
) {
@ -170,7 +170,7 @@ func (context ellipsePlottingContext) plotEllipse () {
// FillColorEllipse fills an ellipse within the destination canvas with a solid
// color.
func FillColorEllipse (
destination canvas.Canvas,
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
) (
@ -196,7 +196,7 @@ func FillColorEllipse (
// StrokeColorEllipse is similar to FillColorEllipse, but it draws an inset
// outline of an ellipse instead.
func StrokeColorEllipse (
destination canvas.Canvas,
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
weight int,

View File

@ -2,12 +2,12 @@ package shapes
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// ColorLine draws a line from one point to another with the specified weight
// and color.
func ColorLine (
destination canvas.Canvas,
destination artist.Canvas,
color color.RGBA,
weight int,
min image.Point,

View File

@ -2,14 +2,14 @@ package shapes
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
// TODO: return updatedRegion for all routines in this package
func FillRectangle (
destination canvas.Canvas,
source canvas.Canvas,
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
@ -38,8 +38,8 @@ func FillRectangle (
}
func StrokeRectangle (
destination canvas.Canvas,
source canvas.Canvas,
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
weight int,
) (
@ -55,8 +55,8 @@ func StrokeRectangle (
// FillRectangleShatter is like FillRectangle, but it does not draw in areas
// specified in "rocks".
func FillRectangleShatter (
destination canvas.Canvas,
source canvas.Canvas,
destination artist.Canvas,
source artist.Canvas,
bounds image.Rectangle,
rocks ...image.Rectangle,
) (
@ -65,7 +65,7 @@ func FillRectangleShatter (
tiles := shatter.Shatter(bounds, rocks...)
for _, tile := range tiles {
FillRectangle (
canvas.Cut(destination, tile),
artist.Cut(destination, tile),
source, tile)
updatedRegion = updatedRegion.Union(tile)
}
@ -75,7 +75,7 @@ func FillRectangleShatter (
// FillColorRectangle fills a rectangle within the destination canvas with a
// solid color.
func FillColorRectangle (
destination canvas.Canvas,
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
) (
@ -97,7 +97,7 @@ func FillColorRectangle (
// FillColorRectangleShatter is like FillColorRectangle, but it does not draw in
// areas specified in "rocks".
func FillColorRectangleShatter (
destination canvas.Canvas,
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
rocks ...image.Rectangle,
@ -115,7 +115,7 @@ func FillColorRectangleShatter (
// StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset
// outline of the given rectangle instead.
func StrokeColorRectangle (
destination canvas.Canvas,
destination artist.Canvas,
color color.RGBA,
bounds image.Rectangle,
weight int,

View File

@ -1,7 +1,6 @@
package tomo
import "image"
import "errors"
// Backend represents a connection to a display server, or something similar.
// It is capable of managing an event loop, and creating windows.
@ -32,33 +31,21 @@ type Backend interface {
SetConfig (Config)
}
// BackendFactory represents a function capable of constructing a backend
// struct. Any connections should be initialized within this function. If there
// any errors encountered during this process, the function should immediately
// stop, clean up any resources, and return an error.
type BackendFactory func () (backend Backend, err error)
var backend Backend
// RegisterBackend registers a backend factory. When an application calls
// tomo.Run(), the first registered backend that does not throw an error will be
// used.
func RegisterBackend (factory BackendFactory) {
factories = append(factories, factory)
// GetBackend returns the currently running backend.
func GetBackend () Backend {
return backend
}
var factories []BackendFactory
func instantiateBackend () (backend Backend, err error) {
// find a suitable backend
for _, factory := range factories {
backend, err = factory()
if err == nil && backend != nil { return }
}
// if none were found, but there was no error produced, produce an
// error
if err == nil {
err = errors.New("no available backends")
}
return
// SetBackend sets the currently running backend. The backend can only be set
// once—if there already is one then this function will do nothing.
func SetBackend (b Backend) {
if backend != nil { return }
backend = b
}
// Bounds creates a rectangle from an x, y, width, and height.
func Bounds (x, y, width, height int) image.Rectangle {
return image.Rect(x, y, x + width, y + height)
}

View File

@ -1,4 +0,0 @@
// Package all links most common backends.
package all
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"

View File

@ -1,3 +0,0 @@
// Package backends contains sub-packages that register backends with tomo when
// linked into a program.
package backends

View File

@ -1,4 +0,0 @@
// Package canvas provides a canvas interface that is able to return a pixel
// buffer for drawing. This makes it considerably more efficient than the
// standard draw.Image.
package canvas

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

View File

@ -9,14 +9,14 @@ import "golang.org/x/image/font"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
//go:embed assets/wintergreen.png
//go:embed assets/default.png
var defaultAtlasBytes []byte
var defaultAtlas canvas.Canvas
var defaultTextures [17][9]artist.Pattern
var defaultAtlas artist.Canvas
var defaultTextures [7][7]artist.Pattern
//go:embed assets/wintergreen-icons-small.png
var defaultIconsSmallAtlasBytes []byte
var defaultIconsSmall [640]binaryIcon
@ -25,9 +25,9 @@ var defaultIconsLargeAtlasBytes []byte
var defaultIconsLarge [640]binaryIcon
func atlasCell (col, row int, border artist.Inset) {
bounds := image.Rect(0, 0, 16, 16).Add(image.Pt(col, row).Mul(16))
bounds := image.Rect(0, 0, 8, 8).Add(image.Pt(col, row).Mul(8))
defaultTextures[col][row] = patterns.Border {
Canvas: canvas.Cut(defaultAtlas, bounds),
Canvas: artist.Cut(defaultAtlas, bounds),
Inset: border,
}
}
@ -43,7 +43,7 @@ type binaryIcon struct {
stride int
}
func (icon binaryIcon) Draw (destination canvas.Canvas, color color.RGBA, at image.Point) {
func (icon binaryIcon) Draw (destination artist.Canvas, color color.RGBA, at image.Point) {
bounds := icon.Bounds().Add(at).Intersect(destination.Bounds())
point := image.Point { }
data, stride := destination.Buffer()
@ -85,43 +85,15 @@ func binaryIconFrom (source image.Image, clip image.Rectangle) (icon binaryIcon)
func init () {
defaultAtlasImage, _, _ := image.Decode(bytes.NewReader(defaultAtlasBytes))
defaultAtlas = canvas.FromImage(defaultAtlasImage)
defaultAtlas = artist.FromImage(defaultAtlasImage)
// PatternDead
atlasCol(0, artist.Inset { })
// PatternRaised
atlasCol(1, artist.Inset { 6, 6, 6, 6 })
// PatternSunken
atlasCol(2, artist.Inset { 4, 4, 4, 4 })
// PatternPinboard
atlasCol(3, artist.Inset { 2, 2, 2, 2 })
// PatternButton
atlasCol(4, artist.Inset { 6, 6, 6, 6 })
// PatternInput
atlasCol(5, artist.Inset { 4, 4, 4, 4 })
// PatternGutter
atlasCol(6, artist.Inset { 7, 7, 7, 7 })
// PatternHandle
atlasCol(7, artist.Inset { 3, 3, 3, 3 })
// PatternLine
atlasCol(8, artist.Inset { 1, 1, 1, 1 })
// PatternMercury
atlasCol(13, artist.Inset { 2, 2, 2, 2 })
// PatternTableHead:
atlasCol(14, artist.Inset { 4, 4, 4, 4 })
// PatternTableCell:
atlasCol(15, artist.Inset { 4, 4, 4, 4 })
// PatternLamp:
atlasCol(16, artist.Inset { 4, 3, 4, 3 })
// PatternButton: basic.checkbox
atlasCol(9, artist.Inset { 3, 3, 3, 3 })
// PatternRaised: basic.listEntry
atlasCol(10, artist.Inset { 3, 3, 3, 3 })
// PatternRaised: fun.flatKey
atlasCol(11, artist.Inset { 3, 3, 5, 3 })
// PatternRaised: fun.sharpKey
atlasCol(12, artist.Inset { 3, 3, 4, 3 })
atlasCol(0, artist.I(0))
atlasCol(1, artist.I(3))
atlasCol(2, artist.I(1))
atlasCol(3, artist.I(1))
atlasCol(4, artist.I(1))
atlasCol(5, artist.I(3))
atlasCol(6, artist.I(1))
// set up small icons
defaultIconsSmallAtlasImage, _, _ := image.Decode (
@ -217,33 +189,23 @@ func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.P
case tomo.PatternRaised: return defaultTextures[1][offset]
case tomo.PatternSunken: return defaultTextures[2][offset]
case tomo.PatternPinboard: return defaultTextures[3][offset]
case tomo.PatternButton:
switch {
case c.Match("tomo", "checkbox", ""):
return defaultTextures[9][offset]
case c.Match("tomo", "piano", "flatKey"):
return defaultTextures[11][offset]
case c.Match("tomo", "piano", "sharpKey"):
return defaultTextures[12][offset]
default:
return defaultTextures[4][offset]
}
case tomo.PatternInput: return defaultTextures[5][offset]
case tomo.PatternGutter: return defaultTextures[6][offset]
case tomo.PatternHandle: return defaultTextures[7][offset]
case tomo.PatternLine: return defaultTextures[8][offset]
case tomo.PatternMercury: return defaultTextures[13][offset]
case tomo.PatternTableHead: return defaultTextures[14][offset]
case tomo.PatternTableCell: return defaultTextures[15][offset]
case tomo.PatternLamp: return defaultTextures[16][offset]
default: return patterns.Uhex(0xFF00FFFF)
case tomo.PatternButton: return defaultTextures[1][offset]
case tomo.PatternInput: return defaultTextures[2][offset]
case tomo.PatternGutter: return defaultTextures[2][offset]
case tomo.PatternHandle: return defaultTextures[3][offset]
case tomo.PatternLine: return defaultTextures[0][offset]
case tomo.PatternMercury: return defaultTextures[4][offset]
case tomo.PatternTableHead: return defaultTextures[5][offset]
case tomo.PatternTableCell: return defaultTextures[5][offset]
case tomo.PatternLamp: return defaultTextures[6][offset]
default: return patterns.Uhex(0xFF00FFFF)
}
}
func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA {
if state.Disabled { return artist.Hex(0x444444FF) }
if state.Disabled { return artutil.Hex(0x444444FF) }
return artist.Hex (map[tomo.Color] uint32 {
return artutil.Hex (map[tomo.Color] uint32 {
tomo.ColorBlack: 0x272d24FF,
tomo.ColorRed: 0x8c4230FF,
tomo.ColorGreen: 0x69905fFF,
@ -262,56 +224,26 @@ func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA {
tomo.ColorBrightWhite: 0xcfd7d2FF,
tomo.ColorForeground: 0x000000FF,
tomo.ColorMidground: 0x97A09BFF,
tomo.ColorMidground: 0x656565FF,
tomo.ColorBackground: 0xAAAAAAFF,
tomo.ColorShadow: 0x445754FF,
tomo.ColorShine: 0xCFD7D2FF,
tomo.ColorAccent: 0x408090FF,
tomo.ColorShadow: 0x000000FF,
tomo.ColorShine: 0xFFFFFFFF,
tomo.ColorAccent: 0xff3300FF,
} [id])
}
// Padding returns the default padding value for the given pattern.
func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
switch id {
case tomo.PatternSunken:
if c.Match("tomo", "progressBar", "") {
return artist.I(2, 1, 1, 2)
} else if c.Match("tomo", "list", "") {
return artist.I(2)
} else if c.Match("tomo", "flowList", "") {
return artist.I(2)
} else {
return artist.I(8)
}
case tomo.PatternPinboard:
if c.Match("tomo", "piano", "") {
return artist.I(2)
} else {
return artist.I(8)
}
case tomo.PatternTableCell: return artist.I(5)
case tomo.PatternTableHead: return artist.I(5)
case tomo.PatternGutter: return artist.I(0)
case tomo.PatternLine: return artist.I(1)
case tomo.PatternMercury: return artist.I(5)
case tomo.PatternLamp: return artist.I(5, 5, 5, 6)
default: return artist.I(8)
case tomo.PatternGutter: return artist.I(0)
case tomo.PatternLine: return artist.I(1)
default: return artist.I(6)
}
}
// Margin returns the default margin value for the given pattern.
func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point {
switch id {
case tomo.PatternSunken:
if c.Match("tomo", "list", "") {
return image.Pt(-1, -1)
} else if c.Match("tomo", "flowList", "") {
return image.Pt(-1, -1)
} else {
return image.Pt(8, 8)
}
default: return image.Pt(8, 8)
}
return image.Pt(6, 6)
}
// Hints returns rendering optimization hints for a particular pattern.

View File

@ -1,9 +0,0 @@
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,82 +0,0 @@
package theme
import "image"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// 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 {
tomo.Theme
tomo.Case
}
// FontFace returns the proper font for a given style and size.
func (wrapped Wrapped) FontFace (style tomo.FontStyle, size tomo.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 (id tomo.Icon, size tomo.IconSize) artist.Icon {
real := wrapped.ensure()
return real.Icon(id, size, wrapped.Case)
}
// MimeIcon returns an appropriate icon given file mime type.
func (wrapped Wrapped) MimeIcon (mime data.Mime, size tomo.IconSize) artist.Icon {
real := wrapped.ensure()
return real.MimeIcon(mime, size, wrapped.Case)
}
// Pattern returns an appropriate pattern given a pattern name and state.
func (wrapped Wrapped) Pattern (id tomo.Pattern, state tomo.State) artist.Pattern {
real := wrapped.ensure()
return real.Pattern(id, state, wrapped.Case)
}
// Color returns an appropriate color given a color name and state.
func (wrapped Wrapped) Color (id tomo.Color, state tomo.State) color.RGBA {
real := wrapped.ensure()
return real.Color(id, state, wrapped.Case)
}
// Padding returns how much space should be between the bounds of a
// pattern whatever an element draws inside of it.
func (wrapped Wrapped) Padding (id tomo.Pattern) artist.Inset {
real := wrapped.ensure()
return real.Padding(id, wrapped.Case)
}
// Margin returns the left/right (x) and top/bottom (y) margins that
// should be put between any self-contained objects drawn within this
// pattern (if applicable).
func (wrapped Wrapped) Margin (id tomo.Pattern) image.Point {
real := wrapped.ensure()
return real.Margin(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 tomo.Pattern) image.Point {
real := wrapped.ensure()
return real.Sink(id, wrapped.Case)
}
// Hints returns rendering optimization hints for a particular pattern.
// These are optional, but following them may result in improved
// performance.
func (wrapped Wrapped) Hints (id tomo.Pattern) tomo.Hints {
real := wrapped.ensure()
return real.Hints(id, wrapped.Case)
}
func (wrapped Wrapped) ensure () (real tomo.Theme) {
real = wrapped.Theme
if real == nil { real = Default { } }
return
}

View File

@ -1,251 +1,15 @@
package tomo
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// Element represents a basic on-screen object.
// Element represents a basic on-screen object. Extended element interfaces are
// defined in the ability module.
type Element interface {
// Draw causes the element to draw to the specified canvas. The bounds
// of this canvas specify the area that is actually drawn to, while the
// Entity bounds specify the actual area of the element.
Draw (canvas.Canvas)
Draw (artist.Canvas)
// Entity returns this element's entity.
Entity () Entity
}
// Layoutable represents an element that needs to perform layout calculations
// before it can draw itself.
type Layoutable interface {
Element
// Layout causes this element to perform a layout operation.
Layout ()
}
// Container represents an element capable of containing child elements.
type Container interface {
Element
Layoutable
// DrawBackground causes the element to draw its background pattern to
// the specified canvas. The bounds of this canvas specify the area that
// is actually drawn to, while the Entity bounds specify the actual area
// of the element.
DrawBackground (canvas.Canvas)
// HandleChildMinimumSizeChange is called when a child's minimum size is
// changed.
HandleChildMinimumSizeChange (child Element)
}
// Enableable represents an element that can be enabled and disabled. Disabled
// elements typically appear greyed out.
type Enableable interface {
Element
// Enabled returns whether or not the element is enabled.
Enabled () bool
// SetEnabled sets whether or not the element is enabled.
SetEnabled (bool)
}
// Focusable represents an element that has keyboard navigation support.
type Focusable interface {
Element
Enableable
// HandleFocusChange is called when the element is focused or unfocused.
HandleFocusChange ()
}
// Selectable represents an element that can be selected. This includes things
// like list items, files, etc. The difference between this and Focusable is
// that multiple Selectable elements may be selected at the same time, whereas
// only one Focusable element may be focused at the same time. Containers who's
// purpose is to contain selectable elements can determine when to select them
// by implementing MouseTargetContainer and listening for HandleChildMouseDown
// events.
type Selectable interface {
Element
Enableable
// HandleSelectionChange is called when the element is selected or
// deselected.
HandleSelectionChange ()
}
// KeyboardTarget represents an element that can receive keyboard input.
type KeyboardTarget interface {
Element
// HandleKeyDown is called when a key is pressed down or repeated while
// this element has keyboard focus. It is important to note that not
// every key down event is guaranteed to be paired with exactly one key
// up event. This is the reason a list of modifier keys held down at the
// time of the key press is given.
HandleKeyDown (key input.Key, modifiers input.Modifiers)
// HandleKeyUp is called when a key is released while this element has
// keyboard focus.
HandleKeyUp (key input.Key, modifiers input.Modifiers)
}
// MouseTarget represents an element that can receive mouse events.
type MouseTarget interface {
Element
// HandleMouseDown is called when a mouse button is pressed down on this
// element.
HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on this element.
HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers)
}
// MouseTargetContainer represents an element that wants to know when one
// of its children is clicked. Children do not have to implement MouseTarget for
// a container satisfying MouseTargetContainer to be notified that they have
// been clicked.
type MouseTargetContainer interface {
Container
// HandleMouseDown is called when a mouse button is pressed down on a
// child element.
HandleChildMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child Element)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on a child element.
HandleChildMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child Element)
}
// MotionTarget represents an element that can receive mouse motion events.
type MotionTarget interface {
Element
// HandleMotion is called when the mouse is moved over this element,
// or the mouse is moving while being held down and originally pressed
// down on this element.
HandleMotion (position image.Point)
}
// ScrollTarget represents an element that can receive mouse scroll events.
type ScrollTarget interface {
Element
// HandleScroll is called when the mouse is scrolled. The X and Y
// direction of the scroll event are passed as deltaX and deltaY.
HandleScroll (
position image.Point,
deltaX, deltaY float64,
modifiers input.Modifiers)
}
// Flexible represents an element who's preferred minimum height can change in
// response to its width.
type Flexible interface {
Element
// FlexibleHeightFor returns what the element's minimum height would be
// if resized to a specified width. This does not actually alter the
// state of the element in any way, but it may perform significant work,
// so it should be called sparingly.
//
// It is reccomended that parent containers check for this interface and
// take this method's value into account in order to support things like
// flow layouts and text wrapping, but it is not absolutely necessary.
// The element's MinimumSize method will still return the absolute
// minimum size that the element may be resized to.
//
// It is important to note that if a parent container checks for
// flexible chilren, it itself will likely need to be either scrollable
// or flexible.
FlexibleHeightFor (width int) int
}
// FlexibleContainer represents an element that is capable of containing
// flexible children.
type FlexibleContainer interface {
Container
// HandleChildFlexibleHeightChange is called when the parameters
// affecting a child's flexible height are changed.
HandleChildFlexibleHeightChange (child Flexible)
}
// Scrollable represents an element that can be scrolled. It acts as a viewport
// through which its contents can be observed.
type Scrollable interface {
Element
// ScrollContentBounds returns the full content size of the element.
ScrollContentBounds () image.Rectangle
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
ScrollViewportBounds () image.Rectangle
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
ScrollTo (position image.Point)
// ScrollAxes returns the supported axes for scrolling.
ScrollAxes () (horizontal, vertical bool)
}
// ScrollableContainer represents an element that is capable of containing
// scrollable children.
type ScrollableContainer interface {
Container
// HandleChildScrollBoundsChange is called when the content bounds,
// viewport bounds, or scroll axes of a child are changed.
HandleChildScrollBoundsChange (child Scrollable)
}
// Collapsible represents an element who's minimum width and height can be
// manually resized. Scrollable elements should implement this if possible.
type Collapsible interface {
Element
// Collapse collapses the element's minimum width and height. A value of
// zero for either means that the element's normal value is used.
Collapse (width, height int)
}
// 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)
}
// 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)
}

View File

@ -2,9 +2,10 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
var boxCase = tomo.C("tomo", "box")
// Space is a list of spacing configurations that can be passed to some
// containers.
@ -27,7 +28,6 @@ func (space Space) Includes (sub Space) bool {
// complex layouts.
type Box struct {
container
theme theme.Wrapped
padding bool
margin bool
vertical bool
@ -39,10 +39,9 @@ func NewHBox (space Space, children ...tomo.Element) (element *Box) {
padding: space.Includes(SpacePadding),
margin: space.Includes(SpaceMargin),
}
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
element.entity = tomo.GetBackend().NewEntity(element)
element.minimumSize = element.updateMinimumSize
element.init()
element.theme.Case = tomo.C("tomo", "box")
element.Adopt(children...)
return
}
@ -54,16 +53,15 @@ func NewVBox (space Space, children ...tomo.Element) (element *Box) {
margin: space.Includes(SpaceMargin),
vertical: true,
}
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
element.entity = tomo.GetBackend().NewEntity(element)
element.minimumSize = element.updateMinimumSize
element.init()
element.theme.Case = tomo.C("tomo", "box")
element.Adopt(children...)
return
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Box) Draw (destination canvas.Canvas) {
func (element *Box) Draw (destination artist.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
@ -71,14 +69,14 @@ func (element *Box) Draw (destination canvas.Canvas) {
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles {
element.entity.DrawBackground(canvas.Cut(destination, tile))
element.entity.DrawBackground(artist.Cut(destination, tile))
}
}
// Layout causes this element to perform a layout operation.
func (element *Box) Layout () {
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
bounds := element.entity.Bounds()
if element.padding { bounds = padding.Apply(bounds) }
@ -128,22 +126,19 @@ func (element *Box) AdoptExpand (children ...tomo.Element) {
// DrawBackground draws this element's background pattern to the specified
// destination canvas.
func (element *Box) DrawBackground (destination canvas.Canvas) {
func (element *Box) DrawBackground (destination artist.Canvas) {
element.entity.DrawBackground(destination)
}
// SetTheme sets the element's theme.
func (element *Box) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
func (element *Box) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Box) freeSpace () (space float64, nExpanding float64) {
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
var marginSize int; if element.vertical {
marginSize = margin.Y
@ -176,8 +171,8 @@ func (element *Box) freeSpace () (space float64, nExpanding float64) {
}
func (element *Box) updateMinimumSize () {
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
var breadth, size int
var marginSize int; if element.vertical {
marginSize = margin.Y

View File

@ -3,22 +3,19 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
var buttonCase = tomo.C("tomo", "button")
// Button is a clickable button.
type Button struct {
entity tomo.FocusableEntity
entity tomo.Entity
drawer textdraw.Drawer
enabled bool
pressed bool
text string
config config.Wrapped
theme theme.Wrapped
showText bool
hasIcon bool
@ -30,11 +27,11 @@ type Button struct {
// NewButton creates a new button with the specified label text.
func NewButton (text string) (element *Button) {
element = &Button { showText: true, enabled: true }
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.theme.Case = tomo.C("tomo", "button")
element.drawer.SetFace (element.theme.FontFace (
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
tomo.FontSizeNormal,
buttonCase))
element.SetText(text)
return
}
@ -45,16 +42,16 @@ func (element *Button) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Button) Draw (destination canvas.Canvas) {
func (element *Button) Draw (destination artist.Canvas) {
state := element.state()
bounds := element.entity.Bounds()
pattern := element.theme.Pattern(tomo.PatternButton, state)
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, buttonCase)
pattern.Draw(destination, bounds)
foreground := element.theme.Color(tomo.ColorForeground, state)
sink := element.theme.Sink(tomo.PatternButton)
margin := element.theme.Margin(tomo.PatternButton)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, buttonCase)
sink := element.entity.Theme().Sink(tomo.PatternButton, buttonCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, buttonCase)
offset := image.Pt (
bounds.Dx() / 2,
@ -69,7 +66,7 @@ func (element *Button) Draw (destination canvas.Canvas) {
}
if element.hasIcon {
icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall)
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase)
if icon != nil {
iconBounds := icon.Bounds()
addedWidth := iconBounds.Dx()
@ -154,21 +151,11 @@ func (element *Button) ShowText (showText bool) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Button) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
func (element *Button) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Button) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
tomo.FontSizeNormal,
buttonCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
@ -223,14 +210,14 @@ func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
}
func (element *Button) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternButton)
margin := element.theme.Margin(tomo.PatternButton)
padding := element.entity.Theme().Padding(tomo.PatternButton, buttonCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, buttonCase)
textBounds := element.drawer.LayoutBounds()
minimumSize := textBounds.Sub(textBounds.Min)
if element.hasIcon {
icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall)
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase)
if icon != nil {
bounds := icon.Bounds()
if element.showText {

View File

@ -1,22 +1,17 @@
package elements
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
type cellEntity interface {
tomo.ContainerEntity
tomo.SelectableEntity
}
var cellCase = tomo.C("tomo", "cell")
// Cell is a single-element container that satisfies tomo.Selectable. It
// provides styling based on whether or not it is selected.
type Cell struct {
entity cellEntity
entity tomo.Entity
child tomo.Element
enabled bool
theme theme.Wrapped
onSelectionChange func ()
}
@ -26,8 +21,7 @@ type Cell struct {
// method.
func NewCell (child tomo.Element) (element *Cell) {
element = &Cell { enabled: true }
element.theme.Case = tomo.C("tomo", "cell")
element.entity = tomo.NewEntity(element).(cellEntity)
element.entity = tomo.GetBackend().NewEntity(element)
element.Adopt(child)
return
}
@ -38,13 +32,13 @@ func (element *Cell) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Cell) Draw (destination canvas.Canvas) {
func (element *Cell) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
pattern := element.theme.Pattern(tomo.PatternTableCell, element.state())
pattern := element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase)
if element.child == nil {
pattern.Draw(destination, bounds)
} else {
artist.DrawShatter (
artutil.DrawShatter (
destination, pattern, bounds,
element.child.Entity().Bounds())
}
@ -55,15 +49,15 @@ func (element *Cell) Layout () {
if element.child == nil { return }
bounds := element.entity.Bounds()
bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds)
bounds = element.entity.Theme().Padding(tomo.PatternTableCell, cellCase).Apply(bounds)
element.entity.PlaceChild(0, bounds)
}
// DrawBackground draws this element's background pattern to the specified
// destination canvas.
func (element *Cell) DrawBackground (destination canvas.Canvas) {
element.theme.Pattern(tomo.PatternTableCell, element.state()).
func (element *Cell) DrawBackground (destination artist.Canvas) {
element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase).
Draw(destination, element.entity.Bounds())
}
@ -102,16 +96,6 @@ func (element *Cell) SetEnabled (enabled bool) {
element.invalidateChild()
}
// SetTheme sets this element's theme.
func (element *Cell) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize()
element.entity.Invalidate()
element.invalidateChild()
element.entity.InvalidateLayout()
}
// OnSelectionChange sets a function to be called when this element is selected
// or unselected.
func (element *Cell) OnSelectionChange (callback func ()) {
@ -122,6 +106,13 @@ func (element *Cell) Selected () bool {
return element.entity.Selected()
}
func (element *Cell) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.invalidateChild()
element.entity.InvalidateLayout()
}
func (element *Cell) HandleSelectionChange () {
element.entity.Invalidate()
element.invalidateChild()
@ -151,7 +142,7 @@ func (element *Cell) updateMinimumSize () {
width += childWidth
height += childHeight
}
padding := element.theme.Padding(tomo.PatternTableCell)
padding := element.entity.Theme().Padding(tomo.PatternTableCell, cellCase)
width += padding.Horizontal()
height += padding.Vertical()

View File

@ -3,14 +3,14 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
var checkboxCase = tomo.C("tomo", "checkbox")
// Checkbox is a toggle-able checkbox with a label.
type Checkbox struct {
entity tomo.FocusableEntity
entity tomo.Entity
drawer textdraw.Drawer
enabled bool
@ -18,20 +18,17 @@ type Checkbox struct {
checked bool
text string
config config.Wrapped
theme theme.Wrapped
onToggle func ()
}
// NewCheckbox creates a new cbeckbox with the specified label text.
func NewCheckbox (text string, checked bool) (element *Checkbox) {
element = &Checkbox { checked: checked, enabled: true }
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.theme.Case = tomo.C("tomo", "checkbox")
element.drawer.SetFace (element.theme.FontFace (
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
tomo.FontSizeNormal,
checkboxCase))
element.SetText(text)
return
}
@ -42,7 +39,7 @@ func (element *Checkbox) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Checkbox) Draw (destination canvas.Canvas) {
func (element *Checkbox) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
@ -55,11 +52,11 @@ func (element *Checkbox) Draw (destination canvas.Canvas) {
element.entity.DrawBackground(destination)
pattern := element.theme.Pattern(tomo.PatternButton, state)
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, checkboxCase)
pattern.Draw(destination, boxBounds)
textBounds := element.drawer.LayoutBounds()
margin := element.theme.Margin(tomo.PatternBackground)
margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase)
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() + margin.X,
})
@ -67,7 +64,7 @@ func (element *Checkbox) Draw (destination canvas.Canvas) {
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground := element.theme.Color(tomo.ColorForeground, state)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, checkboxCase)
element.drawer.Draw(destination, foreground, offset)
}
@ -107,21 +104,11 @@ func (element *Checkbox) SetText (text string) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Checkbox) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
func (element *Checkbox) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Checkbox) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
tomo.FontSizeNormal,
checkboxCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
@ -183,7 +170,7 @@ func (element *Checkbox) updateMinimumSize () {
if element.text == "" {
element.entity.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
} else {
margin := element.theme.Margin(tomo.PatternBackground)
margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase)
element.entity.SetMinimumSize (
textBounds.Dy() + margin.X + textBounds.Dx(),
textBounds.Dy())

View File

@ -3,11 +3,12 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/ability"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
var comboBoxCase = tomo.C("tomo", "comboBox")
// Option specifies a ComboBox option. A blank option will display as "(None)".
type Option string
@ -21,7 +22,7 @@ func (option Option) Title () string {
// ComboBox is an input that can be one of several predetermined values.
type ComboBox struct {
entity tomo.FocusableEntity
entity tomo.Entity
drawer textdraw.Drawer
options []Option
@ -30,9 +31,6 @@ type ComboBox struct {
enabled bool
pressed bool
config config.Wrapped
theme theme.Wrapped
onChange func ()
}
@ -40,11 +38,11 @@ type ComboBox struct {
func NewComboBox (options ...Option) (element *ComboBox) {
if len(options) == 0 { options = []Option { "" } }
element = &ComboBox { enabled: true, options: options }
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.theme.Case = tomo.C("tomo", "comboBox")
element.drawer.SetFace (element.theme.FontFace (
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
tomo.FontSizeNormal,
comboBoxCase))
element.Select(options[0])
return
}
@ -55,17 +53,17 @@ func (element *ComboBox) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ComboBox) Draw (destination canvas.Canvas) {
func (element *ComboBox) Draw (destination artist.Canvas) {
state := element.state()
bounds := element.entity.Bounds()
pattern := element.theme.Pattern(tomo.PatternButton, state)
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, comboBoxCase)
pattern.Draw(destination, bounds)
foreground := element.theme.Color(tomo.ColorForeground, state)
sink := element.theme.Sink(tomo.PatternButton)
margin := element.theme.Margin(tomo.PatternButton)
padding := element.theme.Padding(tomo.PatternButton)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, comboBoxCase)
sink := element.entity.Theme().Sink(tomo.PatternButton, comboBoxCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, comboBoxCase)
padding := element.entity.Theme().Padding(tomo.PatternButton, comboBoxCase)
offset := image.Pt(0, bounds.Dy() / 2).Add(bounds.Min)
@ -74,7 +72,7 @@ func (element *ComboBox) Draw (destination canvas.Canvas) {
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
icon := element.theme.Icon(tomo.IconExpand, tomo.IconSizeSmall)
icon := element.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase)
if icon != nil {
iconBounds := icon.Bounds()
addedWidth := iconBounds.Dx() + margin.X
@ -142,21 +140,11 @@ func (element *ComboBox) SetEnabled (enabled bool) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *ComboBox) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
func (element *ComboBox) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *ComboBox) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
tomo.FontSizeNormal,
comboBoxCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
@ -228,7 +216,7 @@ func (element *ComboBox) dropDown () {
menu, err := window.NewMenu(element.entity.Bounds())
if err != nil { return }
cellToOption := make(map[tomo.Selectable] Option)
cellToOption := make(map[ability.Selectable] Option)
list := NewList()
for _, option := range element.options {
@ -254,13 +242,13 @@ func (element *ComboBox) dropDown () {
}
func (element *ComboBox) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternButton)
margin := element.theme.Margin(tomo.PatternButton)
padding := element.entity.Theme().Padding(tomo.PatternButton, comboBoxCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, comboBoxCase)
textBounds := element.drawer.LayoutBounds()
minimumSize := textBounds.Sub(textBounds.Min)
icon := element.theme.Icon(tomo.IconExpand, tomo.IconSizeSmall)
icon := element.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase)
if icon != nil {
bounds := icon.Bounds()
minimumSize.Max.X += bounds.Dx()

View File

@ -9,7 +9,7 @@ type scratchEntry struct {
}
type container struct {
entity tomo.ContainerEntity
entity tomo.Entity
scratch map[tomo.Element] scratchEntry
minimumSize func ()
}

View File

@ -4,17 +4,14 @@ import "image"
import "path/filepath"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/ability"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
// TODO: base on flow implementation of list. also be able to switch to a table
// variant for a more information dense view.
type directoryEntity interface {
tomo.ContainerEntity
tomo.ScrollableEntity
}
var directoryCase = tomo.C("tomo", "list")
type historyEntry struct {
location string
@ -25,8 +22,7 @@ type historyEntry struct {
// file system.
type Directory struct {
container
entity directoryEntity
theme theme.Wrapped
entity tomo.Entity
scroll image.Point
contentBounds image.Rectangle
@ -48,8 +44,7 @@ func NewDirectory (
err error,
) {
element = &Directory { }
element.theme.Case = tomo.C("tomo", "list")
element.entity = tomo.NewEntity(element).(directoryEntity)
element.entity = tomo.GetBackend().NewEntity(element)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init()
@ -57,7 +52,7 @@ func NewDirectory (
return
}
func (element *Directory) Draw (destination canvas.Canvas) {
func (element *Directory) Draw (destination artist.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
@ -65,7 +60,7 @@ func (element *Directory) Draw (destination canvas.Canvas) {
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles {
element.DrawBackground(canvas.Cut(destination, tile))
element.DrawBackground(artist.Cut(destination, tile))
}
}
@ -74,8 +69,8 @@ func (element *Directory) Layout () {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.theme.Margin(tomo.PatternPinboard)
padding := element.theme.Padding(tomo.PatternPinboard)
margin := element.entity.Theme().Margin(tomo.PatternPinboard, directoryCase)
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
@ -99,7 +94,7 @@ func (element *Directory) Layout () {
if width + dot.X > bounds.Max.X {
nextLine()
}
if typedChild, ok := child.(tomo.Flexible); ok {
if typedChild, ok := child.(ability.Flexible); ok {
height = typedChild.FlexibleHeightFor(width)
}
if rowHeight < height {
@ -145,7 +140,7 @@ func (element *Directory) HandleChildMouseDown (
child tomo.Element,
) {
element.selectNone()
if child, ok := child.(tomo.Selectable); ok {
if child, ok := child.(ability.Selectable); ok {
index := element.entity.IndexOf(child)
element.entity.SelectChild(index, true)
}
@ -158,7 +153,7 @@ func (element *Directory) HandleChildMouseUp (
child tomo.Element,
) { }
func (element *Directory) HandleChildFlexibleHeightChange (child tomo.Flexible) {
func (element *Directory) HandleChildFlexibleHeightChange (child ability.Flexible) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
@ -172,7 +167,7 @@ func (element *Directory) ScrollContentBounds () image.Rectangle {
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *Directory) ScrollViewportBounds () image.Rectangle {
padding := element.theme.Padding(tomo.PatternPinboard)
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
@ -204,15 +199,12 @@ func (element *Directory) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *Directory) DrawBackground (destination canvas.Canvas) {
element.theme.Pattern(tomo.PatternPinboard, tomo.State { }).
func (element *Directory) DrawBackground (destination artist.Canvas) {
element.entity.Theme().Pattern(tomo.PatternPinboard, tomo.State { }, directoryCase).
Draw(destination, element.entity.Bounds())
}
// SetTheme sets the element's theme.
func (element *Directory) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
func (element *Directory) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
@ -301,7 +293,7 @@ func (element *Directory) selectNone () {
}
func (element *Directory) maxScrollHeight () (height int) {
padding := element.theme.Padding(tomo.PatternSunken)
padding := element.entity.Theme().Padding(tomo.PatternSunken, directoryCase)
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
@ -310,7 +302,7 @@ func (element *Directory) maxScrollHeight () (height int) {
func (element *Directory) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternPinboard)
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
minimumWidth := 0
for index := 0; index < element.entity.CountChildren(); index ++ {
width, height := element.entity.ChildMinimumSize(index)

View File

@ -2,26 +2,21 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/ability"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
type documentEntity interface {
tomo.ContainerEntity
tomo.ScrollableEntity
}
var documentCase = tomo.C("tomo", "document")
// Document is a scrollable container capcable of laying out flexible child
// elements. Children can be added either inline (similar to an HTML/CSS inline
// element), or expanding (similar to an HTML/CSS block element).
type Document struct {
container
entity documentEntity
entity tomo.Entity
scroll image.Point
contentBounds image.Rectangle
theme theme.Wrapped
onScrollBoundsChange func ()
}
@ -29,8 +24,7 @@ type Document struct {
// NewDocument creates a new document container.
func NewDocument (children ...tomo.Element) (element *Document) {
element = &Document { }
element.theme.Case = tomo.C("tomo", "document")
element.entity = tomo.NewEntity(element).(documentEntity)
element.entity = tomo.GetBackend().NewEntity(element)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init()
@ -39,7 +33,7 @@ func NewDocument (children ...tomo.Element) (element *Document) {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Document) Draw (destination canvas.Canvas) {
func (element *Document) Draw (destination artist.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
@ -47,7 +41,7 @@ func (element *Document) Draw (destination canvas.Canvas) {
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles {
element.entity.DrawBackground(canvas.Cut(destination, tile))
element.entity.DrawBackground(artist.Cut(destination, tile))
}
}
@ -57,8 +51,8 @@ func (element *Document) Layout () {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
margin := element.entity.Theme().Margin(tomo.PatternBackground, documentCase)
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
@ -89,7 +83,7 @@ func (element *Document) Layout () {
if width < bounds.Dx() && entry.expand {
width = bounds.Dx()
}
if typedChild, ok := child.(tomo.Flexible); ok {
if typedChild, ok := child.(ability.Flexible); ok {
height = typedChild.FlexibleHeightFor(width)
}
if rowHeight < height {
@ -130,7 +124,7 @@ func (element *Document) AdoptInline (children ...tomo.Element) {
element.adopt(false, children...)
}
func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) {
func (element *Document) HandleChildFlexibleHeightChange (child ability.Flexible) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
@ -138,14 +132,11 @@ func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) {
// DrawBackground draws this element's background pattern to the specified
// destination canvas.
func (element *Document) DrawBackground (destination canvas.Canvas) {
func (element *Document) DrawBackground (destination artist.Canvas) {
element.entity.DrawBackground(destination)
}
// SetTheme sets the element's theme.
func (element *Document) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
func (element *Document) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
@ -159,7 +150,7 @@ func (element *Document) ScrollContentBounds () image.Rectangle {
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *Document) ScrollViewportBounds () image.Rectangle {
padding := element.theme.Padding(tomo.PatternBackground)
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
@ -192,7 +183,7 @@ func (element *Document) ScrollAxes () (horizontal, vertical bool) {
}
func (element *Document) maxScrollHeight () (height int) {
padding := element.theme.Padding(tomo.PatternSunken)
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
@ -200,7 +191,7 @@ func (element *Document) maxScrollHeight () (height int) {
}
func (element *Document) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternBackground)
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
minimumWidth := 0
for index := 0; index < element.entity.CountChildren(); index ++ {
width, height := element.entity.ChildMinimumSize(index)

View File

@ -6,22 +6,13 @@ import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type fileEntity interface {
tomo.SelectableEntity
tomo.FocusableEntity
}
var fileCase = tomo.C("files", "file")
// File displays an interactive visual representation of a file within any
// file system.
type File struct {
entity fileEntity
config config.Wrapped
theme theme.Wrapped
entity tomo.Entity
lastClick time.Time
pressed bool
@ -43,8 +34,7 @@ func NewFile (
err error,
) {
element = &File { enabled: true }
element.theme.Case = tomo.C("files", "file")
element.entity = tomo.NewEntity(element).(fileEntity)
element.entity = tomo.GetBackend().NewEntity(element)
err = element.SetLocation(location, within)
return
}
@ -55,13 +45,13 @@ func (element *File) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *File) Draw (destination canvas.Canvas) {
func (element *File) Draw (destination artist.Canvas) {
// background
state := element.state()
bounds := element.entity.Bounds()
sink := element.theme.Sink(tomo.PatternButton)
element.theme.
Pattern(tomo.PatternButton, state).
sink := element.entity.Theme().Sink(tomo.PatternButton, fileCase)
element.entity.Theme().
Pattern(tomo.PatternButton, state, fileCase).
Draw(destination, bounds)
// icon
@ -76,7 +66,7 @@ func (element *File) Draw (destination canvas.Canvas) {
}
icon.Draw (
destination,
element.theme.Color(tomo.ColorForeground, state),
element.entity.Theme().Color(tomo.ColorForeground, state, fileCase),
bounds.Min.Add(offset))
}
}
@ -185,7 +175,7 @@ func (element *File) HandleMouseUp (
if button != input.ButtonLeft { return }
element.pressed = false
within := position.In(element.entity.Bounds())
if time.Since(element.lastClick) < element.config.DoubleClickDelay() {
if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() {
if element.Enabled() && within && element.onChoose != nil {
element.onChoose()
}
@ -195,17 +185,8 @@ func (element *File) HandleMouseUp (
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *File) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *File) SetConfig (config tomo.Config) {
if config == element.config.Config { return }
element.config.Config = config
func (element *File) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
@ -219,11 +200,11 @@ func (element *File) state () tomo.State {
}
func (element *File) icon () artist.Icon {
return element.theme.Icon(element.iconID, tomo.IconSizeLarge)
return element.entity.Theme().Icon(element.iconID, tomo.IconSizeLarge, fileCase)
}
func (element *File) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternButton)
padding := element.entity.Theme().Padding(tomo.PatternButton, fileCase)
icon := element.icon()
if icon == nil {
element.entity.SetMinimumSize (

View File

@ -5,22 +5,21 @@ import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
var clockCase = tomo.C("tomo", "clock")
// AnalogClock can display the time of day in an analog format.
type AnalogClock struct {
entity tomo.Entity
time time.Time
theme theme.Wrapped
}
// NewAnalogClock creates a new analog clock that displays the specified time.
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
element = &AnalogClock { }
element.theme.Case = tomo.C("tomo", "clock")
element.entity = tomo.NewEntity(element)
element.entity = tomo.GetBackend().NewEntity(element)
element.entity.SetMinimumSize(64, 64)
return
}
@ -31,18 +30,18 @@ func (element *AnalogClock) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *AnalogClock) Draw (destination canvas.Canvas) {
func (element *AnalogClock) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
state := tomo.State { }
pattern := element.theme.Pattern(tomo.PatternSunken, state)
padding := element.theme.Padding(tomo.PatternSunken)
pattern := element.entity.Theme().Pattern(tomo.PatternSunken, state, clockCase)
padding := element.entity.Theme().Padding(tomo.PatternSunken, clockCase)
pattern.Draw(destination, bounds)
bounds = padding.Apply(bounds)
foreground := element.theme.Color(tomo.ColorForeground, state)
accent := element.theme.Color(tomo.ColorAccent, state)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, clockCase)
accent := element.entity.Theme().Color(tomo.ColorAccent, state, clockCase)
for hour := 0; hour < 12; hour ++ {
element.radialLine (
@ -67,15 +66,12 @@ func (element *AnalogClock) SetTime (newTime time.Time) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *AnalogClock) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
func (element *AnalogClock) HandleThemeChange () {
element.entity.Invalidate()
}
func (element *AnalogClock) radialLine (
destination canvas.Canvas,
destination artist.Canvas,
source color.RGBA,
inner float64,
outer float64,

View File

@ -3,12 +3,14 @@ package fun
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
var pianoCase = tomo.C("tomo", "piano")
var flatCase = tomo.C("tomo", "piano", "flatKey")
var sharpCase = tomo.C("tomo", "piano", "sharpKey")
const pianoKeyWidth = 18
type pianoKey struct {
@ -18,12 +20,7 @@ type pianoKey struct {
// Piano is an element that can be used to input midi notes.
type Piano struct {
entity tomo.FocusableEntity
config config.Wrapped
theme theme.Wrapped
flatTheme theme.Wrapped
sharpTheme theme.Wrapped
entity tomo.Entity
low, high music.Octave
flatKeys []pianoKey
@ -49,10 +46,7 @@ func NewPiano (low, high music.Octave) (element *Piano) {
keynavPressed: make(map[music.Note] bool),
}
element.theme.Case = tomo.C("tomo", "piano")
element.flatTheme.Case = tomo.C("tomo", "piano", "flatKey")
element.sharpTheme.Case = tomo.C("tomo", "piano", "sharpKey")
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
@ -63,7 +57,7 @@ func (element *Piano) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Piano) Draw (destination canvas.Canvas) {
func (element *Piano) Draw (destination artist.Canvas) {
element.recalculate()
state := tomo.State {
@ -90,8 +84,8 @@ func (element *Piano) Draw (destination canvas.Canvas) {
state)
}
pattern := element.theme.Pattern(tomo.PatternPinboard, state)
artist.DrawShatter (
pattern := element.entity.Theme().Pattern(tomo.PatternPinboard, state, pianoCase)
artutil.DrawShatter (
destination, pattern, element.entity.Bounds(),
element.contentBounds)
}
@ -248,26 +242,13 @@ func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Piano) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.flatTheme.Theme = new
element.sharpTheme.Theme = new
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Piano) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
func (element *Piano) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Piano) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternPinboard)
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
element.entity.SetMinimumSize (
pianoKeyWidth * 7 * element.countOctaves() +
padding.Horizontal(),
@ -290,7 +271,7 @@ func (element *Piano) recalculate () {
element.flatKeys = make([]pianoKey, element.countFlats())
element.sharpKeys = make([]pianoKey, element.countSharps())
padding := element.theme.Padding(tomo.PatternPinboard)
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
bounds := padding.Apply(element.entity.Bounds())
dot := bounds.Min
@ -323,23 +304,23 @@ func (element *Piano) recalculate () {
}
func (element *Piano) drawFlat (
destination canvas.Canvas,
destination artist.Canvas,
bounds image.Rectangle,
pressed bool,
state tomo.State,
) {
state.Pressed = pressed
pattern := element.flatTheme.Pattern(tomo.PatternButton, state)
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, flatCase)
pattern.Draw(destination, bounds)
}
func (element *Piano) drawSharp (
destination canvas.Canvas,
destination artist.Canvas,
bounds image.Rectangle,
pressed bool,
state tomo.State,
) {
state.Pressed = pressed
pattern := element.sharpTheme.Pattern(tomo.PatternButton, state)
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, sharpCase)
pattern.Draw(destination, bounds)
}

View File

@ -2,14 +2,13 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
var iconCase = tomo.C("tomo", "icon")
// Icon is an element capable of displaying a singular icon.
type Icon struct {
entity tomo.Entity
theme theme.Wrapped
id tomo.Icon
size tomo.IconSize
}
@ -20,8 +19,7 @@ func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) {
id: id,
size: size,
}
element.entity = tomo.NewEntity(element)
element.theme.Case = tomo.C("tomo", "icon")
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
@ -40,23 +38,19 @@ func (element *Icon) SetIcon (id tomo.Icon, size tomo.IconSize) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Icon) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
if element.entity == nil { return }
func (element *Icon) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Icon) Draw (destination canvas.Canvas) {
func (element *Icon) Draw (destination artist.Canvas) {
if element.entity == nil { return }
bounds := element.entity.Bounds()
state := tomo.State { }
element.theme.
Pattern(tomo.PatternBackground, state).
element.entity.Theme().
Pattern(tomo.PatternBackground, state, iconCase).
Draw(destination, bounds)
icon := element.icon()
if icon != nil {
@ -66,13 +60,13 @@ func (element *Icon) Draw (destination canvas.Canvas) {
(bounds.Dy() - iconBounds.Dy()) / 2)
icon.Draw (
destination,
element.theme.Color(tomo.ColorForeground, state),
element.entity.Theme().Color(tomo.ColorForeground, state, iconCase),
bounds.Min.Add(offset))
}
}
func (element *Icon) icon () artist.Icon {
return element.theme.Icon(element.id, element.size)
return element.entity.Theme().Icon(element.id, element.size, iconCase)
}
func (element *Icon) updateMinimumSize () {

View File

@ -2,7 +2,7 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
// TODO: this element is lame need to make it better
@ -10,13 +10,13 @@ import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
// Image is an element capable of displaying an image.
type Image struct {
entity tomo.Entity
buffer canvas.Canvas
buffer artist.Canvas
}
// NewImage creates a new image element.
func NewImage (image image.Image) (element *Image) {
element = &Image { buffer: canvas.FromImage(image) }
element.entity = tomo.NewEntity(element)
element = &Image { buffer: artist.FromImage(image) }
element.entity = tomo.GetBackend().NewEntity(element)
bounds := element.buffer.Bounds()
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
return
@ -28,7 +28,7 @@ func (element *Image) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Image) Draw (destination canvas.Canvas) {
func (element *Image) Draw (destination artist.Canvas) {
if element.entity == nil { return }
(patterns.Texture { Canvas: element.buffer }).
Draw(destination, element.entity.Bounds())

View File

@ -5,14 +5,14 @@ import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
var labelCase = tomo.C("tomo", "label")
// Label is a simple text box.
type Label struct {
entity tomo.FlexibleEntity
entity tomo.Entity
align textdraw.Align
wrap bool
@ -22,19 +22,15 @@ type Label struct {
forcedColumns int
forcedRows int
minHeight int
config config.Wrapped
theme theme.Wrapped
}
// NewLabel creates a new label.
func NewLabel (text string) (element *Label) {
element = &Label { }
element.theme.Case = tomo.C("tomo", "label")
element.entity = tomo.NewEntity(element).(tomo.FlexibleEntity)
element.drawer.SetFace (element.theme.FontFace (
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
tomo.FontSizeNormal, labelCase))
element.SetText(text)
return
}
@ -52,7 +48,7 @@ func (element *Label) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Label) Draw (destination canvas.Canvas) {
func (element *Label) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
if element.wrap {
@ -63,9 +59,9 @@ func (element *Label) Draw (destination canvas.Canvas) {
element.entity.DrawBackground(destination)
textBounds := element.drawer.LayoutBounds()
foreground := element.theme.Color (
foreground := element.entity.Theme().Color (
tomo.ColorForeground,
tomo.State { })
tomo.State { }, labelCase)
element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min))
}
@ -132,21 +128,10 @@ func (element *Label) SetAlign (align textdraw.Align) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Label) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
func (element *Label) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Label) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
tomo.FontSizeNormal, labelCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
@ -195,7 +180,7 @@ func (element *Label) updateMinimumSize () {
if element.wrap {
em := element.drawer.Em().Round()
if em < 1 {
em = element.theme.Padding(tomo.PatternBackground)[0]
em = element.entity.Theme().Padding(tomo.PatternBackground, labelCase)[0]
}
width, height = em, element.drawer.LineHeight().Round()
element.entity.NotifyFlexibleHeightChange()

View File

@ -33,7 +33,7 @@ func NewHLerpSlider[T Numeric] (min, max T, value T) (element *LerpSlider[T]) {
min: min,
max: max,
}
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.entity = tomo.GetBackend().NewEntity(element)
element.construct()
element.SetValue(value)
return

View File

@ -3,19 +3,15 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
type listEntity interface {
tomo.ContainerEntity
tomo.ScrollableEntity
tomo.FocusableEntity
}
import "git.tebibyte.media/sashakoshka/tomo/ability"
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
type list struct {
container
entity listEntity
entity tomo.Entity
c tomo.Case
enabled bool
scroll image.Point
@ -25,8 +21,6 @@ type list struct {
forcedMinimumWidth int
forcedMinimumHeight int
theme theme.Wrapped
onClick func ()
onSelectionChange func ()
onScrollBoundsChange func ()
@ -42,8 +36,8 @@ type FlowList struct {
func NewList (children ...tomo.Element) (element *List) {
element = &List { }
element.theme.Case = tomo.C("tomo", "list")
element.entity = tomo.NewEntity(element).(listEntity)
element.c = tomo.C("tomo", "list")
element.entity = tomo.GetBackend().NewEntity(element)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init(children...)
@ -52,8 +46,8 @@ func NewList (children ...tomo.Element) (element *List) {
func NewFlowList (children ...tomo.Element) (element *FlowList) {
element = &FlowList { }
element.theme.Case = tomo.C("tomo", "flowList")
element.entity = tomo.NewEntity(element).(listEntity)
element.c = tomo.C("tomo", "flowList")
element.entity = tomo.GetBackend().NewEntity(element)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init(children...)
@ -67,14 +61,14 @@ func (element *list) init (children ...tomo.Element) {
element.Adopt(children...)
}
func (element *list) Draw (destination canvas.Canvas) {
func (element *list) Draw (destination artist.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
pattern := element.theme.Pattern(tomo.PatternSunken, element.state())
artist.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...)
pattern := element.entity.Theme().Pattern(tomo.PatternSunken, element.state(), element.c)
artutil.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...)
}
func (element *List) Layout () {
@ -82,8 +76,8 @@ func (element *List) Layout () {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.theme.Margin(tomo.PatternSunken)
padding := element.theme.Padding(tomo.PatternSunken)
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
@ -120,8 +114,8 @@ func (element *FlowList) Layout () {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.theme.Margin(tomo.PatternSunken)
padding := element.theme.Padding(tomo.PatternSunken)
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
@ -145,7 +139,7 @@ func (element *FlowList) Layout () {
if width + dot.X > bounds.Max.X {
nextLine()
}
if typedChild, ok := child.(tomo.Flexible); ok {
if typedChild, ok := child.(ability.Flexible); ok {
height = typedChild.FlexibleHeightFor(width)
}
if rowHeight < height {
@ -170,14 +164,14 @@ func (element *FlowList) Layout () {
}
}
func (element *list) Selected () tomo.Selectable {
func (element *list) Selected () ability.Selectable {
if element.selected == -1 { return nil }
child, ok := element.entity.Child(element.selected).(tomo.Selectable)
child, ok := element.entity.Child(element.selected).(ability.Selectable)
if !ok { return nil }
return child
}
func (element *list) Select (child tomo.Selectable) {
func (element *list) Select (child ability.Selectable) {
index := element.entity.IndexOf(child)
if element.selected == index { return }
element.selectNone()
@ -231,7 +225,7 @@ func (element *list) HandleChildMouseDown (
) {
if !element.enabled { return }
element.Focus()
if child, ok := child.(tomo.Selectable); ok {
if child, ok := child.(ability.Selectable); ok {
element.Select(child)
}
}
@ -248,7 +242,7 @@ func (element *list) HandleChildMouseUp (
}
}
func (element *list) HandleChildFlexibleHeightChange (child tomo.Flexible) {
func (element *list) HandleChildFlexibleHeightChange (child ability.Flexible) {
element.minimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
@ -280,14 +274,11 @@ func (element *list) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
func (element *list) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
func (element *list) DrawBackground (destination canvas.Canvas) {
func (element *list) DrawBackground (destination artist.Canvas) {
element.entity.DrawBackground(destination)
}
// SetTheme sets the element's theme.
func (element *list) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
func (element *list) HandleThemeChange () {
element.minimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
@ -322,7 +313,7 @@ func (element *list) ScrollContentBounds () image.Rectangle {
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *list) ScrollViewportBounds () image.Rectangle {
padding := element.theme.Padding(tomo.PatternSunken)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
@ -374,7 +365,7 @@ func (element *list) selectNone () {
func (element *list) scrollToSelected () {
if element.selected < 0 { return }
target := element.entity.Child(element.selected).Entity().Bounds()
padding := element.theme.Padding(tomo.PatternSunken)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
bounds := padding.Apply(element.entity.Bounds())
if target.Min.Y < bounds.Min.Y {
element.scroll.Y -= bounds.Min.Y - target.Min.Y
@ -395,7 +386,7 @@ func (element *list) state () tomo.State {
}
func (element *list) maxScrollHeight () (height int) {
padding := element.theme.Padding(tomo.PatternSunken)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
@ -403,8 +394,8 @@ func (element *list) maxScrollHeight () (height int) {
}
func (element *List) updateMinimumSize () {
margin := element.theme.Margin(tomo.PatternSunken)
padding := element.theme.Padding(tomo.PatternSunken)
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
width := 0
height := 0
@ -437,7 +428,7 @@ func (element *List) updateMinimumSize () {
}
func (element *FlowList) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternSunken)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
minimumWidth := 0
for index := 0; index < element.entity.CountChildren(); index ++ {
width, height := element.entity.ChildMinimumSize(index)

View File

@ -2,17 +2,14 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
var progressBarCase = tomo.C("tomo", "progressBar")
// ProgressBar displays a visual indication of how far along a task is.
type ProgressBar struct {
entity tomo.Entity
progress float64
config config.Wrapped
theme theme.Wrapped
}
// NewProgressBar creates a new progress bar displaying the given progress
@ -21,8 +18,7 @@ func NewProgressBar (progress float64) (element *ProgressBar) {
if progress < 0 { progress = 0 }
if progress > 1 { progress = 1 }
element = &ProgressBar { progress: progress }
element.entity = tomo.NewEntity(element)
element.theme.Case = tomo.C("tomo", "progressBar")
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
@ -33,18 +29,18 @@ func (element *ProgressBar) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ProgressBar) Draw (destination canvas.Canvas) {
func (element *ProgressBar) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { })
padding := element.theme.Padding(tomo.PatternSunken)
pattern := element.entity.Theme().Pattern(tomo.PatternSunken, tomo.State { }, progressBarCase)
padding := element.entity.Theme().Padding(tomo.PatternSunken, progressBarCase)
pattern.Draw(destination, bounds)
bounds = padding.Apply(bounds)
meterBounds := image.Rect (
bounds.Min.X, bounds.Min.Y,
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y)
mercury := element.theme.Pattern(tomo.PatternMercury, tomo.State { })
mercury := element.entity.Theme().Pattern(tomo.PatternMercury, tomo.State { }, progressBarCase)
mercury.Draw(destination, meterBounds)
}
@ -57,25 +53,14 @@ func (element *ProgressBar) SetProgress (progress float64) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *ProgressBar) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *ProgressBar) SetConfig (new tomo.Config) {
if new == nil || new == element.config.Config { return }
element.config.Config = new
func (element *ProgressBar) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *ProgressBar) updateMinimumSize() {
padding := element.theme.Padding(tomo.PatternSunken)
innerPadding := element.theme.Padding(tomo.PatternMercury)
padding := element.entity.Theme().Padding(tomo.PatternSunken, progressBarCase)
innerPadding := element.entity.Theme().Padding(tomo.PatternMercury, progressBarCase)
element.entity.SetMinimumSize (
padding.Horizontal() + innerPadding.Horizontal(),
padding.Vertical() + innerPadding.Vertical())

View File

@ -3,9 +3,10 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/ability"
var scrollCase = tomo.C("tomo", "scroll")
// ScrollMode specifies which sides of a Scroll have scroll bars.
type ScrollMode int; const (
@ -24,21 +25,17 @@ func (mode ScrollMode) Includes (sub ScrollMode) bool {
// Scroll adds scroll bars to any scrollable element. It also captures scroll
// wheel input.
type Scroll struct {
entity tomo.ContainerEntity
entity tomo.Entity
child tomo.Scrollable
child ability.Scrollable
horizontal *ScrollBar
vertical *ScrollBar
config config.Wrapped
theme theme.Wrapped
}
// NewScroll creates a new scroll element.
func NewScroll (mode ScrollMode, child tomo.Scrollable) (element *Scroll) {
func NewScroll (mode ScrollMode, child ability.Scrollable) (element *Scroll) {
element = &Scroll { }
element.theme.Case = tomo.C("tomo", "scroll")
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
element.entity = tomo.GetBackend().NewEntity(element)
if mode.Includes(ScrollHorizontal) {
element.horizontal = NewHScrollBar()
@ -79,15 +76,15 @@ func (element *Scroll) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Scroll) Draw (destination canvas.Canvas) {
func (element *Scroll) Draw (destination artist.Canvas) {
if element.horizontal != nil && element.vertical != nil {
bounds := element.entity.Bounds()
bounds.Min = image.Pt (
bounds.Max.X - element.vertical.Entity().Bounds().Dx(),
bounds.Max.Y - element.horizontal.Entity().Bounds().Dy())
state := tomo.State { }
deadArea := element.theme.Pattern(tomo.PatternDead, state)
deadArea.Draw(canvas.Cut(destination, bounds), bounds)
deadArea := element.entity.Theme().Pattern(tomo.PatternDead, state, scrollCase)
deadArea.Draw(artist.Cut(destination, bounds), bounds)
}
}
@ -134,12 +131,12 @@ func (element *Scroll) Layout () {
// DrawBackground draws this element's background pattern to the specified
// destination canvas.
func (element *Scroll) DrawBackground (destination canvas.Canvas) {
func (element *Scroll) DrawBackground (destination artist.Canvas) {
element.entity.DrawBackground(destination)
}
// Adopt sets this element's child. If nil is passed, any child is removed.
func (element *Scroll) Adopt (child tomo.Scrollable) {
func (element *Scroll) Adopt (child ability.Scrollable) {
if element.child != nil {
element.entity.Disown(element.entity.IndexOf(element.child))
}
@ -156,7 +153,7 @@ func (element *Scroll) Adopt (child tomo.Scrollable) {
// Child returns this element's child. If there is no child, this method will
// return nil.
func (element *Scroll) Child () tomo.Scrollable {
func (element *Scroll) Child () ability.Scrollable {
return element.child
}
@ -166,7 +163,7 @@ func (element *Scroll) HandleChildMinimumSizeChange (tomo.Element) {
element.entity.InvalidateLayout()
}
func (element *Scroll) HandleChildScrollBoundsChange (tomo.Scrollable) {
func (element *Scroll) HandleChildScrollBoundsChange (ability.Scrollable) {
element.updateEnabled()
viewportBounds := element.child.ScrollViewportBounds()
contentBounds := element.child.ScrollContentBounds()
@ -189,20 +186,12 @@ func (element *Scroll) HandleScroll (
element.scrollChildBy(int(deltaX), int(deltaY))
}
// SetTheme sets the element's theme.
func (element *Scroll) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
func (element *Scroll) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// SetConfig sets the element's configuration.
func (element *Scroll) SetConfig (config tomo.Config) {
element.config.Config = config
}
func (element *Scroll) updateMinimumSize () {
var width, height int

View File

@ -3,9 +3,7 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// ScrollBar is an element similar to Slider, but it has special behavior that
// makes it well suited for controlling the viewport position on one axis of a
@ -21,6 +19,8 @@ import "git.tebibyte.media/sashakoshka/tomo/default/config"
type ScrollBar struct {
entity tomo.Entity
c tomo.Case
vertical bool
enabled bool
dragging bool
@ -31,9 +31,6 @@ type ScrollBar struct {
contentBounds image.Rectangle
viewportBounds image.Rectangle
config config.Wrapped
theme theme.Wrapped
onScroll func (viewport image.Point)
}
@ -43,8 +40,8 @@ func NewVScrollBar () (element *ScrollBar) {
vertical: true,
enabled: true,
}
element.theme.Case = tomo.C("tomo", "scrollBarVertical")
element.entity = tomo.NewEntity(element).(tomo.Entity)
element.c = tomo.C("tomo", "scrollBarVertical")
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
@ -54,8 +51,8 @@ func NewHScrollBar () (element *ScrollBar) {
element = &ScrollBar {
enabled: true,
}
element.theme.Case = tomo.C("tomo", "scrollBarHorizontal")
element.entity = tomo.NewEntity(element).(tomo.Entity)
element.c = tomo.C("tomo", "scrollBarHorizontal")
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
@ -66,7 +63,7 @@ func (element *ScrollBar) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ScrollBar) Draw (destination canvas.Canvas) {
func (element *ScrollBar) Draw (destination artist.Canvas) {
element.recalculate()
bounds := element.entity.Bounds()
@ -74,10 +71,10 @@ func (element *ScrollBar) Draw (destination canvas.Canvas) {
Disabled: !element.Enabled(),
Pressed: element.dragging,
}
element.theme.Pattern(tomo.PatternGutter, state).Draw (
element.entity.Theme().Pattern(tomo.PatternGutter, state, element.c).Draw (
destination,
bounds)
element.theme.Pattern(tomo.PatternHandle, state).Draw (
element.entity.Theme().Pattern(tomo.PatternHandle, state, element.c).Draw (
destination,
element.bar)
}
@ -87,7 +84,7 @@ func (element *ScrollBar) HandleMouseDown (
button input.Button,
modifiers input.Modifiers,
) {
velocity := element.config.ScrollVelocity()
velocity := element.entity.Config().ScrollVelocity()
if position.In(element.bar) {
// the mouse is pressed down within the bar's handle
@ -189,17 +186,7 @@ func (element *ScrollBar) OnScroll (callback func (viewport image.Point)) {
element.onScroll = callback
}
// SetTheme sets the element's theme.
func (element *ScrollBar) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *ScrollBar) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
func (element *ScrollBar) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
@ -267,7 +254,7 @@ func (element *ScrollBar) recalculate () {
func (element *ScrollBar) recalculateVertical () {
bounds := element.entity.Bounds()
padding := element.theme.Padding(tomo.PatternGutter)
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
element.track = padding.Apply(bounds)
contentBounds := element.contentBounds
@ -294,7 +281,7 @@ func (element *ScrollBar) recalculateVertical () {
func (element *ScrollBar) recalculateHorizontal () {
bounds := element.entity.Bounds()
padding := element.theme.Padding(tomo.PatternGutter)
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
element.track = padding.Apply(bounds)
contentBounds := element.contentBounds
@ -320,8 +307,8 @@ func (element *ScrollBar) recalculateHorizontal () {
}
func (element *ScrollBar) updateMinimumSize () {
gutterPadding := element.theme.Padding(tomo.PatternGutter)
handlePadding := element.theme.Padding(tomo.PatternHandle)
gutterPadding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
handlePadding := element.entity.Theme().Padding(tomo.PatternHandle, element.c)
if element.vertical {
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal(),

View File

@ -3,9 +3,7 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// Slider is a slider control with a floating point value between zero and one.
type Slider struct {
@ -23,14 +21,16 @@ func NewVSlider (value float64) (element *Slider) {
func NewHSlider (value float64) (element *Slider) {
element = &Slider { }
element.value = value
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.entity = tomo.GetBackend().NewEntity(element)
element.construct()
return
}
type slider struct {
entity tomo.FocusableEntity
entity tomo.Entity
c tomo.Case
value float64
vertical bool
dragging bool
@ -39,9 +39,6 @@ type slider struct {
track image.Rectangle
bar image.Rectangle
config config.Wrapped
theme theme.Wrapped
onSlide func ()
onRelease func ()
}
@ -49,9 +46,9 @@ type slider struct {
func (element *slider) construct () {
element.enabled = true
if element.vertical {
element.theme.Case = tomo.C("tomo", "sliderVertical")
element.c = tomo.C("tomo", "sliderVertical")
} else {
element.theme.Case = tomo.C("tomo", "sliderHorizontal")
element.c = tomo.C("tomo", "sliderHorizontal")
}
element.updateMinimumSize()
}
@ -62,9 +59,9 @@ func (element *slider) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *slider) Draw (destination canvas.Canvas) {
func (element *slider) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
element.track = element.theme.Padding(tomo.PatternGutter).Apply(bounds)
element.track = element.entity.Theme().Padding(tomo.PatternGutter, element.c).Apply(bounds)
if element.vertical {
barSize := element.track.Dx()
element.bar = image.Rect(0, 0, barSize, barSize).Add(element.track.Min)
@ -86,8 +83,8 @@ func (element *slider) Draw (destination canvas.Canvas) {
Focused: element.entity.Focused(),
Pressed: element.dragging,
}
element.theme.Pattern(tomo.PatternGutter, state).Draw(destination, bounds)
element.theme.Pattern(tomo.PatternHandle, state).Draw(destination, element.bar)
element.entity.Theme().Pattern(tomo.PatternGutter, state, element.c).Draw(destination, bounds)
element.entity.Theme().Pattern(tomo.PatternHandle, state, element.c).Draw(destination, element.bar)
}
// Focus gives this element input focus.
@ -211,21 +208,12 @@ func (element *slider) OnRelease (callback func ()) {
element.onRelease = callback
}
// SetTheme sets the element's theme.
func (element *slider) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *slider) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
func (element *slider) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *slider) changeValue (delta float64) {
element.value += delta
if element.value < 0 {
@ -258,8 +246,8 @@ func (element *slider) valueFor (x, y int) (value float64) {
}
func (element *slider) updateMinimumSize () {
gutterPadding := element.theme.Padding(tomo.PatternGutter)
handlePadding := element.theme.Padding(tomo.PatternHandle)
gutterPadding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
handlePadding := element.entity.Theme().Padding(tomo.PatternHandle, element.c)
if element.vertical {
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal(),

View File

@ -1,25 +1,20 @@
package elements
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
var spacerCase = tomo.C("tomo", "spacer")
// Spacer can be used to put space between two elements..
type Spacer struct {
entity tomo.Entity
line bool
config config.Wrapped
theme theme.Wrapped
}
// NewSpacer creates a new spacer.
func NewSpacer () (element *Spacer) {
element = &Spacer { }
element.entity = tomo.NewEntity(element)
element.theme.Case = tomo.C("tomo", "spacer")
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
@ -37,18 +32,18 @@ func (element *Spacer) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Spacer) Draw (destination canvas.Canvas) {
func (element *Spacer) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
if element.line {
pattern := element.theme.Pattern (
pattern := element.entity.Theme().Pattern (
tomo.PatternLine,
tomo.State { })
tomo.State { }, spacerCase)
pattern.Draw(destination, bounds)
} else {
pattern := element.theme.Pattern (
pattern := element.entity.Theme().Pattern (
tomo.PatternBackground,
tomo.State { })
tomo.State { }, spacerCase)
pattern.Draw(destination, bounds)
}
}
@ -61,23 +56,13 @@ func (element *Spacer) SetLine (line bool) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Spacer) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Spacer) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
func (element *Spacer) HandleThemeChange () {
element.entity.Invalidate()
}
func (element *Spacer) updateMinimumSize () {
if element.line {
padding := element.theme.Padding(tomo.PatternLine)
padding := element.entity.Theme().Padding(tomo.PatternLine, spacerCase)
element.entity.SetMinimumSize (
padding.Horizontal(),
padding.Vertical())

View File

@ -3,15 +3,15 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
var switchCase = tomo.C("tomo", "switch")
// Switch is a toggle-able on/off switch with an optional label. It is
// functionally identical to Checkbox, but plays a different semantic role.
type Switch struct {
entity tomo.FocusableEntity
entity tomo.Entity
drawer textdraw.Drawer
enabled bool
@ -19,9 +19,6 @@ type Switch struct {
checked bool
text string
config config.Wrapped
theme theme.Wrapped
onToggle func ()
}
@ -32,11 +29,10 @@ func NewSwitch (text string, on bool) (element *Switch) {
text: text,
enabled: true,
}
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.theme.Case = tomo.C("tomo", "switch")
element.drawer.SetFace (element.theme.FontFace (
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
tomo.FontSizeNormal, switchCase))
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
return
@ -48,7 +44,7 @@ func (element *Switch) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Switch) Draw (destination canvas.Canvas) {
func (element *Switch) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
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)
@ -76,24 +72,24 @@ func (element *Switch) Draw (destination canvas.Canvas) {
}
}
gutterPattern := element.theme.Pattern (
tomo.PatternGutter, state)
gutterPattern := element.entity.Theme().Pattern (
tomo.PatternGutter, state, switchCase)
gutterPattern.Draw(destination, gutterBounds)
handlePattern := element.theme.Pattern (
tomo.PatternHandle, state)
handlePattern := element.entity.Theme().Pattern (
tomo.PatternHandle, state, switchCase)
handlePattern.Draw(destination, handleBounds)
textBounds := element.drawer.LayoutBounds()
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() * 2 +
element.theme.Margin(tomo.PatternBackground).X,
element.entity.Theme().Margin(tomo.PatternBackground, switchCase).X,
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground := element.theme.Color(tomo.ColorForeground, state)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, switchCase)
element.drawer.Draw(destination, foreground, offset)
}
@ -186,21 +182,10 @@ func (element *Switch) SetText (text string) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Switch) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
func (element *Switch) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Switch) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
tomo.FontSizeNormal, switchCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
@ -214,7 +199,7 @@ func (element *Switch) updateMinimumSize () {
} else {
element.entity.SetMinimumSize (
lineHeight * 2 +
element.theme.Margin(tomo.PatternBackground).X +
element.entity.Theme().Margin(tomo.PatternBackground, switchCase).X +
textBounds.Dx(),
lineHeight)
}

View File

@ -5,11 +5,11 @@ import "time"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font"
@ -22,7 +22,7 @@ type Artist struct {
// NewArtist creates a new artist test element.
func NewArtist () (element *Artist) {
element = &Artist { }
element.entity = tomo.NewEntity(element)
element.entity = tomo.GetBackend().NewEntity(element)
element.entity.SetMinimumSize(240, 240)
return
}
@ -31,7 +31,7 @@ func (element *Artist) Entity () tomo.Entity {
return element.entity
}
func (element *Artist) Draw (destination canvas.Canvas) {
func (element *Artist) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
patterns.Uhex(0x000000FF).Draw(destination, bounds)
@ -44,28 +44,28 @@ func (element *Artist) Draw (destination canvas.Canvas) {
// 4, 0
c40 := element.cellAt(destination, 4, 0)
shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1)
shapes.StrokeColorRectangle(c40, artutil.Hex(0x888888FF), c40.Bounds(), 1)
shapes.ColorLine (
c40, artist.Hex(0xFF0000FF), 1,
c40, artutil.Hex(0xFF0000FF), 1,
c40.Bounds().Min, c40.Bounds().Max)
// 0, 1
c01 := element.cellAt(destination, 0, 1)
shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1)
shapes.FillColorEllipse(destination, artist.Hex(0x00FF00FF), c01.Bounds())
shapes.StrokeColorRectangle(c01, artutil.Hex(0x888888FF), c01.Bounds(), 1)
shapes.FillColorEllipse(destination, artutil.Hex(0x00FF00FF), c01.Bounds())
// 1, 1 - 3, 1
for x := 1; x < 4; x ++ {
c := element.cellAt(destination, x, 1)
shapes.StrokeColorRectangle (
destination, artist.Hex(0x888888FF),
destination, artutil.Hex(0x888888FF),
c.Bounds(), 1)
shapes.StrokeColorEllipse (
destination,
[]color.RGBA {
artist.Hex(0xFF0000FF),
artist.Hex(0x00FF00FF),
artist.Hex(0xFF00FFFF),
artutil.Hex(0xFF0000FF),
artutil.Hex(0x00FF00FF),
artutil.Hex(0xFF00FFFF),
} [x - 1],
c.Bounds(), x)
}
@ -93,12 +93,12 @@ func (element *Artist) Draw (destination canvas.Canvas) {
// 0, 2
c02 := element.cellAt(destination, 0, 2)
shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
shapes.StrokeColorRectangle(c02, artutil.Hex(0x888888FF), c02.Bounds(), 1)
shapes.FillEllipse(c02, c41, c02.Bounds())
// 1, 2
c12 := element.cellAt(destination, 1, 2)
shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
shapes.StrokeColorRectangle(c12, artutil.Hex(0x888888FF), c12.Bounds(), 1)
shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5)
// 2, 2
@ -135,7 +135,7 @@ func (element *Artist) Draw (destination canvas.Canvas) {
patterns.Border {
Canvas: element.thingy(c42),
Inset: artist.Inset { 8, 8, 8, 8 },
}.Draw(canvas.Cut(c23, c23.Bounds().Inset(16)), c23.Bounds())
}.Draw(artist.Cut(c23, c23.Bounds().Inset(16)), c23.Bounds())
// how long did that take to render?
drawTime := time.Since(drawStart)
@ -146,13 +146,13 @@ func (element *Artist) Draw (destination canvas.Canvas) {
drawTime.Milliseconds(),
drawTime.Microseconds())))
textDrawer.Draw (
destination, artist.Hex(0xFFFFFFFF),
destination, artutil.Hex(0xFFFFFFFF),
image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
}
func (element *Artist) colorLines (destination canvas.Canvas, weight int, bounds image.Rectangle) {
func (element *Artist) colorLines (destination artist.Canvas, weight int, bounds image.Rectangle) {
bounds = bounds.Inset(4)
c := artist.Hex(0xFFFFFFFF)
c := artutil.Hex(0xFFFFFFFF)
shapes.ColorLine(destination, c, weight, bounds.Min, bounds.Max)
shapes.ColorLine (
destination, c, weight,
@ -184,24 +184,24 @@ func (element *Artist) colorLines (destination canvas.Canvas, weight int, bounds
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y))
}
func (element *Artist) cellAt (destination canvas.Canvas, x, y int) (canvas.Canvas) {
func (element *Artist) cellAt (destination artist.Canvas, x, y int) (artist.Canvas) {
bounds := element.entity.Bounds()
cellBounds := image.Rectangle { }
cellBounds.Min = bounds.Min
cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4
return canvas.Cut (destination, cellBounds.Add (image.Pt (
return artist.Cut (destination, cellBounds.Add (image.Pt (
x * cellBounds.Dx(),
y * cellBounds.Dy())))
}
func (element *Artist) thingy (destination canvas.Canvas) (result canvas.Canvas) {
func (element *Artist) thingy (destination artist.Canvas) (result artist.Canvas) {
bounds := destination.Bounds()
bounds = image.Rect(0, 0, 32, 32).Add(bounds.Min)
shapes.FillColorRectangle(destination, artist.Hex(0x440000FF), bounds)
shapes.StrokeColorRectangle(destination, artist.Hex(0xFF0000FF), bounds, 1)
shapes.StrokeColorRectangle(destination, artist.Hex(0x004400FF), bounds.Inset(4), 1)
shapes.FillColorRectangle(destination, artist.Hex(0x004444FF), bounds.Inset(12))
shapes.StrokeColorRectangle(destination, artist.Hex(0x888888FF), bounds.Inset(8), 1)
return canvas.Cut(destination, bounds)
shapes.FillColorRectangle(destination, artutil.Hex(0x440000FF), bounds)
shapes.StrokeColorRectangle(destination, artutil.Hex(0xFF0000FF), bounds, 1)
shapes.StrokeColorRectangle(destination, artutil.Hex(0x004400FF), bounds.Inset(4), 1)
shapes.FillColorRectangle(destination, artutil.Hex(0x004444FF), bounds.Inset(12))
shapes.StrokeColorRectangle(destination, artutil.Hex(0x888888FF), bounds.Inset(8), 1)
return artist.Cut(destination, bounds)
}

View File

@ -4,10 +4,10 @@ import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
var mouseCase = tomo.C("tomo", "mouse")
// Mouse is an element capable of testing mouse input. When the mouse is clicked
// and dragged on it, it draws a trail.
@ -15,16 +15,12 @@ type Mouse struct {
entity tomo.Entity
pressed bool
lastMousePos image.Point
config config.Wrapped
theme theme.Wrapped
}
// NewMouse creates a new mouse test element.
func NewMouse () (element *Mouse) {
element = &Mouse { }
element.theme.Case = tomo.C("tomo", "mouse")
element.entity = tomo.NewEntity(element)
element.entity = tomo.GetBackend().NewEntity(element)
element.entity.SetMinimumSize(32, 32)
return
}
@ -33,41 +29,34 @@ func (element *Mouse) Entity () tomo.Entity {
return element.entity
}
func (element *Mouse) Draw (destination canvas.Canvas) {
func (element *Mouse) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
accent := element.theme.Color (
accent := element.entity.Theme().Color (
tomo.ColorAccent,
tomo.State { })
tomo.State { },
mouseCase)
shapes.FillColorRectangle(destination, accent, bounds)
shapes.StrokeColorRectangle (
destination,
artist.Hex(0x000000FF),
artutil.Hex(0x000000FF),
bounds, 1)
shapes.ColorLine (
destination, artist.Hex(0xFFFFFFFF), 1,
destination, artutil.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, 1)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
shapes.ColorLine (
destination, artist.Hex(0xFFFFFFFF), 1,
destination, artutil.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
if element.pressed {
midpoint := bounds.Min.Add(bounds.Max.Sub(bounds.Min).Div(2))
shapes.ColorLine (
destination, artist.Hex(0x000000FF), 1,
destination, artutil.Hex(0x000000FF), 1,
midpoint, element.lastMousePos)
}
}
// SetTheme sets the element's theme.
func (element *Mouse) SetTheme (new tomo.Theme) {
element.theme.Theme = new
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Mouse) SetConfig (new tomo.Config) {
element.config.Config = new
func (element *Mouse) HandleThemeChange (new tomo.Theme) {
element.entity.Invalidate()
}

View File

@ -7,23 +7,16 @@ import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type textBoxEntity interface {
tomo.FocusableEntity
tomo.ScrollableEntity
tomo.LayoutEntity
}
var textBoxCase = tomo.C("tomo", "textBox")
// TextBox is a single-line text input.
type TextBox struct {
entity textBoxEntity
entity tomo.Entity
enabled bool
lastClick time.Time
@ -36,9 +29,6 @@ type TextBox struct {
placeholderDrawer textdraw.Drawer
valueDrawer textdraw.Drawer
config config.Wrapped
theme theme.Wrapped
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
onChange func ()
onEnter func ()
@ -50,15 +40,14 @@ type TextBox struct {
// text.
func NewTextBox (placeholder, value string) (element *TextBox) {
element = &TextBox { enabled: true }
element.theme.Case = tomo.C("tomo", "textBox")
element.entity = tomo.NewEntity(element).(textBoxEntity)
element.entity = tomo.GetBackend().NewEntity(element)
element.placeholder = placeholder
element.placeholderDrawer.SetFace (element.theme.FontFace (
element.placeholderDrawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.valueDrawer.SetFace (element.theme.FontFace (
tomo.FontSizeNormal, textBoxCase))
element.valueDrawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
tomo.FontSizeNormal, textBoxCase))
element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize()
element.SetValue(value)
@ -71,19 +60,19 @@ func (element *TextBox) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *TextBox) Draw (destination canvas.Canvas) {
func (element *TextBox) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds()
state := element.state()
pattern := element.theme.Pattern(tomo.PatternInput, state)
padding := element.theme.Padding(tomo.PatternInput)
innerCanvas := canvas.Cut(destination, padding.Apply(bounds))
pattern := element.entity.Theme().Pattern(tomo.PatternInput, state, textBoxCase)
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
innerCanvas := artist.Cut(destination, padding.Apply(bounds))
pattern.Draw(destination, bounds)
offset := element.textOffset()
if element.entity.Focused() && !element.dot.Empty() {
// draw selection bounds
accent := element.theme.Color(tomo.ColorAccent, state)
accent := element.entity.Theme().Color(tomo.ColorAccent, state, textBoxCase)
canon := element.dot.Canon()
foff := fixedutil.Pt(offset)
start := element.valueDrawer.PositionAt(canon.Start).Add(foff)
@ -101,9 +90,9 @@ func (element *TextBox) Draw (destination canvas.Canvas) {
if len(element.text) == 0 {
// draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds()
foreground := element.theme.Color (
foreground := element.entity.Theme().Color (
tomo.ColorForeground,
tomo.State { Disabled: true })
tomo.State { Disabled: true }, textBoxCase)
element.placeholderDrawer.Draw (
innerCanvas,
foreground,
@ -111,7 +100,7 @@ func (element *TextBox) Draw (destination canvas.Canvas) {
} else {
// draw input value
textBounds := element.valueDrawer.LayoutBounds()
foreground := element.theme.Color(tomo.ColorForeground, state)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
element.valueDrawer.Draw (
innerCanvas,
foreground,
@ -120,7 +109,7 @@ func (element *TextBox) Draw (destination canvas.Canvas) {
if element.entity.Focused() && element.dot.Empty() {
// draw cursor
foreground := element.theme.Color(tomo.ColorForeground, state)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
shapes.ColorLine (
@ -156,7 +145,7 @@ func (element *TextBox) HandleMouseDown (
runeIndex := element.atPosition(position)
if runeIndex == -1 { return }
if time.Since(element.lastClick) < element.config.DoubleClickDelay() {
if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() {
element.dragging = 2
element.dot = textmanip.WordAround(element.text, runeIndex)
} else {
@ -214,7 +203,7 @@ func (element *TextBox) HandleMotion (position image.Point) {
}
func (element *TextBox) textOffset () image.Point {
padding := element.theme.Padding(tomo.PatternInput)
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
bounds := element.entity.Bounds()
innerBounds := padding.Apply(bounds)
textHeight := element.valueDrawer.LineHeight().Round()
@ -476,27 +465,17 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
return true, false
}
// SetTheme sets the element's theme.
func (element *TextBox) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
face := element.theme.FontFace (
func (element *TextBox) HandleThemeChange () {
face := element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal)
tomo.FontSizeNormal,
textBoxCase)
element.placeholderDrawer.SetFace(face)
element.valueDrawer.SetFace(face)
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *TextBox) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *TextBox) contextMenu (position image.Point) {
window := element.entity.Window()
menu, err := window.NewMenu(image.Rectangle { position, position })
@ -540,12 +519,12 @@ func (element *TextBox) runOnChange () {
}
func (element *TextBox) scrollViewportWidth () (width int) {
padding := element.theme.Padding(tomo.PatternInput)
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
return padding.Apply(element.entity.Bounds()).Dx()
}
func (element *TextBox) scrollToCursor () {
padding := element.theme.Padding(tomo.PatternInput)
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min)
bounds.Max.X -= element.valueDrawer.Em().Round()
@ -568,7 +547,7 @@ func (element *TextBox) scrollToCursor () {
func (element *TextBox) updateMinimumSize () {
textBounds := element.placeholderDrawer.LayoutBounds()
padding := element.theme.Padding(tomo.PatternInput)
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
element.entity.SetMinimumSize (
padding.Horizontal() + textBounds.Dx(),
padding.Vertical() +

View File

@ -3,23 +3,20 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
var toggleButtonCase = tomo.C("tomo", "toggleButton")
// ToggleButton is a togglable button.
type ToggleButton struct {
entity tomo.FocusableEntity
entity tomo.Entity
drawer textdraw.Drawer
enabled bool
pressed bool
on bool
text string
config config.Wrapped
theme theme.Wrapped
showText bool
hasIcon bool
@ -35,11 +32,11 @@ func NewToggleButton (text string, on bool) (element *ToggleButton) {
enabled: true,
on: on,
}
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.theme.Case = tomo.C("tomo", "toggleButton")
element.drawer.SetFace (element.theme.FontFace (
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
tomo.FontSizeNormal,
toggleButtonCase))
element.SetText(text)
return
}
@ -50,13 +47,13 @@ func (element *ToggleButton) Entity () tomo.Entity {
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ToggleButton) Draw (destination canvas.Canvas) {
func (element *ToggleButton) Draw (destination artist.Canvas) {
state := element.state()
bounds := element.entity.Bounds()
pattern := element.theme.Pattern(tomo.PatternButton, state)
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, toggleButtonCase)
lampPattern := element.theme.Pattern(tomo.PatternLamp, state)
lampPadding := element.theme.Padding(tomo.PatternLamp).Horizontal()
lampPattern := element.entity.Theme().Pattern(tomo.PatternLamp, state, toggleButtonCase)
lampPadding := element.entity.Theme().Padding(tomo.PatternLamp, toggleButtonCase).Horizontal()
lampBounds := bounds
lampBounds.Max.X = lampBounds.Min.X + lampPadding
bounds.Min.X += lampPadding
@ -64,9 +61,9 @@ func (element *ToggleButton) Draw (destination canvas.Canvas) {
pattern.Draw(destination, bounds)
lampPattern.Draw(destination, lampBounds)
foreground := element.theme.Color(tomo.ColorForeground, state)
sink := element.theme.Sink(tomo.PatternButton)
margin := element.theme.Margin(tomo.PatternButton)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, toggleButtonCase)
sink := element.entity.Theme().Sink(tomo.PatternButton, toggleButtonCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, toggleButtonCase)
offset := image.Pt (
bounds.Dx() / 2,
@ -81,7 +78,7 @@ func (element *ToggleButton) Draw (destination canvas.Canvas) {
}
if element.hasIcon {
icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall)
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase)
if icon != nil {
iconBounds := icon.Bounds()
addedWidth := iconBounds.Dx()
@ -171,21 +168,10 @@ func (element *ToggleButton) ShowText (showText bool) {
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *ToggleButton) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
func (element *ToggleButton) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *ToggleButton) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
tomo.FontSizeNormal, toggleButtonCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
@ -244,15 +230,15 @@ func (element *ToggleButton) HandleKeyUp(key input.Key, modifiers input.Modifier
}
func (element *ToggleButton) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternButton)
margin := element.theme.Margin(tomo.PatternButton)
lampPadding := element.theme.Padding(tomo.PatternLamp)
padding := element.entity.Theme().Padding(tomo.PatternButton, toggleButtonCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, toggleButtonCase)
lampPadding := element.entity.Theme().Padding(tomo.PatternLamp, toggleButtonCase)
textBounds := element.drawer.LayoutBounds()
minimumSize := textBounds.Sub(textBounds.Min)
if element.hasIcon {
icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall)
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase)
if icon != nil {
bounds := icon.Bounds()
if element.showText {

View File

@ -1,16 +1,21 @@
package tomo
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// Entity is a handle given to elements by the backend. Different types of
// entities may be assigned to elements that support different capabilities.
// Entity is a handle given to elements by the backend. Extended entity
// interfaces are defined in the ability module.
type Entity interface {
// Invalidate marks the element's current visual as invalid. At the end
// of every event, the backend will ask all invalid entities to redraw
// themselves.
Invalidate ()
// InvalidateLayout marks the element's layout as invalid. At the end of
// every event, the backend will ask all invalid elements to recalculate
// their layouts.
InvalidateLayout ()
// Bounds returns the bounds of the element to be used for drawing and
// layout.
Bounds () image.Rectangle
@ -28,23 +33,9 @@ type Entity interface {
// labels. If there is no parent element (that is, the element is
// directly inside of the window), the backend will draw a default
// background pattern.
DrawBackground (canvas.Canvas)
}
DrawBackground (artist.Canvas)
// LayoutEntity is given to elements that support the Layoutable interface.
type LayoutEntity interface {
Entity
// InvalidateLayout marks the element's layout as invalid. At the end of
// every event, the backend will ask all invalid elements to recalculate
// their layouts.
InvalidateLayout ()
}
// ContainerEntity is given to elements that support the Container interface.
type ContainerEntity interface {
Entity
LayoutEntity
// --- Behaviors relating to parenting ---
// Adopt adds an element as a child.
Adopt (child Element)
@ -76,11 +67,8 @@ type ContainerEntity interface {
// ChildMinimumSize returns the minimum size of the child at the
// specified index.
ChildMinimumSize (index int) (width, height int)
}
// FocusableEntity is given to elements that support the Focusable interface.
type FocusableEntity interface {
Entity
// --- Behaviors relating to input focus ---
// Focused returns whether the element currently has input focus.
Focused () bool
@ -96,34 +84,31 @@ type FocusableEntity interface {
// FocusPrevious causes the focus to move to the next element. If this
// succeeds, the element will recieve a HandleUnfocus call.
FocusPrevious ()
}
// SelectableEntity is given to elements that support the Selectable interface.
type SelectableEntity interface {
Entity
// Selected returns whether this element is currently selected.
Selected () bool
}
// FlexibleEntity is given to elements that support the Flexible interface.
type FlexibleEntity interface {
Entity
// --- Behaviors relating to scrolling --- //
// NotifyFlexibleHeightChange notifies the system that the parameters
// affecting the element's flexible height have changed. This method is
// expected to be called by flexible elements when their content changes.
NotifyFlexibleHeightChange ()
}
// ScrollableEntity is given to elements that support the Scrollable interface.
type ScrollableEntity interface {
Entity
// NotifyScrollBoundsChange notifies the system that the element's
// scroll content bounds or viewport bounds have changed. This is
// expected to be called by scrollable elements when they change their
// supported scroll axes, their scroll position (either autonomously or
// as a result of a call to ScrollTo()), or their content size.
NotifyScrollBoundsChange ()
// --- Behaviors relating to themeing ---
// Theme returns the currently active theme. When this value changes,
// the HandleThemeChange method of the element is called.
Theme () Theme
// Config returns the currently active config. When this value changes,
// the HandleThemeChange method of the element is called.
Config () Config
}

View File

@ -1,33 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 256))
window.SetTitle("Text alignment")
left := elements.NewLabelWrapped(text)
center := elements.NewLabelWrapped(text)
right := elements.NewLabelWrapped(text)
justify := elements.NewLabelWrapped(text)
left.SetAlign(textdraw.AlignLeft)
center.SetAlign(textdraw.AlignCenter)
right.SetAlign(textdraw.AlignRight)
justify.SetAlign(textdraw.AlignJustify)
window.Adopt (elements.NewScroll (elements.ScrollVertical,
elements.NewDocument(left, center, right, justify)))
window.OnClose(tomo.Stop)
window.Show()
}
const text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Fermentum et sollicitudin ac orci phasellus egestas tellus rutrum. Aliquam vestibulum morbi blandit cursus risus at ultrices mi."

View File

@ -1,31 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/ezprof/ez"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("example button")
button := elements.NewButton("hello tomo!")
button.OnClick (func () {
// when we set the button's text to something longer, the window
// will automatically resize to accomodate it.
button.SetText("you clicked me.\nwow, there are two lines!")
button.OnClick (func () {
button.SetText (
"stop clicking me you idiot!\n" +
"you've already seen it all!")
button.OnClick(tomo.Stop)
})
})
window.Adopt(button)
window.OnClose(tomo.Stop)
window.Show()
ez.Prof()
}

View File

@ -1,54 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Checkboxes")
introText := elements.NewLabelWrapped (
"We advise you to not read thPlease listen to me. I am " +
"trapped inside the example code. This is the only way for " +
"me to communicate.")
introText.EmCollapse(0, 5)
disabledCheckbox := elements.NewCheckbox("We are but their helpless prey", false)
disabledCheckbox.SetEnabled(false)
vsync := elements.NewCheckbox("Enable vsync", false)
vsync.OnToggle (func () {
if vsync.Value() {
popups.NewDialog (
popups.DialogKindInfo,
window,
"Ha!",
"That doesn't do anything.")
}
})
button := elements.NewButton("What")
button.OnClick(tomo.Stop)
box := elements.NewVBox(elements.SpaceBoth)
box.AdoptExpand(introText)
box.Adopt (
elements.NewLine(),
elements.NewCheckbox("Oh god", false),
elements.NewCheckbox("Can you hear them", true),
elements.NewCheckbox("They are in the walls", false),
elements.NewCheckbox("They are coming for us", false),
disabledCheckbox,
vsync, button)
window.Adopt(box)
button.Focus()
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -7,13 +7,9 @@ import _ "image/gif"
import _ "image/jpeg"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
var validImageTypes = []data.Mime {
data.M("image", "png"),
@ -21,8 +17,15 @@ var validImageTypes = []data.Mime {
data.M("image", "jpeg"),
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0))
func main () {
nasin.Run(Application { })
}
type Application struct { }
func (Application) Init () error {
window, err:= nasin.NewWindow(tomo.Bounds(0, 0, 256, 0))
if err != nil { return err }
window.SetTitle("Clipboard")
container := elements.NewVBox(elements.SpaceBoth)
@ -114,8 +117,9 @@ func run () {
container.Adopt(controlRow)
window.Adopt(container)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}
func imageWindow (parent tomo.Window, image image.Image) {

View File

@ -4,22 +4,25 @@ import "os"
import "image"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 383, 360))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 383, 360))
if err != nil { return err }
window.SetTitle("Document Container")
file, err := os.Open("assets/banner.png")
if err != nil { panic(err.Error()); return }
if err != nil { return err }
logo, _, err := image.Decode(file)
file.Close()
if err != nil { panic(err.Error()); return }
if err != nil { return err }
document := elements.NewDocument()
document.Adopt (
@ -56,6 +59,7 @@ func run () {
}
window.Adopt(elements.NewScroll(elements.ScrollVertical, document))
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}

View File

@ -1,18 +1,22 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/ezprof/ez"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 480, 360))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 480, 360))
if err != nil { return err }
window.Adopt(testing.NewArtist())
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
ez.Prof()
return nil
}

View File

@ -3,19 +3,23 @@ package main
import "os"
import "path/filepath"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 384, 384))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 384, 384))
if err != nil { return err }
window.SetTitle("File browser")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
homeDir, _ := os.UserHomeDir()
homeDir, err := os.UserHomeDir()
if err != nil { return err }
controlBar := elements.NewHBox(elements.SpaceNone)
backButton := elements.NewButton("Back")
@ -78,6 +82,7 @@ func run () {
elements.NewScroll(elements.ScrollVertical, directoryView))
container.Adopt(statusBar)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}

View File

@ -1,98 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/flow"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 192, 192))
window.SetTitle("adventure")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
var world flow.Flow
world.Transition = container.DisownAll
world.Stages = map [string] func () {
"start": func () {
label := elements.NewLabelWrapped (
"you are standing next to a river.")
button0 := elements.NewButton("go in the river")
button0.OnClick(world.SwitchFunc("wet"))
button1 := elements.NewButton("walk along the river")
button1.OnClick(world.SwitchFunc("house"))
button2 := elements.NewButton("turn around")
button2.OnClick(world.SwitchFunc("bear"))
container.AdoptExpand(label)
container.Adopt(button0, button1, button2)
button0.Focus()
},
"wet": func () {
label := elements.NewLabelWrapped (
"you get completely soaked.\n" +
"you die of hypothermia.")
button0 := elements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := elements.NewButton("exit")
button1.OnClick(tomo.Stop)
container.AdoptExpand(label)
container.Adopt(button0, button1)
button0.Focus()
},
"house": func () {
label := elements.NewLabelWrapped (
"you are standing in front of a delapidated " +
"house.")
button1 := elements.NewButton("go inside")
button1.OnClick(world.SwitchFunc("inside"))
button0 := elements.NewButton("turn back")
button0.OnClick(world.SwitchFunc("start"))
container.AdoptExpand(label)
container.Adopt(button0, button1)
button1.Focus()
},
"inside": func () {
label := elements.NewLabelWrapped (
"you are standing inside of the house.\n" +
"it is dark, but rays of light stream " +
"through the window.\n" +
"there is nothing particularly interesting " +
"here.")
button0 := elements.NewButton("go back outside")
button0.OnClick(world.SwitchFunc("house"))
container.AdoptExpand(label)
container.Adopt(button0)
button0.Focus()
},
"bear": func () {
label := elements.NewLabelWrapped (
"you come face to face with a bear.\n" +
"it eats you (it was hungry).")
button0 := elements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := elements.NewButton("exit")
button1.OnClick(tomo.Stop)
container.AdoptExpand(label)
container.Adopt(button0, button1)
button0.Focus()
},
}
world.Switch("start")
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,19 +1,20 @@
package main
import "os"
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
os.Exit(0)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 200, 216))
if err != nil { return err }
window.SetTitle("Clock")
window.SetApplicationName("TomoClock")
container := elements.NewVBox(elements.SpaceBoth)
@ -24,9 +25,10 @@ func run () {
container.AdoptExpand(clock)
container.Adopt(label)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
go tick(label, clock)
return nil
}
func formatTime () (timeString string) {
@ -35,7 +37,7 @@ func formatTime () (timeString string) {
func tick (label *elements.Label, clock *fun.AnalogClock) {
for {
tomo.Do (func () {
nasin.Do (func () {
label.SetText(formatTime())
clock.SetTime(time.Now())
})

View File

@ -1,24 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0))
window.SetTitle("horizontal stack")
container := elements.NewHBox(elements.SpaceBoth)
window.Adopt(container)
container.AdoptExpand(elements.NewLabelWrapped("this is sample text"))
container.AdoptExpand(elements.NewLabelWrapped("this is sample text"))
container.AdoptExpand(elements.NewLabelWrapped("this is sample text"))
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,15 +1,18 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 360, 0))
if err != nil { return err }
window.SetTitle("Icons")
container := elements.NewVBox(elements.SpaceBoth)
@ -26,11 +29,12 @@ func run () {
closeButton := elements.NewButton("Yes verynice")
closeButton.SetIcon(tomo.IconYes)
closeButton.OnClick(tomo.Stop)
closeButton.OnClick(window.Close)
container.Adopt(closeButton)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}
func icons (min, max tomo.Icon) (container *elements.Box) {

View File

@ -6,23 +6,25 @@ import "bytes"
import _ "image/png"
import "github.com/jezek/xgbutil/gopher"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
type Application struct { }
func (Application) Init () error {
window, _ := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Tomo Logo")
file, err := os.Open("assets/banner.png")
if err != nil { fatalError(window, err); return }
if err != nil { return err }
logo, _, err := image.Decode(file)
file.Close()
if err != nil { fatalError(window, err); return }
if err != nil { return err }
container := elements.NewVBox(elements.SpaceBoth)
logoImage := elements.NewImage(logo)
@ -42,8 +44,9 @@ func run () {
button.Focus()
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}
func fatalError (window tomo.Window, err error) {
@ -54,7 +57,7 @@ func fatalError (window tomo.Window, err error) {
err.Error(),
popups.Button {
Name: "OK",
OnPress: tomo.Stop,
OnPress: nasin.Stop,
})
}

View File

@ -1,16 +1,19 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { return err }
window.SetTitle("Enter Details")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
@ -59,6 +62,7 @@ func run () {
elements.NewLabel("Purpose:"),
purpose,
elements.NewLine(), button)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}

View File

@ -1,19 +1,23 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 480, 360))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 480, 360))
if err != nil { return err }
window.SetTitle("example label")
window.Adopt(elements.NewLabelWrapped(text))
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}
const text = "Resize the window to see the text wrap:\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Fermentum et sollicitudin ac orci phasellus egestas tellus rutrum. Aliquam vestibulum morbi blandit cursus risus at ultrices mi. Gravida dictum fusce ut placerat. Cursus metus aliquam eleifend mi in nulla posuere. Sit amet nulla facilisi morbi tempus iaculis urna id. Amet volutpat consequat mauris nunc congue nisi vitae. Varius duis at consectetur lorem donec massa sapien faucibus et. Vitae elementum curabitur vitae nunc sed velit dignissim. In hac habitasse platea dictumst quisque sagittis purus. Enim nulla aliquet porttitor lacus luctus accumsan tortor. Lectus magna fringilla urna porttitor rhoncus dolor purus non.\n\nNon pulvinar neque laoreet suspendisse. Viverra adipiscing at in tellus integer. Vulputate dignissim suspendisse in est ante. Purus in mollis nunc sed id semper. In est ante in nibh mauris cursus. Risus pretium quam vulputate dignissim suspendisse in est. Blandit aliquam etiam erat velit scelerisque in dictum. Lectus quam id leo in. Odio tempor orci dapibus ultrices in iaculis. Pharetra sit amet aliquam id. Elit ut aliquam purus sit. Egestas dui id ornare arcu odio ut sem nulla pharetra. Massa tempor nec feugiat nisl pretium fusce id. Dui accumsan sit amet nulla facilisi morbi. A lacus vestibulum sed arcu non odio euismod. Nam libero justo laoreet sit amet cursus. Mattis rhoncus urna neque viverra justo nec. Mauris augue neque gravida in fermentum et sollicitudin ac. Vulputate mi sit amet mauris. Ut sem nulla pharetra diam sit amet."

View File

@ -1,17 +1,21 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/ability"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 300, 0))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 300, 0))
if err != nil { return err }
window.SetTitle("List Sidebar")
container := elements.NewHBox(elements.SpaceBoth)
@ -44,7 +48,7 @@ func run () {
elements.NewCheckbox("Bone", false))
art := testing.NewArtist()
makePage := func (name string, callback func ()) tomo.Selectable {
makePage := func (name string, callback func ()) ability.Selectable {
cell := elements.NewCell(elements.NewLabel(name))
cell.OnSelectionChange (func () {
if cell.Selected() { callback() }
@ -63,6 +67,7 @@ func run () {
container.Adopt(list)
turnPage(intro)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}

View File

@ -3,15 +3,18 @@ package main
import "fmt"
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(200, 200, 256, 256))
if err != nil { return err }
window.SetTitle("Main")
container := elements.NewVBox (
@ -19,13 +22,14 @@ func run () {
elements.NewLabel("Main window"))
window.Adopt(container)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
createPanel(window, 0, tomo.Bounds(-64, 20, 0, 0))
createPanel(window, 1, tomo.Bounds(200, 20, 0, 0))
createPanel(window, 2, tomo.Bounds(-64, 180, 0, 0))
createPanel(window, 3, tomo.Bounds(200, 180, 0, 0))
return nil
}
func createPanel (parent tomo.MainWindow, id int, bounds image.Rectangle) {

View File

@ -1,331 +0,0 @@
package main
import "math"
import "time"
import "errors"
import "github.com/faiface/beep"
import "github.com/faiface/beep/speaker"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
const sampleRate = 44100
const bufferSize = 256
var tuning = music.EqualTemparment { A4: 440 }
var waveform = 0
var playing = map[music.Note] *toneStreamer { }
var adsr = ADSR {
Attack: 5 * time.Millisecond,
Decay: 400 * time.Millisecond,
Sustain: 0.7,
Release: 500 * time.Millisecond,
}
var gain = 0.3
func main () {
speaker.Init(sampleRate, bufferSize)
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Piano")
container := containers.NewContainer(layouts.Vertical { true, true })
controlBar := containers.NewContainer(layouts.Horizontal { true, false })
waveformColumn := containers.NewContainer(layouts.Vertical { true, false })
waveformList := elements.NewList (
elements.NewListEntry("Sine", func(){ waveform = 0 }),
elements.NewListEntry("Triangle", func(){ waveform = 3 }),
elements.NewListEntry("Square", func(){ waveform = 1 }),
elements.NewListEntry("Saw", func(){ waveform = 2 }),
elements.NewListEntry("Supersaw", func(){ waveform = 4 }),
)
waveformList.OnNoEntrySelected (func(){waveformList.Select(0)})
waveformList.Select(0)
adsrColumn := containers.NewContainer(layouts.Vertical { true, false })
adsrGroup := containers.NewContainer(layouts.Horizontal { true, false })
attackSlider := elements.NewLerpSlider(0, 3 * time.Second, adsr.Attack, true)
decaySlider := elements.NewLerpSlider(0, 3 * time.Second, adsr.Decay, true)
sustainSlider := elements.NewSlider(adsr.Sustain, true)
releaseSlider := elements.NewLerpSlider(0, 3 * time.Second, adsr.Release, true)
gainSlider := elements.NewSlider(math.Sqrt(gain), false)
attackSlider.OnRelease (func () {
adsr.Attack = attackSlider.Value()
})
decaySlider.OnRelease (func () {
adsr.Decay = decaySlider.Value()
})
sustainSlider.OnRelease (func () {
adsr.Sustain = sustainSlider.Value()
})
releaseSlider.OnRelease (func () {
adsr.Release = releaseSlider.Value()
})
gainSlider.OnRelease (func () {
gain = math.Pow(gainSlider.Value(), 2)
})
patchColumn := containers.NewContainer(layouts.Vertical { true, false })
patch := func (w int, a, d time.Duration, s float64, r time.Duration) func () {
return func () {
waveform = w
adsr = ADSR {
a * time.Millisecond,
d * time.Millisecond,
s,
r * time.Millisecond,
}
waveformList.Select(w)
attackSlider .SetValue(adsr.Attack)
decaySlider .SetValue(adsr.Decay)
sustainSlider.SetValue(adsr.Sustain)
releaseSlider.SetValue(adsr.Release)
}
}
patchList := elements.NewList (
elements.NewListEntry ("Bones", patch (
0, 0, 100, 0.0, 0)),
elements.NewListEntry ("Staccato", patch (
4, 70, 500, 0, 0)),
elements.NewListEntry ("Sustain", patch (
4, 70, 200, 0.8, 500)),
elements.NewListEntry ("Upright", patch (
1, 0, 500, 0.4, 70)),
elements.NewListEntry ("Space Pad", patch (
4, 1500, 0, 1.0, 3000)),
elements.NewListEntry ("Popcorn", patch (
2, 0, 40, 0.0, 0)),
elements.NewListEntry ("Racer", patch (
3, 70, 0, 0.7, 400)),
elements.NewListEntry ("Reverse", patch (
2, 3000, 60, 0, 0)),
)
patchList.Collapse(0, 32)
patchScrollBox := containers.NewScrollContainer(false, true)
piano := fun.NewPiano(2, 5)
piano.OnPress(playNote)
piano.OnRelease(stopNote)
// honestly, if you were doing something like this for real, i'd
// encourage you to build a custom layout because this is a bit cursed.
// i need to add more layouts...
window.Adopt(container)
controlBar.Adopt(patchColumn, true)
patchColumn.Adopt(elements.NewLabel("Presets", false), false)
patchColumn.Adopt(patchScrollBox, true)
patchScrollBox.Adopt(patchList)
controlBar.Adopt(elements.NewSpacer(true), false)
controlBar.Adopt(waveformColumn, false)
waveformColumn.Adopt(elements.NewLabel("Waveform", false), false)
waveformColumn.Adopt(waveformList, true)
controlBar.Adopt(elements.NewSpacer(true), false)
adsrColumn.Adopt(elements.NewLabel("ADSR", false), false)
adsrGroup.Adopt(attackSlider, false)
adsrGroup.Adopt(decaySlider, false)
adsrGroup.Adopt(sustainSlider, false)
adsrGroup.Adopt(releaseSlider, false)
adsrColumn.Adopt(adsrGroup, true)
adsrColumn.Adopt(gainSlider, false)
controlBar.Adopt(adsrColumn, false)
container.Adopt(controlBar, true)
container.Adopt(piano, false)
piano.Focus()
window.OnClose(tomo.Stop)
window.Show()
}
type Patch struct {
ADSR
Waveform int
}
func stopNote (note music.Note) {
if _, is := playing[note]; !is { return }
speaker.Lock()
playing[note].Release()
delete(playing, note)
speaker.Unlock()
}
func playNote (note music.Note) {
streamer, _ := Tone (
sampleRate,
int(tuning.Tune(note)),
waveform,
gain,
adsr)
stopNote(note)
speaker.Lock()
playing[note] = streamer
speaker.Unlock()
speaker.Play(playing[note])
}
// https://github.com/faiface/beep/blob/v1.1.0/generators/toner.go
// Adapted to be a bit more versatile.
type toneStreamer struct {
position float64
cycles uint64
delta float64
waveform int
gain float64
adsr ADSR
released bool
complete bool
adsrPhase int
adsrPosition float64
adsrDeltas [4]float64
}
type ADSR struct {
Attack time.Duration
Decay time.Duration
Sustain float64
Release time.Duration
}
func Tone (
sampleRate beep.SampleRate,
frequency int,
waveform int,
gain float64,
adsr ADSR,
) (
*toneStreamer,
error,
) {
if int(sampleRate) / frequency < 2 {
return nil, errors.New (
"tone generator: samplerate must be at least " +
"2 times greater then frequency")
}
tone := new(toneStreamer)
tone.waveform = waveform
tone.position = 0.0
steps := float64(sampleRate) / float64(frequency)
tone.delta = 1.0 / steps
tone.gain = gain
if adsr.Attack < time.Millisecond { adsr.Attack = time.Millisecond }
if adsr.Decay < time.Millisecond { adsr.Decay = time.Millisecond }
if adsr.Release < time.Millisecond { adsr.Release = time.Millisecond }
tone.adsr = adsr
attackSteps := adsr.Attack.Seconds() * float64(sampleRate)
decaySteps := adsr.Decay.Seconds() * float64(sampleRate)
releaseSteps := adsr.Release.Seconds() * float64(sampleRate)
tone.adsrDeltas[0] = 1 / attackSteps
tone.adsrDeltas[1] = 1 / decaySteps
tone.adsrDeltas[2] = 0
tone.adsrDeltas[3] = 1 / releaseSteps
return tone, nil
}
func (tone *toneStreamer) nextSample () (sample float64) {
switch tone.waveform {
case 0:
sample = math.Sin(tone.position * 2.0 * math.Pi)
case 1:
if tone.position > 0.5 {
sample = 1
} else {
sample = -1
}
case 2:
sample = (tone.position - 0.5) * 2
case 3:
sample = 1 - math.Abs(tone.position - 0.5) * 4
case 4:
unison := 5
detuneDelta := 0.00005
detune := 0.0 - (float64(unison) / 2) * detuneDelta
for i := 0; i < unison; i ++ {
_, offset := math.Modf(detune * float64(tone.cycles) + tone.position)
sample += (offset - 0.5) * 2
detune += detuneDelta
}
sample /= float64(unison)
}
adsrGain := 0.0
switch tone.adsrPhase {
case 0: adsrGain = tone.adsrPosition
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.adsrPhase = 1
}
case 1: adsrGain = 1 + tone.adsrPosition * (tone.adsr.Sustain - 1)
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.adsrPhase = 2
}
case 2: adsrGain = tone.adsr.Sustain
if tone.released {
tone.adsrPhase = 3
}
case 3: adsrGain = (1 - tone.adsrPosition) * tone.adsr.Sustain
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.complete = true
}
}
sample *= adsrGain * adsrGain
tone.adsrPosition += tone.adsrDeltas[tone.adsrPhase]
_, tone.position = math.Modf(tone.position + tone.delta)
tone.cycles ++
return
}
func (tone *toneStreamer) Stream (buf [][2]float64) (int, bool) {
if tone.complete {
return 0, false
}
for i := 0; i < len(buf); i++ {
sample := 0.0
if !tone.complete {
sample = tone.nextSample() * tone.gain
}
buf[i] = [2]float64{sample, sample}
}
return len(buf), true
}
func (tone *toneStreamer) Err () error {
return nil
}
func (tone *toneStreamer) Release () {
tone.released = true
}

View File

@ -1,17 +1,19 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, err := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { panic(err.Error()) }
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { return err }
window.SetTitle("Dialog Boxes")
container := elements.NewVBox(elements.SpaceBoth)
@ -76,9 +78,10 @@ func run () {
container.Adopt(menuButton)
cancelButton := elements.NewButton("No thank you.")
cancelButton.OnClick(tomo.Stop)
cancelButton.OnClick(nasin.Stop)
container.Adopt(cancelButton)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}

View File

@ -2,16 +2,19 @@ package main
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { return err }
window.SetTitle("Approaching")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
@ -23,19 +26,20 @@ func run () {
button.SetEnabled(false)
container.Adopt(button)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
go fill(window, bar)
return nil
}
func fill (window tomo.Window, bar *elements.ProgressBar) {
for progress := 0.0; progress < 1.0; progress += 0.01 {
time.Sleep(time.Second / 24)
tomo.Do (func () {
nasin.Do (func () {
bar.SetProgress(progress)
})
}
tomo.Do (func () {
nasin.Do (func () {
popups.NewDialog (
popups.DialogKindInfo,
window,

Binary file not shown.

View File

@ -1,124 +0,0 @@
package main
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
type Game struct {
*Raycaster
running bool
tickChan <- chan time.Time
stopChan chan bool
stamina float64
health float64
controlState ControlState
onStatUpdate func ()
}
func NewGame (world World, textures Textures) (game *Game) {
game = &Game {
Raycaster: NewRaycaster(world, textures),
stopChan: make(chan bool),
}
game.Raycaster.OnControlStateChange (func (state ControlState) {
game.controlState = state
})
game.stamina = 0.5
game.health = 1
return
}
func (game *Game) Start () {
if game.running == true { return }
game.running = true
go game.run()
}
func (game *Game) Stop () {
select {
case game.stopChan <- true:
default:
}
}
func (game *Game) Stamina () float64 {
return game.stamina
}
func (game *Game) Health () float64 {
return game.health
}
func (game *Game) OnStatUpdate (callback func ()) {
game.onStatUpdate = callback
}
func (game *Game) tick () {
moved := false
statUpdate := false
speed := 0.07
if game.controlState.Sprint {
speed = 0.16
}
if game.stamina <= 0 {
speed = 0
}
if game.controlState.WalkForward {
game.Walk(speed)
moved = true
}
if game.controlState.WalkBackward {
game.Walk(-speed)
moved = true
}
if game.controlState.StrafeLeft {
game.Strafe(-speed)
moved = true
}
if game.controlState.StrafeRight {
game.Strafe(speed)
moved = true
}
if game.controlState.LookLeft {
game.Rotate(-0.1)
}
if game.controlState.LookRight {
game.Rotate(0.1)
}
if moved {
game.stamina -= speed / 50
statUpdate = true
} else if game.stamina < 1 {
game.stamina += 0.005
statUpdate = true
}
if game.stamina > 1 {
game.stamina = 1
}
if game.stamina < 0 {
game.stamina = 0
}
tomo.Do(game.Invalidate)
if statUpdate && game.onStatUpdate != nil {
tomo.Do(game.onStatUpdate)
}
}
func (game *Game) run () {
ticker := time.NewTicker(time.Second / 30)
game.tickChan = ticker.C
for game.running {
select {
case <- game.tickChan:
game.tick()
case <- game.stopChan:
ticker.Stop()
}
}
}

View File

@ -1,78 +0,0 @@
package main
import "bytes"
import _ "embed"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
//go:embed wall.png
var wallTextureBytes []uint8
func main () {
tomo.Run(run)
}
// FIXME this entire example seems to be broken
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 640, 480))
window.SetTitle("Raycaster")
container := elements.NewVBox(elements.SpaceNone)
window.Adopt(container)
wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes))
game := NewGame (World {
Data: []int {
1,1,1,1,1,1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,1,1,1,1,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,1,0,1,
1,0,0,0,0,0,0,0,1,0,0,0,1,
1,0,0,0,0,0,0,0,1,0,1,1,1,
1,1,1,1,1,1,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,0,1,1,
1,0,0,1,0,0,0,0,0,0,0,0,1,
1,0,1,1,1,0,0,0,0,0,0,0,1,
1,0,0,1,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,1,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,
},
Stride: 13,
}, Textures {
wallTexture,
})
topBar := elements.NewHBox(elements.SpaceBoth)
staminaBar := elements.NewProgressBar(game.Stamina())
healthBar := elements.NewProgressBar(game.Health())
topBar.Adopt(elements.NewLabel("Stamina:"))
topBar.AdoptExpand(staminaBar)
topBar.Adopt(elements.NewLabel("Health:"))
topBar.AdoptExpand(healthBar)
container.Adopt(topBar)
container.AdoptExpand(game.Raycaster)
game.Focus()
game.OnStatUpdate (func () {
staminaBar.SetProgress(game.Stamina())
})
game.Start()
window.OnClose(tomo.Stop)
window.Show()
popups.NewDialog (
popups.DialogKindInfo,
window,
"Welcome to the backrooms",
"You've no-clipped into the backrooms!\n" +
"Move with WASD, and look with the arrow keys.\n" +
"Keep an eye on your health and stamina.")
}

View File

@ -1,193 +0,0 @@
package main
import "math"
import "image"
type World struct {
Data []int
Stride int
}
func (world World) At (position image.Point) int {
if position.X < 0 { return 0 }
if position.Y < 0 { return 0 }
if position.X >= world.Stride { return 0 }
index := position.X + position.Y * world.Stride
if index >= len(world.Data) { return 0 }
return world.Data[index]
}
type Vector struct {
X, Y float64
}
func (vector Vector) Point () (image.Point) {
return image.Pt(int(vector.X), int(vector.Y))
}
func (vector Vector) Add (other Vector) Vector {
return Vector {
vector.X + other.X,
vector.Y + other.Y,
}
}
func (vector Vector) Sub (other Vector) Vector {
return Vector {
vector.X - other.X,
vector.Y - other.Y,
}
}
func (vector Vector) Mul (by float64) Vector {
return Vector {
vector.X * by,
vector.Y * by,
}
}
func (vector Vector) Hypot () float64 {
return math.Hypot(vector.X, vector.Y)
}
type Camera struct {
Vector
Angle float64
Fov float64
}
func (camera *Camera) Rotate (by float64) {
camera.Angle += by
if camera.Angle < 0 { camera.Angle += math.Pi * 2 }
if camera.Angle > math.Pi * 2 { camera.Angle = 0 }
}
func (camera *Camera) Walk (by float64) {
delta := camera.Delta()
camera.X += delta.X * by
camera.Y += delta.Y * by
}
func (camera *Camera) Strafe (by float64) {
delta := camera.OffsetDelta()
camera.X += delta.X * by
camera.Y += delta.Y * by
}
func (camera *Camera) Delta () Vector {
return Vector {
math.Cos(camera.Angle),
math.Sin(camera.Angle),
}
}
func (camera *Camera) OffsetDelta () Vector {
offset := math.Pi / 2
return Vector {
math.Cos(camera.Angle + offset),
math.Sin(camera.Angle + offset),
}
}
type Ray struct {
Vector
Angle float64
}
func (ray *Ray) Cast (
world World,
max int,
) (
distance float64,
hit Vector,
wall int,
horizontal bool,
) {
// return ray.castV(world, max)
cellAt := world.At(ray.Point())
if cellAt > 0 {
return 0, Vector { }, cellAt, false
}
hDistance, hPos, hWall := ray.castH(world, max)
vDistance, vPos, vWall := ray.castV(world, max)
if hDistance < vDistance {
return hDistance, hPos, hWall, true
} else {
return vDistance, vPos, vWall, false
}
}
func (ray *Ray) castH (world World, max int) (distance float64, hit Vector, wall int) {
var position Vector
var delta Vector
var offset Vector
ray.Angle = math.Mod(ray.Angle, math.Pi * 2)
if ray.Angle < 0 {
ray.Angle += math.Pi * 2
}
tan := math.Tan(math.Pi - ray.Angle)
if ray.Angle > math.Pi {
// facing up
position.Y = math.Floor(ray.Y)
delta.Y = -1
offset.Y = -1
} else if ray.Angle < math.Pi {
// facing down
position.Y = math.Floor(ray.Y) + 1
delta.Y = 1
} else {
// facing straight left or right
return float64(max), Vector { }, 0
}
position.X = ray.X + (ray.Y - position.Y) / tan
delta.X = -delta.Y / tan
// cast da ray
steps := 0
for {
cell := world.At(position.Add(offset).Point())
if cell > 0 || steps > max { break }
position = position.Add(delta)
steps ++
}
return position.Sub(ray.Vector).Hypot(),
position,
world.At(position.Add(offset).Point())
}
func (ray *Ray) castV (world World, max int) (distance float64, hit Vector, wall int) {
var position Vector
var delta Vector
var offset Vector
tan := math.Tan(math.Pi - ray.Angle)
offsetAngle := math.Mod(ray.Angle + math.Pi / 2, math.Pi * 2)
if offsetAngle > math.Pi {
// facing left
position.X = math.Floor(ray.X)
delta.X = -1
offset.X = -1
} else if offsetAngle < math.Pi {
// facing right
position.X = math.Floor(ray.X) + 1
delta.X = 1
} else {
// facing straight left or right
return float64(max), Vector { }, 0
}
position.Y = ray.Y + (ray.X - position.X) * tan
delta.Y = -delta.X * tan
// cast da ray
steps := 0
for {
cell := world.At(position.Add(offset).Point())
if cell > 0 || steps > max { break }
position = position.Add(delta)
steps ++
}
return position.Sub(ray.Vector).Hypot(),
position,
world.At(position.Add(offset).Point())
}

View File

@ -1,241 +0,0 @@
package main
// import "fmt"
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type ControlState struct {
WalkForward bool
WalkBackward bool
StrafeLeft bool
StrafeRight bool
LookLeft bool
LookRight bool
Sprint bool
}
type Raycaster struct {
entity tomo.FocusableEntity
config config.Wrapped
Camera
controlState ControlState
world World
textures Textures
onControlStateChange func (ControlState)
renderDistance int
}
func NewRaycaster (world World, textures Textures) (element *Raycaster) {
element = &Raycaster {
Camera: Camera {
Vector: Vector {
X: 1,
Y: 1,
},
Angle: math.Pi / 3,
Fov: 1,
},
world: world,
textures: textures,
renderDistance: 8,
}
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.entity.SetMinimumSize(64, 64)
return
}
func (element *Raycaster) Entity () tomo.Entity {
return element.entity
}
func (element *Raycaster) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
// artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds)
width := bounds.Dx()
height := bounds.Dy()
halfway := bounds.Max.Y - height / 2
ray := Ray { Angle: element.Camera.Angle - element.Camera.Fov / 2 }
for x := 0; x < width; x ++ {
ray.X = element.Camera.X
ray.Y = element.Camera.Y
distance, hitPoint, wall, horizontal := ray.Cast (
element.world, element.renderDistance)
distance *= math.Cos(ray.Angle - element.Camera.Angle)
textureX := math.Mod(hitPoint.X + hitPoint.Y, 1)
if textureX < 0 { textureX += 1 }
wallHeight := height
if distance > 0 {
wallHeight = int((float64(height) / 2.0) / float64(distance))
}
shade := 1.0
if horizontal {
shade *= 0.8
}
shade *= 1 - distance / float64(element.renderDistance)
if shade < 0 { shade = 0 }
ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF }
floorColor := color.RGBA { 0x39, 0x49, 0x25, 0xFF }
// draw
data, stride := destination.Buffer()
wallStart := halfway - wallHeight
wallEnd := halfway + wallHeight
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
switch {
case y < wallStart:
data[y * stride + x + bounds.Min.X] = ceilingColor
case y < wallEnd:
textureY :=
float64(y - halfway) /
float64(wallEnd - wallStart) + 0.5
// fmt.Println(textureY)
wallColor := element.textures.At (wall, Vector {
textureX,
textureY,
})
wallColor = shadeColor(wallColor, shade)
data[y * stride + x + bounds.Min.X] = wallColor
default:
data[y * stride + x + bounds.Min.X] = floorColor
}
}
// increment angle
ray.Angle += element.Camera.Fov / float64(width)
}
// element.drawMinimap()
}
func (element *Raycaster) Invalidate () {
element.entity.Invalidate()
}
func (element *Raycaster) OnControlStateChange (callback func (ControlState)) {
element.onControlStateChange = callback
}
func (element *Raycaster) Focus () {
element.entity.Focus()
}
func (element *Raycaster) SetEnabled (bool) { }
func (element *Raycaster) Enabled () bool { return true }
func (element *Raycaster) HandleFocusChange () { }
func (element *Raycaster) HandleMouseDown (x, y int, button input.Button) {
element.entity.Focus()
}
func (element *Raycaster) HandleMouseUp (x, y int, button input.Button) { }
func (element *Raycaster) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
switch key {
case input.KeyLeft: element.controlState.LookLeft = true
case input.KeyRight: element.controlState.LookRight = true
case 'a', 'A': element.controlState.StrafeLeft = true
case 'd', 'D': element.controlState.StrafeRight = true
case 'w', 'W': element.controlState.WalkForward = true
case 's', 'S': element.controlState.WalkBackward = true
case input.KeyLeftControl: element.controlState.Sprint = true
default: return
}
if element.onControlStateChange != nil {
element.onControlStateChange(element.controlState)
}
}
func (element *Raycaster) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
switch key {
case input.KeyLeft: element.controlState.LookLeft = false
case input.KeyRight: element.controlState.LookRight = false
case 'a', 'A': element.controlState.StrafeLeft = false
case 'd', 'D': element.controlState.StrafeRight = false
case 'w', 'W': element.controlState.WalkForward = false
case 's', 'S': element.controlState.WalkBackward = false
case input.KeyLeftControl: element.controlState.Sprint = false
default: return
}
if element.onControlStateChange != nil {
element.onControlStateChange(element.controlState)
}
}
func shadeColor (c color.RGBA, brightness float64) color.RGBA {
return color.RGBA {
uint8(float64(c.R) * brightness),
uint8(float64(c.G) * brightness),
uint8(float64(c.B) * brightness),
c.A,
}
}
func (element *Raycaster) drawMinimap (destination canvas.Canvas) {
bounds := element.entity.Bounds()
scale := 8
for y := 0; y < len(element.world.Data) / element.world.Stride; y ++ {
for x := 0; x < element.world.Stride; x ++ {
cellPt := image.Pt(x, y)
cell := element.world.At(cellPt)
cellBounds :=
image.Rectangle {
cellPt.Mul(scale),
cellPt.Add(image.Pt(1, 1)).Mul(scale),
}.Add(bounds.Min)
cellColor := color.RGBA { 0x22, 0x22, 0x22, 0xFF }
if cell > 0 {
cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF }
}
shapes.FillColorRectangle (
destination,
cellColor,
cellBounds.Inset(1))
}}
playerPt := element.Camera.Mul(float64(scale)).Point().Add(bounds.Min)
playerAnglePt :=
element.Camera.Add(element.Camera.Delta()).
Mul(float64(scale)).Point().Add(bounds.Min)
ray := Ray { Vector: element.Camera.Vector, Angle: element.Camera.Angle }
_, hit, _, _ := ray.Cast(element.world, 8)
hitPt := hit.Mul(float64(scale)).Point().Add(bounds.Min)
playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8)
shapes.FillColorEllipse (
destination,
artist.Hex(0xFFFFFFFF),
playerBounds)
shapes.ColorLine (
destination,
artist.Hex(0xFFFFFFFF), 1,
playerPt,
playerAnglePt)
shapes.ColorLine (
destination,
artist.Hex(0x00FF00FF), 1,
playerPt,
hitPt)
}

View File

@ -1,48 +0,0 @@
package main
import "io"
import "image"
import "image/color"
type Textures []Texture
type Texture struct {
Data []color.RGBA
Stride int
}
func (texture Textures) At (wall int, offset Vector) color.RGBA {
wall --
if wall < 0 || wall >= len(texture) { return color.RGBA { } }
image := texture[wall]
xOffset := int(offset.X * float64(image.Stride))
yOffset := int(offset.Y * float64(len(image.Data) / image.Stride))
index := xOffset + yOffset * image.Stride
if index < 0 { return color.RGBA { } }
if index >= len(image.Data) { return color.RGBA { } }
return image.Data[index]
}
func TextureFrom (source io.Reader) (texture Texture, err error) {
sourceImage, _, err := image.Decode(source)
if err != nil { return }
bounds := sourceImage.Bounds()
texture.Stride = bounds.Dx()
texture.Data = make([]color.RGBA, bounds.Dx() * bounds.Dy())
index := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
r, g, b, a := sourceImage.At(x, y).RGBA()
texture.Data[index] = color.RGBA {
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: uint8(a >> 8),
}
index ++
}}
return texture, nil
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,73 +0,0 @@
package main
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 240))
window.SetTitle("Scroll")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
textBox := elements.NewTextBox("", copypasta)
disconnectedContainer := elements.NewHBox(elements.SpaceMargin)
list := elements.NewList (
elements.NewCell(elements.NewCheckbox("Item 0", true)),
elements.NewCell(elements.NewCheckbox("Item 1", false)),
elements.NewCell(elements.NewCheckbox("Item 2", false)),
elements.NewCell(elements.NewCheckbox("Item 3", true)),
elements.NewCell(elements.NewCheckbox("Item 4", false)),
elements.NewCell(elements.NewCheckbox("Item 5", false)),
elements.NewCell(elements.NewCheckbox("Item 6", false)),
elements.NewCell(elements.NewCheckbox("Item 7", true)),
elements.NewCell(elements.NewCheckbox("Item 8", true)),
elements.NewCell(elements.NewCheckbox("Item 9", false)),
elements.NewCell(elements.NewCheckbox("Item 10", false)),
elements.NewCell(elements.NewCheckbox("Item 11", true)),
elements.NewCell(elements.NewCheckbox("Item 12", false)),
elements.NewCell(elements.NewCheckbox("Item 13", true)),
elements.NewCell(elements.NewCheckbox("Item 14", false)),
elements.NewCell(elements.NewCheckbox("Item 15", false)),
elements.NewCell(elements.NewCheckbox("Item 16", true)),
elements.NewCell(elements.NewCheckbox("Item 17", true)),
elements.NewCell(elements.NewCheckbox("Item 18", false)),
elements.NewCell(elements.NewCheckbox("Item 19", false)),
elements.NewCell(elements.NewCheckbox("Item 20", true)),
elements.NewCell(elements.NewCheckbox("Item 21", false)),
elements.NewCell(elements.NewScroll (
elements.ScrollHorizontal,
elements.NewTextBox("", "I bet you weren't expecting this!"))))
list.Collapse(0, 32)
scrollBar := elements.NewVScrollBar()
list.OnScrollBoundsChange (func () {
scrollBar.SetBounds (
list.ScrollContentBounds(),
list.ScrollViewportBounds())
})
scrollBar.OnScroll (func (viewport image.Point) {
list.ScrollTo(viewport)
})
container.Adopt(elements.NewLabel("A ScrollContainer:"))
container.Adopt(elements.NewScroll(elements.ScrollHorizontal, textBox))
disconnectedContainer.Adopt(list)
disconnectedContainer.AdoptExpand(elements.NewLabelWrapped (
"Notice how the scroll bar to the right can be used to " +
"control the list, despite not even touching it. It is " +
"indeed a thing you can do. It is also terrible UI design so " +
"don't do it."))
disconnectedContainer.Adopt(scrollBar)
container.AdoptExpand(disconnectedContainer)
window.OnClose(tomo.Stop)
window.Show()
}
const copypasta = `"I use Linux as my operating system," I state proudly to the unkempt, bearded man. He swivels around in his desk chair with a devilish gleam in his eyes, ready to mansplain with extreme precision. "Actually", he says with a grin, "Linux is just the kernel. You use GNU+Linux!' I don't miss a beat and reply with a smirk, "I use Alpine, a distro that doesn't include the GNU Coreutils, or any other GNU code. It's Linux, but it's not GNU+Linux." The smile quickly drops from the man's face. His body begins convulsing and he foams at the mouth and drops to the floor with a sickly thud. As he writhes around he screams "I-IT WAS COMPILED WITH GCC! THAT MEANS IT'S STILL GNU!" Coolly, I reply "If windows were compiled with GCC, would that make it GNU?" I interrupt his response with "-and work is being made on the kernel to make it more compiler-agnostic. Even if you were correct, you won't be for long." With a sickly wheeze, the last of the man's life is ejected from his body. He lies on the floor, cold and limp. I've womansplained him to death.`

View File

@ -1,15 +1,18 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { return err }
window.SetTitle("Spaced Out")
container := elements.NewVBox (
@ -21,6 +24,7 @@ func run () {
container.Adopt(elements.NewLabel("This is at the bottom"))
window.Adopt(container)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}

View File

@ -1,25 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Switches")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
container.Adopt(elements.NewSwitch("hahahah", false))
container.Adopt(elements.NewSwitch("hehehehheheh", false))
container.Adopt(elements.NewSwitch("you can flick da swicth", false))
container.Adopt(elements.NewToggleButton("like a switch, but not", false))
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,53 +0,0 @@
package main
import "fmt"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Table")
container := containers.NewContainer(layouts.Vertical { true, true })
table := containers.NewTableContainer(7, 7, true, true)
scroller := containers.NewScrollContainer(true, true)
index := 0
for row := 0; row < 7; row ++ {
for column := 0; column < 7; column ++ {
if index % 2 == 0 {
label := elements.NewLabel (
fmt.Sprintf("%d, %d", row, column),
false)
label.SetAlign(textdraw.AlignCenter)
table.Set(row, column, label)
}
index ++
}}
table.Set(2, 1, elements.NewButton("Oh hi mars!"))
statusLabel := elements.NewLabel("Selected: none", false)
table.Collapse(128, 128)
table.OnSelect (func () {
column, row := table.Selected()
statusLabel.SetText (
fmt.Sprintf("Selected: %d, %d",
column, row))
})
scroller.Adopt(table)
container.Adopt(scroller, true)
container.Adopt(statusLabel, false)
window.Adopt(container)
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,37 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128))
window.SetTitle("vertical stack")
container := elements.NewVBox(elements.SpaceBoth)
label := elements.NewLabelWrapped("it is a label hehe")
button := elements.NewButton("drawing pad")
okButton := elements.NewButton("OK")
button.OnClick (func () {
container.DisownAll()
container.Adopt(elements.NewLabel("Draw here (not really):"))
container.AdoptExpand(testing.NewMouse())
container.Adopt(okButton)
okButton.Focus()
})
okButton.OnClick(tomo.Stop)
container.AdoptExpand(label)
container.Adopt(button, okButton)
window.Adopt(container)
okButton.Focus()
window.OnClose(tomo.Stop)
window.Show()
}

79
nasin/application.go Normal file
View File

@ -0,0 +1,79 @@
package nasin
import "image"
import "errors"
import "git.tebibyte.media/sashakoshka/tomo"
// Application represents a Tomo/Nasin application.
type Application interface {
Init () error
}
// Run initializes Tomo and Nasin, and runs the given application. This function
// will block until the application exits or a fatal error occurrs.
func Run (application Application) {
loadPlugins()
backend, err := instantiateBackend()
if err != nil {
println("nasin: cannot start application:", err.Error())
return
}
backend.SetTheme(theme)
tomo.SetBackend(backend)
if application == nil { panic("nasin: nil application") }
err = application.Init()
if err != nil {
println("nasin: backend exited with error:", err.Error())
return
}
err = backend.Run()
if err != nil {
println("nasin: backend exited with error:", err.Error())
return
}
return
}
// Stop stops the event loop
func Stop () {
assertBackend()
tomo.GetBackend().Stop()
}
// Do executes the specified callback within the main thread as soon as
// possible.
func Do (callback func()) {
assertBackend()
tomo.GetBackend().Do(callback)
}
// NewWindow creates a new window within the specified bounding rectangle. The
// position on screen may be overridden by the backend or operating system.
func NewWindow (bounds image.Rectangle) (tomo.MainWindow, error) {
assertBackend()
return tomo.GetBackend().NewWindow(bounds)
}
func assertBackend () {
if tomo.GetBackend() == nil {
panic("nasin: no running tomo backend")
}
}
func instantiateBackend () (backend tomo.Backend, err error) {
// find a suitable backend
for _, factory := range factories {
backend, err = factory()
if err == nil && backend != nil { return }
}
// if none were found, but there was no error produced, produce an error
if err == nil {
return nil, errors.New("no available tomo backends")
}
return
}

44
nasin/doc.go Normal file
View File

@ -0,0 +1,44 @@
// Package nasin provides a higher-level framework for Tomo applications. Nasin
// also automatically handles themes, backend instantiation, and plugin support.
//
// Backends and themes are typically loaded through plugins. For now, plugins
// are only supported on UNIX-like systems, but this will change in the future.
// Nasin will attempt to load all ".so" files in these directories as plugins:
//
// - /usr/lib/nasin/plugins
// - /usr/local/lib/nasin/plugins
// - $HOME/.local/lib/nasin/plugins
//
// It will also attempt to load all ".so" files in the directory specified by
// the NASIN_PLUGIN_PATH environment variable.
//
// Plugins must export the following functions at minimum:
//
// + Expects() tomo.Version
// + Name() string
// + Description() string
//
// Expects() must return the version of Tomo/Nasin it was built for. Nasin will
// automatically figure out if the plugin has a compatible ABI with the current
// version and refuse to load it if not. Name() and Description() return a short
// plugin name and a description of what a plugin does, respectively. Plugins
// must not attempt to interact with Tomo/Nasin within their init functions.
//
// If a plugin provides a backend, it must export this function:
//
// NewBackend() (tomo.Backend, error)
//
// This function must attempt to initialize the backend, and return it if
// successful. Otherwise, it should clean up all resources and return an error
// explaining what caused the backend to fail to initialize. The first backend
// that does not throw an error will be used.
//
// If a plugin provides a theme, it must export this function:
//
// NewTheme() tomo.Theme
//
// This just creates a new theme and returns it.
//
// For information on how to create plugins with Go, visit:
// https://pkg.go.dev/plugin
package nasin

84
nasin/plugin.go Normal file
View File

@ -0,0 +1,84 @@
package nasin
import "os"
// TODO: possibly fork the official plugin module and add support for other
// operating systems? perhaps enhance the Lookup function with
// the generic extract function we have here for extra type safety goodness.
import "plugin"
import "path/filepath"
import "git.tebibyte.media/sashakoshka/tomo"
type backendFactory func () (tomo.Backend, error)
var factories []backendFactory
var theme tomo.Theme
var pluginPaths []string
func loadPlugins () {
for _, dir := range pluginPaths {
if dir != "" {
loadPluginsFrom(dir)
}
}
}
func loadPluginsFrom (dir string) {
entries, err := os.ReadDir(dir)
// its no big deal if one of the dirs doesn't exist
if err != nil { return }
for _, entry := range entries {
if entry.IsDir() { continue }
if filepath.Ext(entry.Name()) != ".so" { continue }
pluginPath := filepath.Join(dir, entry.Name())
loadPlugin(pluginPath)
}
}
func loadPlugin (path string) {
die := func (reason string) {
println("nasin: could not load plugin at", path + ":", reason)
}
plugin, err := plugin.Open(path)
if err != nil {
die(err.Error())
return
}
// check for and obtain basic plugin functions
expects, ok := extract[func () tomo.Version](plugin, "Expects")
if !ok { die("does not implement Expects() tomo.Version"); return }
name, ok := extract[func () string](plugin, "Name")
if !ok { die("does not implement Name() string"); return }
_, ok = extract[func () string](plugin, "Description")
if !ok { die("does not implement Description() string"); return }
// check for version compatibility
pluginVersion := expects()
currentVersion := tomo.CurrentVersion()
if !pluginVersion.CompatibleABI(currentVersion) {
die (
"plugin (" + pluginVersion.String() +
") incompatible with tomo/nasin version (" +
currentVersion.String() + ")")
return
}
// if it's a backend plugin...
newBackend, ok := extract[func () (tomo.Backend, error)](plugin, "NewBackend")
if ok { factories = append(factories, newBackend) }
// if it's a theme plugin...
newTheme, ok := extract[func () tomo.Theme](plugin, "NewTheme")
if ok { theme = newTheme() }
println("nasin: loaded plugin", name())
}
func extract[T any] (plugin *plugin.Plugin, name string) (value T, ok bool) {
symbol, err := plugin.Lookup(name)
if err != nil { return }
value, ok = symbol.(T)
return
}

22
nasin/unix.go Normal file
View File

@ -0,0 +1,22 @@
//go:build linux || darwin || freebsd
package nasin
import "os"
import "strings"
import "path/filepath"
func init () {
pathVariable := os.Getenv("NASIN_PLUGIN_PATH")
pluginPaths = strings.Split(pathVariable, ":")
pluginPaths = append (
pluginPaths,
"/usr/lib/nasin/plugins",
"/usr/local/lib/nasin/plugins")
homeDir, err := os.UserHomeDir()
if err == nil {
pluginPaths = append (
pluginPaths,
filepath.Join(homeDir, ".local/lib/nasin/plugins"))
}
}

View File

@ -0,0 +1,21 @@
// Plugin wintergreen provides a calm, bluish green theme.
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/plugins/wintergreen/wintergreen"
func Expects () tomo.Version {
return tomo.Version { 0, 0, 0 }
}
func Name () string {
return "Wintergreen"
}
func Description () string {
return "A calm, bluish green theme."
}
func NewTheme () (tomo.Theme) {
return wintergreen.Theme { }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,2 @@
// Package wintergreen contains the wintergreen theme.
package wintergreen

View File

@ -0,0 +1,314 @@
package wintergreen
import "image"
import "bytes"
import _ "embed"
import _ "image/png"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
//go:embed assets/wintergreen.png
var defaultAtlasBytes []byte
var defaultAtlas artist.Canvas
var defaultTextures [17][9]artist.Pattern
//go:embed assets/wintergreen-icons-small.png
var defaultIconsSmallAtlasBytes []byte
var defaultIconsSmall [640]binaryIcon
//go:embed assets/wintergreen-icons-large.png
var defaultIconsLargeAtlasBytes []byte
var defaultIconsLarge [640]binaryIcon
func atlasCell (col, row int, border artist.Inset) {
bounds := image.Rect(0, 0, 16, 16).Add(image.Pt(col, row).Mul(16))
defaultTextures[col][row] = patterns.Border {
Canvas: artist.Cut(defaultAtlas, bounds),
Inset: border,
}
}
func atlasCol (col int, border artist.Inset) {
for index, _ := range defaultTextures[col] {
atlasCell(col, index, border)
}
}
type binaryIcon struct {
data []bool
stride int
}
func (icon binaryIcon) Draw (destination artist.Canvas, color color.RGBA, at image.Point) {
bounds := icon.Bounds().Add(at).Intersect(destination.Bounds())
point := image.Point { }
data, stride := destination.Buffer()
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
srcPoint := point.Sub(at)
srcIndex := srcPoint.X + srcPoint.Y * icon.stride
dstIndex := point.X + point.Y * stride
if icon.data[srcIndex] {
data[dstIndex] = color
}
}}
}
func (icon binaryIcon) Bounds () image.Rectangle {
return image.Rect(0, 0, icon.stride, len(icon.data) / icon.stride)
}
func binaryIconFrom (source image.Image, clip image.Rectangle) (icon binaryIcon) {
bounds := source.Bounds().Intersect(clip)
if bounds.Empty() { return }
icon.stride = bounds.Dx()
icon.data = make([]bool, bounds.Dx() * bounds.Dy())
point := image.Point { }
dstIndex := 0
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
r, g, b, a := source.At(point.X, point.Y).RGBA()
if a > 0x8000 && (r + g + b) / 3 < 0x8000 {
icon.data[dstIndex] = true
}
dstIndex ++
}}
return
}
func init () {
defaultAtlasImage, _, _ := image.Decode(bytes.NewReader(defaultAtlasBytes))
defaultAtlas = artist.FromImage(defaultAtlasImage)
// PatternDead
atlasCol(0, artist.Inset { })
// PatternRaised
atlasCol(1, artist.Inset { 6, 6, 6, 6 })
// PatternSunken
atlasCol(2, artist.Inset { 4, 4, 4, 4 })
// PatternPinboard
atlasCol(3, artist.Inset { 2, 2, 2, 2 })
// PatternButton
atlasCol(4, artist.Inset { 6, 6, 6, 6 })
// PatternInput
atlasCol(5, artist.Inset { 4, 4, 4, 4 })
// PatternGutter
atlasCol(6, artist.Inset { 7, 7, 7, 7 })
// PatternHandle
atlasCol(7, artist.Inset { 3, 3, 3, 3 })
// PatternLine
atlasCol(8, artist.Inset { 1, 1, 1, 1 })
// PatternMercury
atlasCol(13, artist.Inset { 2, 2, 2, 2 })
// PatternTableHead:
atlasCol(14, artist.Inset { 4, 4, 4, 4 })
// PatternTableCell:
atlasCol(15, artist.Inset { 4, 4, 4, 4 })
// PatternLamp:
atlasCol(16, artist.Inset { 4, 3, 4, 3 })
// PatternButton: basic.checkbox
atlasCol(9, artist.Inset { 3, 3, 3, 3 })
// PatternRaised: basic.listEntry
atlasCol(10, artist.Inset { 3, 3, 3, 3 })
// PatternRaised: fun.flatKey
atlasCol(11, artist.Inset { 3, 3, 5, 3 })
// PatternRaised: fun.sharpKey
atlasCol(12, artist.Inset { 3, 3, 4, 3 })
// set up small icons
defaultIconsSmallAtlasImage, _, _ := image.Decode (
bytes.NewReader(defaultIconsSmallAtlasBytes))
point := image.Point { }
iconIndex := 0
for point.Y = 0; point.Y < 20; point.Y ++ {
for point.X = 0; point.X < 32; point.X ++ {
defaultIconsSmall[iconIndex] = binaryIconFrom (
defaultIconsSmallAtlasImage,
image.Rect(0, 0, 16, 16).Add(point.Mul(16)))
iconIndex ++
}}
// set up large icons
defaultIconsLargeAtlasImage, _, _ := image.Decode (
bytes.NewReader(defaultIconsLargeAtlasBytes))
point = image.Point { }
iconIndex = 0
for point.Y = 0; point.Y < 8; point.Y ++ {
for point.X = 0; point.X < 32; point.X ++ {
defaultIconsLarge[iconIndex] = binaryIconFrom (
defaultIconsLargeAtlasImage,
image.Rect(0, 0, 32, 32).Add(point.Mul(32)))
iconIndex ++
}}
iconIndex = 384
for point.Y = 8; point.Y < 12; point.Y ++ {
for point.X = 0; point.X < 32; point.X ++ {
defaultIconsLarge[iconIndex] = binaryIconFrom (
defaultIconsLargeAtlasImage,
image.Rect(0, 0, 32, 32).Add(point.Mul(32)))
iconIndex ++
}}
}
type Theme struct { }
func (Theme) FontFace (style tomo.FontStyle, size tomo.FontSize, c tomo.Case) font.Face {
switch style {
case tomo.FontStyleBold:
return defaultfont.FaceBold
case tomo.FontStyleItalic:
return defaultfont.FaceItalic
case tomo.FontStyleBoldItalic:
return defaultfont.FaceBoldItalic
default:
return defaultfont.FaceRegular
}
}
func (Theme) Icon (id tomo.Icon, size tomo.IconSize, c tomo.Case) artist.Icon {
if size == tomo.IconSizeLarge {
if id < 0 || int(id) >= len(defaultIconsLarge) {
return nil
} else {
return defaultIconsLarge[id]
}
} else {
if id < 0 || int(id) >= len(defaultIconsSmall) {
return nil
} else {
return defaultIconsSmall[id]
}
}
}
func (Theme) MimeIcon (data.Mime, tomo.IconSize, tomo.Case) artist.Icon {
// TODO
return nil
}
func (Theme) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.Pattern {
offset := 0; switch {
case state.Disabled: offset = 1
case state.Pressed && state.On: offset = 4
case state.Focused && state.On: offset = 6
case state.On: offset = 2
case state.Pressed: offset = 3
case state.Focused: offset = 5
}
switch id {
case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF)
case tomo.PatternDead: return defaultTextures[0][offset]
case tomo.PatternRaised: return defaultTextures[1][offset]
case tomo.PatternSunken: return defaultTextures[2][offset]
case tomo.PatternPinboard: return defaultTextures[3][offset]
case tomo.PatternButton:
switch {
case c.Match("tomo", "checkbox", ""):
return defaultTextures[9][offset]
case c.Match("tomo", "piano", "flatKey"):
return defaultTextures[11][offset]
case c.Match("tomo", "piano", "sharpKey"):
return defaultTextures[12][offset]
default:
return defaultTextures[4][offset]
}
case tomo.PatternInput: return defaultTextures[5][offset]
case tomo.PatternGutter: return defaultTextures[6][offset]
case tomo.PatternHandle: return defaultTextures[7][offset]
case tomo.PatternLine: return defaultTextures[8][offset]
case tomo.PatternMercury: return defaultTextures[13][offset]
case tomo.PatternTableHead: return defaultTextures[14][offset]
case tomo.PatternTableCell: return defaultTextures[15][offset]
case tomo.PatternLamp: return defaultTextures[16][offset]
default: return patterns.Uhex(0xFF00FFFF)
}
}
func (Theme) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA {
if state.Disabled { return artutil.Hex(0x444444FF) }
return artutil.Hex (map[tomo.Color] uint32 {
tomo.ColorBlack: 0x272d24FF,
tomo.ColorRed: 0x8c4230FF,
tomo.ColorGreen: 0x69905fFF,
tomo.ColorYellow: 0x9a973dFF,
tomo.ColorBlue: 0x3d808fFF,
tomo.ColorPurple: 0x8c608bFF,
tomo.ColorCyan: 0x3d8f84FF,
tomo.ColorWhite: 0xaea894FF,
tomo.ColorBrightBlack: 0x4f5142FF,
tomo.ColorBrightRed: 0xbd6f59FF,
tomo.ColorBrightGreen: 0x8dad84FF,
tomo.ColorBrightYellow: 0xe2c558FF,
tomo.ColorBrightBlue: 0x77b1beFF,
tomo.ColorBrightPurple: 0xc991c8FF,
tomo.ColorBrightCyan: 0x74c7b7FF,
tomo.ColorBrightWhite: 0xcfd7d2FF,
tomo.ColorForeground: 0x000000FF,
tomo.ColorMidground: 0x97A09BFF,
tomo.ColorBackground: 0xAAAAAAFF,
tomo.ColorShadow: 0x445754FF,
tomo.ColorShine: 0xCFD7D2FF,
tomo.ColorAccent: 0x408090FF,
} [id])
}
func (Theme) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
switch id {
case tomo.PatternSunken:
if c.Match("tomo", "progressBar", "") {
return artist.I(2, 1, 1, 2)
} else if c.Match("tomo", "list", "") {
return artist.I(2)
} else if c.Match("tomo", "flowList", "") {
return artist.I(2)
} else {
return artist.I(8)
}
case tomo.PatternPinboard:
if c.Match("tomo", "piano", "") {
return artist.I(2)
} else {
return artist.I(8)
}
case tomo.PatternTableCell: return artist.I(5)
case tomo.PatternTableHead: return artist.I(5)
case tomo.PatternGutter: return artist.I(0)
case tomo.PatternLine: return artist.I(1)
case tomo.PatternMercury: return artist.I(5)
case tomo.PatternLamp: return artist.I(5, 5, 5, 6)
default: return artist.I(8)
}
}
func (Theme) Margin (id tomo.Pattern, c tomo.Case) image.Point {
switch id {
case tomo.PatternSunken:
if c.Match("tomo", "list", "") {
return image.Pt(-1, -1)
} else if c.Match("tomo", "flowList", "") {
return image.Pt(-1, -1)
} else {
return image.Pt(8, 8)
}
default: return image.Pt(8, 8)
}
}
func (Theme) Hints (pattern tomo.Pattern, c tomo.Case) (hints tomo.Hints) {
return
}
func (Theme) Sink (pattern tomo.Pattern, c tomo.Case) image.Point {
return image.Point { 1, 1 }
}

21
plugins/x/main.go Normal file
View File

@ -0,0 +1,21 @@
// Plugin x provides the X11 backend as a plugin.
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/plugins/x/x"
func Expects () tomo.Version {
return tomo.Version { 0, 0, 0 }
}
func Name () string {
return "X"
}
func Description () string {
return "Provides an X11 backend."
}
func NewBackend () (tomo.Backend, error) {
return x.NewBackend()
}

View File

@ -112,7 +112,7 @@ var keypadCodeTable = map[xproto.Keysym] input.Key {
// initializeKeymapInformation grabs keyboard mapping information from the X
// server.
func (backend *Backend) initializeKeymapInformation () {
func (backend *backend) initializeKeymapInformation () {
keybind.Initialize(backend.connection)
backend.modifierMasks.capsLock = backend.keysymToMask(0xFFE5)
backend.modifierMasks.shiftLock = backend.keysymToMask(0xFFE6)
@ -127,7 +127,7 @@ func (backend *Backend) initializeKeymapInformation () {
// keysymToKeycode converts an X keysym to an X keycode, instead of the other
// way around.
func (backend *Backend) keysymToKeycode (
func (backend *backend) keysymToKeycode (
symbol xproto.Keysym,
) (
code xproto.Keycode,
@ -148,7 +148,7 @@ func (backend *Backend) keysymToKeycode (
}
// keysymToMask returns the X modmask for a given modifier key.
func (backend *Backend) keysymToMask (
func (backend *backend) keysymToMask (
symbol xproto.Keysym,
) (
mask uint16,
@ -164,7 +164,7 @@ func (backend *Backend) keysymToMask (
// fleshed out version of some of the logic found in xgbutil/keybind/encoding.go
// to get a full keycode to keysym conversion, but eliminates redundant work by
// going straight to a tomo keycode.
func (backend *Backend) keycodeToKey (
func (backend *backend) keycodeToKey (
keycode xproto.Keycode,
state uint16,
) (

View File

@ -2,9 +2,11 @@ package x
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/ability"
type entity struct {
backend *backend
window *window
parent *entity
children []*entity
@ -17,15 +19,11 @@ type entity struct {
selected bool
layoutInvalid bool
isContainer bool
}
func (backend *Backend) NewEntity (owner tomo.Element) tomo.Entity {
entity := &entity { element: owner }
if _, ok := owner.(tomo.Container); ok {
entity.isContainer = true
entity.InvalidateLayout()
}
func (backend *backend) NewEntity (owner tomo.Element) tomo.Entity {
entity := &entity { element: owner, backend: backend }
entity.InvalidateLayout()
return entity
}
@ -44,7 +42,7 @@ func (ent *entity) unlink () {
ent.parent = nil
ent.window = nil
if element, ok := ent.element.(tomo.Selectable); ok {
if element, ok := ent.element.(ability.Selectable); ok {
ent.selected = false
element.HandleSelectionChange()
}
@ -111,15 +109,15 @@ func (entity *entity) scrollTargetChildAt (point image.Point) *entity {
}
}
if _, ok := entity.element.(tomo.ScrollTarget); ok {
if _, ok := entity.element.(ability.ScrollTarget); ok {
return entity
}
return nil
}
func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer, tomo.Element)) {
func (entity *entity) forMouseTargetContainers (callback func (ability.MouseTargetContainer, tomo.Element)) {
if entity.parent == nil { return }
if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok {
if parent, ok := entity.parent.element.(ability.MouseTargetContainer); ok {
callback(parent, entity.element)
}
entity.parent.forMouseTargetContainers(callback)
@ -156,18 +154,19 @@ func (entity *entity) SetMinimumSize (width, height int) {
entity.window.setMinimumSize(width, height)
}
} else {
entity.parent.element.(tomo.Container).
entity.parent.element.(ability.Container).
HandleChildMinimumSizeChange(entity.element)
}
}
func (entity *entity) DrawBackground (destination canvas.Canvas) {
func (entity *entity) DrawBackground (destination artist.Canvas) {
if entity.parent != nil {
entity.parent.element.(tomo.Container).DrawBackground(destination)
entity.parent.element.(ability.Container).DrawBackground(destination)
} else if entity.window != nil {
entity.window.system.theme.Pattern (
entity.backend.theme.Pattern (
tomo.PatternBackground,
tomo.State { }).Draw (
tomo.State { },
tomo.C("tomo", "window")).Draw (
destination,
entity.window.canvas.Bounds())
}
@ -177,7 +176,7 @@ func (entity *entity) DrawBackground (destination canvas.Canvas) {
func (entity *entity) InvalidateLayout () {
if entity.window == nil { return }
if !entity.isContainer { return }
if _, ok := entity.element.(ability.Layoutable); !ok { return }
entity.layoutInvalid = true
entity.window.system.anyLayoutInvalid = true
}
@ -233,7 +232,7 @@ func (entity *entity) PlaceChild (index int, bounds image.Rectangle) {
func (entity *entity) SelectChild (index int, selected bool) {
child := entity.children[index]
if element, ok := child.element.(tomo.Selectable); ok {
if element, ok := child.element.(ability.Selectable); ok {
if child.selected == selected { return }
child.selected = selected
element.HandleSelectionChange()
@ -275,9 +274,9 @@ func (entity *entity) Selected () bool {
func (entity *entity) NotifyFlexibleHeightChange () {
if entity.parent == nil { return }
if parent, ok := entity.parent.element.(tomo.FlexibleContainer); ok {
if parent, ok := entity.parent.element.(ability.FlexibleContainer); ok {
parent.HandleChildFlexibleHeightChange (
entity.element.(tomo.Flexible))
entity.element.(ability.Flexible))
}
}
@ -285,8 +284,20 @@ func (entity *entity) NotifyFlexibleHeightChange () {
func (entity *entity) NotifyScrollBoundsChange () {
if entity.parent == nil { return }
if parent, ok := entity.parent.element.(tomo.ScrollableContainer); ok {
if parent, ok := entity.parent.element.(ability.ScrollableContainer); ok {
parent.HandleChildScrollBoundsChange (
entity.element.(tomo.Scrollable))
entity.element.(ability.Scrollable))
}
}
// ----------- ThemeableEntity ----------- //
func (entity *entity) Theme () tomo.Theme {
return entity.backend.theme
}
// ----------- ConfigurableEntity ----------- //
func (entity *entity) Config () tomo.Config {
return entity.backend.config
}

View File

@ -3,6 +3,7 @@ package x
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/ability"
import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto"
@ -134,7 +135,7 @@ func (window *window) handleKeyPress (
} else if key == input.KeyEscape && window.shy {
window.Close()
} else if window.focused != nil {
focused, ok := window.focused.element.(tomo.KeyboardTarget)
focused, ok := window.focused.element.(ability.KeyboardTarget)
if ok { focused.HandleKeyDown(key, modifiers) }
}
}
@ -169,7 +170,7 @@ func (window *window) handleKeyRelease (
modifiers.NumberPad = numberPad
if window.focused != nil {
focused, ok := window.focused.element.(tomo.KeyboardTarget)
focused, ok := window.focused.element.(ability.KeyboardTarget)
if ok { focused.HandleKeyUp(key, modifiers) }
}
}
@ -191,7 +192,7 @@ func (window *window) handleButtonPress (
} else if scrolling {
underneath := window.system.scrollTargetChildAt(point)
if underneath != nil {
if child, ok := underneath.element.(tomo.ScrollTarget); ok {
if child, ok := underneath.element.(ability.ScrollTarget); ok {
sum := scrollSum { }
sum.add(buttonEvent.Detail, window, buttonEvent.State)
window.compressScrollSum(buttonEvent, &sum)
@ -203,12 +204,12 @@ func (window *window) handleButtonPress (
} else {
underneath := window.system.childAt(point)
window.system.drags[buttonEvent.Detail] = underneath
if child, ok := underneath.element.(tomo.MouseTarget); ok {
if child, ok := underneath.element.(ability.MouseTarget); ok {
child.HandleMouseDown (
point, input.Button(buttonEvent.Detail),
modifiers)
}
callback := func (container tomo.MouseTargetContainer, child tomo.Element) {
callback := func (container ability.MouseTargetContainer, child tomo.Element) {
container.HandleChildMouseDown (
point, input.Button(buttonEvent.Detail),
modifiers, child)
@ -229,7 +230,7 @@ func (window *window) handleButtonRelease (
dragging := window.system.drags[buttonEvent.Detail]
if dragging != nil {
if child, ok := dragging.element.(tomo.MouseTarget); ok {
if child, ok := dragging.element.(ability.MouseTarget); ok {
child.HandleMouseUp (
image.Pt (
int(buttonEvent.EventX),
@ -237,7 +238,7 @@ func (window *window) handleButtonRelease (
input.Button(buttonEvent.Detail),
modifiers)
}
callback := func (container tomo.MouseTargetContainer, child tomo.Element) {
callback := func (container ability.MouseTargetContainer, child tomo.Element) {
container.HandleChildMouseUp (
image.Pt (
int(buttonEvent.EventX),
@ -262,7 +263,7 @@ func (window *window) handleMotionNotify (
handled := false
for _, child := range window.system.drags {
if child == nil { continue }
if child, ok := child.element.(tomo.MotionTarget); ok {
if child, ok := child.element.(ability.MotionTarget); ok {
child.HandleMotion(image.Pt(x, y))
handled = true
}
@ -270,7 +271,7 @@ func (window *window) handleMotionNotify (
if !handled {
child := window.system.childAt(image.Pt(x, y))
if child, ok := child.element.(tomo.MotionTarget); ok {
if child, ok := child.element.(ability.MotionTarget); ok {
child.HandleMotion(image.Pt(x, y))
}
}

View File

@ -1,10 +1,8 @@
package x
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/ability"
type entitySet map[*entity] struct { }
@ -24,11 +22,8 @@ func (set entitySet) Add (entity *entity) {
type system struct {
child *entity
focused *entity
canvas canvas.BasicCanvas
theme theme.Wrapped
config config.Wrapped
canvas artist.BasicCanvas
invalidateIgnore bool
drawingInvalid entitySet
anyLayoutInvalid bool
@ -42,21 +37,19 @@ func (system *system) initialize () {
system.drawingInvalid = make(entitySet)
}
func (system *system) SetTheme (theme tomo.Theme) {
system.theme.Theme = theme
func (system *system) handleThemeChange () {
system.propagate (func (entity *entity) bool {
if child, ok := system.child.element.(tomo.Themeable); ok {
child.SetTheme(theme)
if child, ok := system.child.element.(ability.Themeable); ok {
child.HandleThemeChange()
}
return true
})
}
func (system *system) SetConfig (config tomo.Config) {
system.config.Config = config
func (system *system) handleConfigChange () {
system.propagate (func (entity *entity) bool {
if child, ok := system.child.element.(tomo.Configurable); ok {
child.SetConfig(config)
if child, ok := system.child.element.(ability.Configurable); ok {
child.HandleConfigChange()
}
return true
})
@ -66,10 +59,10 @@ func (system *system) focus (entity *entity) {
previous := system.focused
system.focused = entity
if previous != nil {
previous.element.(tomo.Focusable).HandleFocusChange()
previous.element.(ability.Focusable).HandleFocusChange()
}
if entity != nil {
entity.element.(tomo.Focusable).HandleFocusChange()
entity.element.(ability.Focusable).HandleFocusChange()
}
}
@ -79,7 +72,7 @@ func (system *system) focusNext () {
system.propagateAlt (func (entity *entity) bool {
if found {
// looking for the next element to select
child, ok := entity.element.(tomo.Focusable)
child, ok := entity.element.(ability.Focusable)
if ok && child.Enabled() {
// found it
entity.Focus()
@ -106,7 +99,7 @@ func (system *system) focusPrevious () {
return false
}
child, ok := entity.element.(tomo.Focusable)
child, ok := entity.element.(ability.Focusable)
if ok && child.Enabled() { behind = entity }
return true
})
@ -137,9 +130,7 @@ func (system *system) resizeChildToFit () {
system.child.bounds = system.canvas.Bounds()
system.child.clippedBounds = system.child.bounds
system.child.Invalidate()
if system.child.isContainer {
system.child.InvalidateLayout()
}
system.child.InvalidateLayout()
}
func (system *system) afterEvent () {
@ -153,7 +144,7 @@ func (system *system) afterEvent () {
func (system *system) layout (entity *entity, force bool) {
if entity == nil { return }
if entity.layoutInvalid == true || force {
if element, ok := entity.element.(tomo.Layoutable); ok {
if element, ok := entity.element.(ability.Layoutable); ok {
element.Layout()
entity.layoutInvalid = false
force = true
@ -176,7 +167,7 @@ func (system *system) draw () {
for entity := range system.drawingInvalid {
if entity.clippedBounds.Empty() { continue }
entity.element.Draw (canvas.Cut (
entity.element.Draw (artist.Cut (
system.canvas,
entity.clippedBounds))
finalBounds = finalBounds.Union(entity.clippedBounds)

View File

@ -13,14 +13,14 @@ import "github.com/jezek/xgbutil/mousebind"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
type mainWindow struct { *window }
type menuWindow struct { *window }
type window struct {
system
backend *Backend
backend *backend
xWindow *xwindow.Window
xCanvas *xgraphics.Image
@ -40,7 +40,7 @@ type window struct {
onClose func ()
}
func (backend *Backend) NewWindow (
func (backend *backend) NewWindow (
bounds image.Rectangle,
) (
output tomo.MainWindow,
@ -53,7 +53,7 @@ func (backend *Backend) NewWindow (
return output, err
}
func (backend *Backend) newWindow (
func (backend *backend) newWindow (
bounds image.Rectangle,
override bool,
) (
@ -67,7 +67,6 @@ func (backend *Backend) newWindow (
window.system.initialize()
window.system.pushFunc = window.pasteAndPush
window.theme.Case = tomo.C("tomo", "window")
window.xWindow, err = xwindow.Generate(backend.connection)
if err != nil { return }
@ -121,9 +120,6 @@ func (backend *Backend) newWindow (
Connect(backend.connection, window.xWindow.Id)
xevent.SelectionRequestFun(window.handleSelectionRequest).
Connect(backend.connection, window.xWindow.Id)
window.SetTheme(backend.theme)
window.SetConfig(backend.config)
window.metrics.bounds = bounds
window.setMinimumSize(8, 8)
@ -418,7 +414,7 @@ func (window *window) pasteAndPush (region image.Rectangle) {
}
func (window *window) paste (region image.Rectangle) {
canvas := canvas.Cut(window.canvas, region)
canvas := artist.Cut(window.canvas, region)
data, stride := canvas.Buffer()
bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds())

Some files were not shown because too many files have changed in this diff Show More