reorganize #17

Merged
sashakoshka merged 53 commits from reorganize into main 2023-05-03 13:42:22 -06:00
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. and its API may change without notice.
Tomo is a GUI toolkit written in pure Go with minimal external dependencies. It 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 makes use of Go's unique language features to do more with less.
easily extendable with custom backends and elements.
You can find out more about how to use it by visiting the examples directory, Nasin is an application framework that runs on top of Tomo. It supports plugins
or pull up its documentation by running `godoc` within the repository. You can which can extend any application with backends, themes, etc.
also view it on the web on
[pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/sashakoshka/tomo) (although ## 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). 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"
import "image/draw" 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"
import "image/color" import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
type Icon interface { type Icon interface {
// Draw draws the icon to the destination canvas at the specified point, // Draw draws the icon to the destination canvas at the specified point,
// using the specified color (if the icon is monochrome). // 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 returns the bounds of the icon.
Bounds () image.Rectangle Bounds () image.Rectangle

View File

@ -1,8 +1,6 @@
package artist package artist
import "image" 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 // Pattern is capable of drawing to a canvas within the bounds of a given
// clipping rectangle. // clipping rectangle.
@ -11,51 +9,5 @@ type Pattern interface {
// specified bounds. The given bounds can be smaller or larger than the // specified bounds. The given bounds can be smaller or larger than the
// bounds of the destination canvas. The destination canvas can be cut // bounds of the destination canvas. The destination canvas can be cut
// using canvas.Cut() to draw only a specific subset of a pattern. // 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 package patterns
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
// Border is a pattern that behaves similarly to border-image in CSS. It divides // 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 // This pattern can be used to make a static image texture into something that
// responds well to being resized. // responds well to being resized.
type Border struct { type Border struct {
canvas.Canvas artist.Canvas
artist.Inset artist.Inset
} }
// Draw draws the border pattern onto the destination canvas within the given // Draw draws the border pattern onto the destination canvas within the given
// bounds. // 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()) drawBounds := bounds.Canon().Intersect(destination.Bounds())
if drawBounds.Empty() { return } if drawBounds.Empty() { return }
srcSections := nonasect(pattern.Bounds(), pattern.Inset) srcSections := nonasect(pattern.Bounds(), pattern.Inset)
srcTextures := [9]Texture { } srcTextures := [9]Texture { }
for index, section := range srcSections { for index, section := range srcSections {
srcTextures[index].Canvas = canvas.Cut(pattern, section) srcTextures[index].Canvas = artist.Cut(pattern, section)
} }
dstSections := nonasect(bounds, pattern.Inset) dstSections := nonasect(bounds, pattern.Inset)

View File

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

View File

@ -2,19 +2,19 @@ package patterns
import "image" import "image"
import "image/color" import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" 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. // Uniform is a pattern that draws a solid color.
type Uniform color.RGBA type Uniform color.RGBA
// Draw fills the bounding rectangle with the pattern's color. // 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) shapes.FillColorRectangle(destination, color.RGBA(pattern), bounds)
} }
// Uhex creates a new Uniform pattern from an RGBA integer value. // Uhex creates a new Uniform pattern from an RGBA integer value.
func Uhex (color uint32) (uniform Uniform) { 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 "math"
import "image" import "image"
import "image/color" 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 // 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 // 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. // of both canvases.
func FillEllipse ( func FillEllipse (
destination canvas.Canvas, destination artist.Canvas,
source canvas.Canvas, source artist.Canvas,
bounds image.Rectangle, bounds image.Rectangle,
) ( ) (
updatedRegion image.Rectangle, updatedRegion image.Rectangle,
@ -42,8 +42,8 @@ func FillEllipse (
} }
func StrokeEllipse ( func StrokeEllipse (
destination canvas.Canvas, destination artist.Canvas,
source canvas.Canvas, source artist.Canvas,
bounds image.Rectangle, bounds image.Rectangle,
weight int, weight int,
) { ) {
@ -170,7 +170,7 @@ func (context ellipsePlottingContext) plotEllipse () {
// FillColorEllipse fills an ellipse within the destination canvas with a solid // FillColorEllipse fills an ellipse within the destination canvas with a solid
// color. // color.
func FillColorEllipse ( func FillColorEllipse (
destination canvas.Canvas, destination artist.Canvas,
color color.RGBA, color color.RGBA,
bounds image.Rectangle, bounds image.Rectangle,
) ( ) (
@ -196,7 +196,7 @@ func FillColorEllipse (
// StrokeColorEllipse is similar to FillColorEllipse, but it draws an inset // StrokeColorEllipse is similar to FillColorEllipse, but it draws an inset
// outline of an ellipse instead. // outline of an ellipse instead.
func StrokeColorEllipse ( func StrokeColorEllipse (
destination canvas.Canvas, destination artist.Canvas,
color color.RGBA, color color.RGBA,
bounds image.Rectangle, bounds image.Rectangle,
weight int, weight int,

View File

@ -2,12 +2,12 @@ package shapes
import "image" import "image"
import "image/color" 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 // ColorLine draws a line from one point to another with the specified weight
// and color. // and color.
func ColorLine ( func ColorLine (
destination canvas.Canvas, destination artist.Canvas,
color color.RGBA, color color.RGBA,
weight int, weight int,
min image.Point, min image.Point,

View File

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

View File

@ -1,7 +1,6 @@
package tomo package tomo
import "image" import "image"
import "errors"
// Backend represents a connection to a display server, or something similar. // Backend represents a connection to a display server, or something similar.
// It is capable of managing an event loop, and creating windows. // It is capable of managing an event loop, and creating windows.
@ -32,33 +31,21 @@ type Backend interface {
SetConfig (Config) SetConfig (Config)
} }
// BackendFactory represents a function capable of constructing a backend var backend 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)
// RegisterBackend registers a backend factory. When an application calls // GetBackend returns the currently running backend.
// tomo.Run(), the first registered backend that does not throw an error will be func GetBackend () Backend {
// used. return backend
func RegisterBackend (factory BackendFactory) {
factories = append(factories, factory)
} }
var factories []BackendFactory // 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 instantiateBackend () (backend Backend, err error) { func SetBackend (b Backend) {
// find a suitable backend if backend != nil { return }
for _, factory := range factories { backend = b
backend, err = factory() }
if err == nil && backend != nil { return }
} // Bounds creates a rectangle from an x, y, width, and height.
func Bounds (x, y, width, height int) image.Rectangle {
// if none were found, but there was no error produced, produce an return image.Rect(x, y, x + width, y + height)
// error
if err == nil {
err = errors.New("no available backends")
}
return
} }

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"
import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/artist" 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 defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
//go:embed assets/wintergreen.png //go:embed assets/default.png
var defaultAtlasBytes []byte var defaultAtlasBytes []byte
var defaultAtlas canvas.Canvas var defaultAtlas artist.Canvas
var defaultTextures [17][9]artist.Pattern var defaultTextures [7][7]artist.Pattern
//go:embed assets/wintergreen-icons-small.png //go:embed assets/wintergreen-icons-small.png
var defaultIconsSmallAtlasBytes []byte var defaultIconsSmallAtlasBytes []byte
var defaultIconsSmall [640]binaryIcon var defaultIconsSmall [640]binaryIcon
@ -25,9 +25,9 @@ var defaultIconsLargeAtlasBytes []byte
var defaultIconsLarge [640]binaryIcon var defaultIconsLarge [640]binaryIcon
func atlasCell (col, row int, border artist.Inset) { 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 { defaultTextures[col][row] = patterns.Border {
Canvas: canvas.Cut(defaultAtlas, bounds), Canvas: artist.Cut(defaultAtlas, bounds),
Inset: border, Inset: border,
} }
} }
@ -43,7 +43,7 @@ type binaryIcon struct {
stride int 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()) bounds := icon.Bounds().Add(at).Intersect(destination.Bounds())
point := image.Point { } point := image.Point { }
data, stride := destination.Buffer() data, stride := destination.Buffer()
@ -85,43 +85,15 @@ func binaryIconFrom (source image.Image, clip image.Rectangle) (icon binaryIcon)
func init () { func init () {
defaultAtlasImage, _, _ := image.Decode(bytes.NewReader(defaultAtlasBytes)) defaultAtlasImage, _, _ := image.Decode(bytes.NewReader(defaultAtlasBytes))
defaultAtlas = canvas.FromImage(defaultAtlasImage) defaultAtlas = artist.FromImage(defaultAtlasImage)
// PatternDead atlasCol(0, artist.I(0))
atlasCol(0, artist.Inset { }) atlasCol(1, artist.I(3))
// PatternRaised atlasCol(2, artist.I(1))
atlasCol(1, artist.Inset { 6, 6, 6, 6 }) atlasCol(3, artist.I(1))
// PatternSunken atlasCol(4, artist.I(1))
atlasCol(2, artist.Inset { 4, 4, 4, 4 }) atlasCol(5, artist.I(3))
// PatternPinboard atlasCol(6, artist.I(1))
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 // set up small icons
defaultIconsSmallAtlasImage, _, _ := image.Decode ( 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.PatternRaised: return defaultTextures[1][offset]
case tomo.PatternSunken: return defaultTextures[2][offset] case tomo.PatternSunken: return defaultTextures[2][offset]
case tomo.PatternPinboard: return defaultTextures[3][offset] case tomo.PatternPinboard: return defaultTextures[3][offset]
case tomo.PatternButton: case tomo.PatternButton: return defaultTextures[1][offset]
switch { case tomo.PatternInput: return defaultTextures[2][offset]
case c.Match("tomo", "checkbox", ""): case tomo.PatternGutter: return defaultTextures[2][offset]
return defaultTextures[9][offset] case tomo.PatternHandle: return defaultTextures[3][offset]
case c.Match("tomo", "piano", "flatKey"): case tomo.PatternLine: return defaultTextures[0][offset]
return defaultTextures[11][offset] case tomo.PatternMercury: return defaultTextures[4][offset]
case c.Match("tomo", "piano", "sharpKey"): case tomo.PatternTableHead: return defaultTextures[5][offset]
return defaultTextures[12][offset] case tomo.PatternTableCell: return defaultTextures[5][offset]
default: case tomo.PatternLamp: return defaultTextures[6][offset]
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) default: return patterns.Uhex(0xFF00FFFF)
} }
} }
func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA { 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.ColorBlack: 0x272d24FF,
tomo.ColorRed: 0x8c4230FF, tomo.ColorRed: 0x8c4230FF,
tomo.ColorGreen: 0x69905fFF, 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.ColorBrightWhite: 0xcfd7d2FF,
tomo.ColorForeground: 0x000000FF, tomo.ColorForeground: 0x000000FF,
tomo.ColorMidground: 0x97A09BFF, tomo.ColorMidground: 0x656565FF,
tomo.ColorBackground: 0xAAAAAAFF, tomo.ColorBackground: 0xAAAAAAFF,
tomo.ColorShadow: 0x445754FF, tomo.ColorShadow: 0x000000FF,
tomo.ColorShine: 0xCFD7D2FF, tomo.ColorShine: 0xFFFFFFFF,
tomo.ColorAccent: 0x408090FF, tomo.ColorAccent: 0xff3300FF,
} [id]) } [id])
} }
// Padding returns the default padding value for the given pattern. // Padding returns the default padding value for the given pattern.
func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset { func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
switch id { 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.PatternGutter: return artist.I(0)
case tomo.PatternLine: return artist.I(1) case tomo.PatternLine: return artist.I(1)
case tomo.PatternMercury: return artist.I(5) default: return artist.I(6)
case tomo.PatternLamp: return artist.I(5, 5, 5, 6)
default: return artist.I(8)
} }
} }
// Margin returns the default margin value for the given pattern. // Margin returns the default margin value for the given pattern.
func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point { func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point {
switch id { return image.Pt(6, 6)
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)
}
} }
// Hints returns rendering optimization hints for a particular pattern. // 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 package tomo
import "image" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// 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 { type Element interface {
// Draw causes the element to draw to the specified canvas. The bounds // 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 // of this canvas specify the area that is actually drawn to, while the
// Entity bounds specify the actual area of the element. // Entity bounds specify the actual area of the element.
Draw (canvas.Canvas) Draw (artist.Canvas)
// Entity returns this element's entity. // Entity returns this element's entity.
Entity () 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 "image"
import "git.tebibyte.media/sashakoshka/tomo" 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/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 // Space is a list of spacing configurations that can be passed to some
// containers. // containers.
@ -27,7 +28,6 @@ func (space Space) Includes (sub Space) bool {
// complex layouts. // complex layouts.
type Box struct { type Box struct {
container container
theme theme.Wrapped
padding bool padding bool
margin bool margin bool
vertical bool vertical bool
@ -39,10 +39,9 @@ func NewHBox (space Space, children ...tomo.Element) (element *Box) {
padding: space.Includes(SpacePadding), padding: space.Includes(SpacePadding),
margin: space.Includes(SpaceMargin), margin: space.Includes(SpaceMargin),
} }
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) element.entity = tomo.GetBackend().NewEntity(element)
element.minimumSize = element.updateMinimumSize element.minimumSize = element.updateMinimumSize
element.init() element.init()
element.theme.Case = tomo.C("tomo", "box")
element.Adopt(children...) element.Adopt(children...)
return return
} }
@ -54,16 +53,15 @@ func NewVBox (space Space, children ...tomo.Element) (element *Box) {
margin: space.Includes(SpaceMargin), margin: space.Includes(SpaceMargin),
vertical: true, vertical: true,
} }
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) element.entity = tomo.GetBackend().NewEntity(element)
element.minimumSize = element.updateMinimumSize element.minimumSize = element.updateMinimumSize
element.init() element.init()
element.theme.Case = tomo.C("tomo", "box")
element.Adopt(children...) element.Adopt(children...)
return return
} }
// Draw causes the element to draw to the specified destination canvas. // 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()) rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ { for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds() 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...) tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles { 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. // Layout causes this element to perform a layout operation.
func (element *Box) Layout () { func (element *Box) Layout () {
margin := element.theme.Margin(tomo.PatternBackground) margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
padding := element.theme.Padding(tomo.PatternBackground) padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
bounds := element.entity.Bounds() bounds := element.entity.Bounds()
if element.padding { bounds = padding.Apply(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 // DrawBackground draws this element's background pattern to the specified
// destination canvas. // destination canvas.
func (element *Box) DrawBackground (destination canvas.Canvas) { func (element *Box) DrawBackground (destination artist.Canvas) {
element.entity.DrawBackground(destination) element.entity.DrawBackground(destination)
} }
// SetTheme sets the element's theme. func (element *Box) HandleThemeChange () {
func (element *Box) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize() element.updateMinimumSize()
element.entity.Invalidate() element.entity.Invalidate()
element.entity.InvalidateLayout() element.entity.InvalidateLayout()
} }
func (element *Box) freeSpace () (space float64, nExpanding float64) { func (element *Box) freeSpace () (space float64, nExpanding float64) {
margin := element.theme.Margin(tomo.PatternBackground) margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
padding := element.theme.Padding(tomo.PatternBackground) padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
var marginSize int; if element.vertical { var marginSize int; if element.vertical {
marginSize = margin.Y marginSize = margin.Y
@ -176,8 +171,8 @@ func (element *Box) freeSpace () (space float64, nExpanding float64) {
} }
func (element *Box) updateMinimumSize () { func (element *Box) updateMinimumSize () {
margin := element.theme.Margin(tomo.PatternBackground) margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
padding := element.theme.Padding(tomo.PatternBackground) padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
var breadth, size int var breadth, size int
var marginSize int; if element.vertical { var marginSize int; if element.vertical {
marginSize = margin.Y marginSize = margin.Y

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,35 +2,29 @@ package elements
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo" 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/shatter"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
type documentEntity interface { var documentCase = tomo.C("tomo", "document")
tomo.ContainerEntity
tomo.ScrollableEntity
}
// Document is a scrollable container capcable of laying out flexible child // Document is a scrollable container capcable of laying out flexible child
// elements. Children can be added either inline (similar to an HTML/CSS inline // elements. Children can be added either inline (similar to an HTML/CSS inline
// element), or expanding (similar to an HTML/CSS block element). // element), or expanding (similar to an HTML/CSS block element).
type Document struct { type Document struct {
container container
entity documentEntity entity tomo.Entity
scroll image.Point scroll image.Point
contentBounds image.Rectangle contentBounds image.Rectangle
theme theme.Wrapped
onScrollBoundsChange func () onScrollBoundsChange func ()
} }
// NewDocument creates a new document container. // NewDocument creates a new document container.
func NewDocument (children ...tomo.Element) (element *Document) { func NewDocument (children ...tomo.Element) (element *Document) {
element = &Document { } element = &Document { }
element.theme.Case = tomo.C("tomo", "document") element.entity = tomo.GetBackend().NewEntity(element)
element.entity = tomo.NewEntity(element).(documentEntity)
element.container.entity = element.entity element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize element.minimumSize = element.updateMinimumSize
element.init() element.init()
@ -39,7 +33,7 @@ func NewDocument (children ...tomo.Element) (element *Document) {
} }
// Draw causes the element to draw to the specified destination canvas. // 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()) rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ { for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds() 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...) tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles { 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() element.scroll.Y = element.maxScrollHeight()
} }
margin := element.theme.Margin(tomo.PatternBackground) margin := element.entity.Theme().Margin(tomo.PatternBackground, documentCase)
padding := element.theme.Padding(tomo.PatternBackground) padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
bounds := padding.Apply(element.entity.Bounds()) bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { } element.contentBounds = image.Rectangle { }
@ -89,7 +83,7 @@ func (element *Document) Layout () {
if width < bounds.Dx() && entry.expand { if width < bounds.Dx() && entry.expand {
width = bounds.Dx() width = bounds.Dx()
} }
if typedChild, ok := child.(tomo.Flexible); ok { if typedChild, ok := child.(ability.Flexible); ok {
height = typedChild.FlexibleHeightFor(width) height = typedChild.FlexibleHeightFor(width)
} }
if rowHeight < height { if rowHeight < height {
@ -130,7 +124,7 @@ func (element *Document) AdoptInline (children ...tomo.Element) {
element.adopt(false, children...) element.adopt(false, children...)
} }
func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) { func (element *Document) HandleChildFlexibleHeightChange (child ability.Flexible) {
element.updateMinimumSize() element.updateMinimumSize()
element.entity.Invalidate() element.entity.Invalidate()
element.entity.InvalidateLayout() element.entity.InvalidateLayout()
@ -138,14 +132,11 @@ func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) {
// DrawBackground draws this element's background pattern to the specified // DrawBackground draws this element's background pattern to the specified
// destination canvas. // destination canvas.
func (element *Document) DrawBackground (destination canvas.Canvas) { func (element *Document) DrawBackground (destination artist.Canvas) {
element.entity.DrawBackground(destination) element.entity.DrawBackground(destination)
} }
// SetTheme sets the element's theme. func (element *Document) HandleThemeChange () {
func (element *Document) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize() element.updateMinimumSize()
element.entity.Invalidate() element.entity.Invalidate()
element.entity.InvalidateLayout() element.entity.InvalidateLayout()
@ -159,7 +150,7 @@ func (element *Document) ScrollContentBounds () image.Rectangle {
// ScrollViewportBounds returns the size and position of the element's // ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds. // viewport relative to ScrollBounds.
func (element *Document) ScrollViewportBounds () image.Rectangle { 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 := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll) bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds return bounds
@ -192,7 +183,7 @@ func (element *Document) ScrollAxes () (horizontal, vertical bool) {
} }
func (element *Document) maxScrollHeight () (height int) { 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() viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 } if height < 0 { height = 0 }
@ -200,7 +191,7 @@ func (element *Document) maxScrollHeight () (height int) {
} }
func (element *Document) updateMinimumSize () { func (element *Document) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternBackground) padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
minimumWidth := 0 minimumWidth := 0
for index := 0; index < element.entity.CountChildren(); index ++ { for index := 0; index < element.entity.CountChildren(); index ++ {
width, height := element.entity.ChildMinimumSize(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"
import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist" 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 { var fileCase = tomo.C("files", "file")
tomo.SelectableEntity
tomo.FocusableEntity
}
// File displays an interactive visual representation of a file within any // File displays an interactive visual representation of a file within any
// file system. // file system.
type File struct { type File struct {
entity fileEntity entity tomo.Entity
config config.Wrapped
theme theme.Wrapped
lastClick time.Time lastClick time.Time
pressed bool pressed bool
@ -43,8 +34,7 @@ func NewFile (
err error, err error,
) { ) {
element = &File { enabled: true } element = &File { enabled: true }
element.theme.Case = tomo.C("files", "file") element.entity = tomo.GetBackend().NewEntity(element)
element.entity = tomo.NewEntity(element).(fileEntity)
err = element.SetLocation(location, within) err = element.SetLocation(location, within)
return return
} }
@ -55,13 +45,13 @@ func (element *File) Entity () tomo.Entity {
} }
// Draw causes the element to draw to the specified destination canvas. // 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 // background
state := element.state() state := element.state()
bounds := element.entity.Bounds() bounds := element.entity.Bounds()
sink := element.theme.Sink(tomo.PatternButton) sink := element.entity.Theme().Sink(tomo.PatternButton, fileCase)
element.theme. element.entity.Theme().
Pattern(tomo.PatternButton, state). Pattern(tomo.PatternButton, state, fileCase).
Draw(destination, bounds) Draw(destination, bounds)
// icon // icon
@ -76,7 +66,7 @@ func (element *File) Draw (destination canvas.Canvas) {
} }
icon.Draw ( icon.Draw (
destination, destination,
element.theme.Color(tomo.ColorForeground, state), element.entity.Theme().Color(tomo.ColorForeground, state, fileCase),
bounds.Min.Add(offset)) bounds.Min.Add(offset))
} }
} }
@ -185,7 +175,7 @@ func (element *File) HandleMouseUp (
if button != input.ButtonLeft { return } if button != input.ButtonLeft { return }
element.pressed = false element.pressed = false
within := position.In(element.entity.Bounds()) 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 { if element.Enabled() && within && element.onChoose != nil {
element.onChoose() element.onChoose()
} }
@ -195,17 +185,8 @@ func (element *File) HandleMouseUp (
element.entity.Invalidate() element.entity.Invalidate()
} }
// SetTheme sets the element's theme. func (element *File) HandleThemeChange () {
func (element *File) SetTheme (theme tomo.Theme) { element.updateMinimumSize()
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
element.entity.Invalidate() element.entity.Invalidate()
} }
@ -219,11 +200,11 @@ func (element *File) state () tomo.State {
} }
func (element *File) icon () artist.Icon { 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 () { func (element *File) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternButton) padding := element.entity.Theme().Padding(tomo.PatternButton, fileCase)
icon := element.icon() icon := element.icon()
if icon == nil { if icon == nil {
element.entity.SetMinimumSize ( element.entity.SetMinimumSize (

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ package elements
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo" 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" import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
// TODO: this element is lame need to make it better // 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. // Image is an element capable of displaying an image.
type Image struct { type Image struct {
entity tomo.Entity entity tomo.Entity
buffer canvas.Canvas buffer artist.Canvas
} }
// NewImage creates a new image element. // NewImage creates a new image element.
func NewImage (image image.Image) (element *Image) { func NewImage (image image.Image) (element *Image) {
element = &Image { buffer: canvas.FromImage(image) } element = &Image { buffer: artist.FromImage(image) }
element.entity = tomo.NewEntity(element) element.entity = tomo.GetBackend().NewEntity(element)
bounds := element.buffer.Bounds() bounds := element.buffer.Bounds()
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy()) element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
return return
@ -28,7 +28,7 @@ func (element *Image) Entity () tomo.Entity {
} }
// Draw causes the element to draw to the specified destination canvas. // 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 } if element.entity == nil { return }
(patterns.Texture { Canvas: element.buffer }). (patterns.Texture { Canvas: element.buffer }).
Draw(destination, element.entity.Bounds()) 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"
import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/input" 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/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. // Label is a simple text box.
type Label struct { type Label struct {
entity tomo.FlexibleEntity entity tomo.Entity
align textdraw.Align align textdraw.Align
wrap bool wrap bool
@ -22,19 +22,15 @@ type Label struct {
forcedColumns int forcedColumns int
forcedRows int forcedRows int
minHeight int minHeight int
config config.Wrapped
theme theme.Wrapped
} }
// NewLabel creates a new label. // NewLabel creates a new label.
func NewLabel (text string) (element *Label) { func NewLabel (text string) (element *Label) {
element = &Label { } element = &Label { }
element.theme.Case = tomo.C("tomo", "label") element.entity = tomo.GetBackend().NewEntity(element)
element.entity = tomo.NewEntity(element).(tomo.FlexibleEntity) element.drawer.SetFace (element.entity.Theme().FontFace (
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular, tomo.FontStyleRegular,
tomo.FontSizeNormal)) tomo.FontSizeNormal, labelCase))
element.SetText(text) element.SetText(text)
return return
} }
@ -52,7 +48,7 @@ func (element *Label) Entity () tomo.Entity {
} }
// Draw causes the element to draw to the specified destination canvas. // 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() bounds := element.entity.Bounds()
if element.wrap { if element.wrap {
@ -63,9 +59,9 @@ func (element *Label) Draw (destination canvas.Canvas) {
element.entity.DrawBackground(destination) element.entity.DrawBackground(destination)
textBounds := element.drawer.LayoutBounds() textBounds := element.drawer.LayoutBounds()
foreground := element.theme.Color ( foreground := element.entity.Theme().Color (
tomo.ColorForeground, tomo.ColorForeground,
tomo.State { }) tomo.State { }, labelCase)
element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min)) element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min))
} }
@ -132,21 +128,10 @@ func (element *Label) SetAlign (align textdraw.Align) {
element.entity.Invalidate() element.entity.Invalidate()
} }
// SetTheme sets the element's theme. func (element *Label) HandleThemeChange () {
func (element *Label) SetTheme (new tomo.Theme) { element.drawer.SetFace (element.entity.Theme().FontFace (
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular, tomo.FontStyleRegular,
tomo.FontSizeNormal)) tomo.FontSizeNormal, labelCase))
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
element.updateMinimumSize() element.updateMinimumSize()
element.entity.Invalidate() element.entity.Invalidate()
} }
@ -195,7 +180,7 @@ func (element *Label) updateMinimumSize () {
if element.wrap { if element.wrap {
em := element.drawer.Em().Round() em := element.drawer.Em().Round()
if em < 1 { 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() width, height = em, element.drawer.LineHeight().Round()
element.entity.NotifyFlexibleHeightChange() element.entity.NotifyFlexibleHeightChange()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,11 +5,11 @@ import "time"
import "image" import "image"
import "image/color" import "image/color"
import "git.tebibyte.media/sashakoshka/tomo" 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"
import "git.tebibyte.media/sashakoshka/tomo/shatter" import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" 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 "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font" import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font"
@ -22,7 +22,7 @@ type Artist struct {
// NewArtist creates a new artist test element. // NewArtist creates a new artist test element.
func NewArtist () (element *Artist) { func NewArtist () (element *Artist) {
element = &Artist { } element = &Artist { }
element.entity = tomo.NewEntity(element) element.entity = tomo.GetBackend().NewEntity(element)
element.entity.SetMinimumSize(240, 240) element.entity.SetMinimumSize(240, 240)
return return
} }
@ -31,7 +31,7 @@ func (element *Artist) Entity () tomo.Entity {
return element.entity return element.entity
} }
func (element *Artist) Draw (destination canvas.Canvas) { func (element *Artist) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds() bounds := element.entity.Bounds()
patterns.Uhex(0x000000FF).Draw(destination, bounds) patterns.Uhex(0x000000FF).Draw(destination, bounds)
@ -44,28 +44,28 @@ func (element *Artist) Draw (destination canvas.Canvas) {
// 4, 0 // 4, 0
c40 := element.cellAt(destination, 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 ( shapes.ColorLine (
c40, artist.Hex(0xFF0000FF), 1, c40, artutil.Hex(0xFF0000FF), 1,
c40.Bounds().Min, c40.Bounds().Max) c40.Bounds().Min, c40.Bounds().Max)
// 0, 1 // 0, 1
c01 := element.cellAt(destination, 0, 1) c01 := element.cellAt(destination, 0, 1)
shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1) shapes.StrokeColorRectangle(c01, artutil.Hex(0x888888FF), c01.Bounds(), 1)
shapes.FillColorEllipse(destination, artist.Hex(0x00FF00FF), c01.Bounds()) shapes.FillColorEllipse(destination, artutil.Hex(0x00FF00FF), c01.Bounds())
// 1, 1 - 3, 1 // 1, 1 - 3, 1
for x := 1; x < 4; x ++ { for x := 1; x < 4; x ++ {
c := element.cellAt(destination, x, 1) c := element.cellAt(destination, x, 1)
shapes.StrokeColorRectangle ( shapes.StrokeColorRectangle (
destination, artist.Hex(0x888888FF), destination, artutil.Hex(0x888888FF),
c.Bounds(), 1) c.Bounds(), 1)
shapes.StrokeColorEllipse ( shapes.StrokeColorEllipse (
destination, destination,
[]color.RGBA { []color.RGBA {
artist.Hex(0xFF0000FF), artutil.Hex(0xFF0000FF),
artist.Hex(0x00FF00FF), artutil.Hex(0x00FF00FF),
artist.Hex(0xFF00FFFF), artutil.Hex(0xFF00FFFF),
} [x - 1], } [x - 1],
c.Bounds(), x) c.Bounds(), x)
} }
@ -93,12 +93,12 @@ func (element *Artist) Draw (destination canvas.Canvas) {
// 0, 2 // 0, 2
c02 := element.cellAt(destination, 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()) shapes.FillEllipse(c02, c41, c02.Bounds())
// 1, 2 // 1, 2
c12 := element.cellAt(destination, 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) shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5)
// 2, 2 // 2, 2
@ -135,7 +135,7 @@ func (element *Artist) Draw (destination canvas.Canvas) {
patterns.Border { patterns.Border {
Canvas: element.thingy(c42), Canvas: element.thingy(c42),
Inset: artist.Inset { 8, 8, 8, 8 }, 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? // how long did that take to render?
drawTime := time.Since(drawStart) drawTime := time.Since(drawStart)
@ -146,13 +146,13 @@ func (element *Artist) Draw (destination canvas.Canvas) {
drawTime.Milliseconds(), drawTime.Milliseconds(),
drawTime.Microseconds()))) drawTime.Microseconds())))
textDrawer.Draw ( textDrawer.Draw (
destination, artist.Hex(0xFFFFFFFF), destination, artutil.Hex(0xFFFFFFFF),
image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24)) 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) 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, bounds.Min, bounds.Max)
shapes.ColorLine ( shapes.ColorLine (
destination, c, weight, 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)) 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() bounds := element.entity.Bounds()
cellBounds := image.Rectangle { } cellBounds := image.Rectangle { }
cellBounds.Min = bounds.Min cellBounds.Min = bounds.Min
cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5 cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4 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(), x * cellBounds.Dx(),
y * cellBounds.Dy()))) 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 := destination.Bounds()
bounds = image.Rect(0, 0, 32, 32).Add(bounds.Min) bounds = image.Rect(0, 0, 32, 32).Add(bounds.Min)
shapes.FillColorRectangle(destination, artist.Hex(0x440000FF), bounds) shapes.FillColorRectangle(destination, artutil.Hex(0x440000FF), bounds)
shapes.StrokeColorRectangle(destination, artist.Hex(0xFF0000FF), bounds, 1) shapes.StrokeColorRectangle(destination, artutil.Hex(0xFF0000FF), bounds, 1)
shapes.StrokeColorRectangle(destination, artist.Hex(0x004400FF), bounds.Inset(4), 1) shapes.StrokeColorRectangle(destination, artutil.Hex(0x004400FF), bounds.Inset(4), 1)
shapes.FillColorRectangle(destination, artist.Hex(0x004444FF), bounds.Inset(12)) shapes.FillColorRectangle(destination, artutil.Hex(0x004444FF), bounds.Inset(12))
shapes.StrokeColorRectangle(destination, artist.Hex(0x888888FF), bounds.Inset(8), 1) shapes.StrokeColorRectangle(destination, artutil.Hex(0x888888FF), bounds.Inset(8), 1)
return canvas.Cut(destination, bounds) 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"
import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist" 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/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
var mouseCase = tomo.C("tomo", "mouse")
// Mouse is an element capable of testing mouse input. When the mouse is clicked // Mouse is an element capable of testing mouse input. When the mouse is clicked
// and dragged on it, it draws a trail. // and dragged on it, it draws a trail.
@ -15,16 +15,12 @@ type Mouse struct {
entity tomo.Entity entity tomo.Entity
pressed bool pressed bool
lastMousePos image.Point lastMousePos image.Point
config config.Wrapped
theme theme.Wrapped
} }
// NewMouse creates a new mouse test element. // NewMouse creates a new mouse test element.
func NewMouse () (element *Mouse) { func NewMouse () (element *Mouse) {
element = &Mouse { } element = &Mouse { }
element.theme.Case = tomo.C("tomo", "mouse") element.entity = tomo.GetBackend().NewEntity(element)
element.entity = tomo.NewEntity(element)
element.entity.SetMinimumSize(32, 32) element.entity.SetMinimumSize(32, 32)
return return
} }
@ -33,41 +29,34 @@ func (element *Mouse) Entity () tomo.Entity {
return element.entity return element.entity
} }
func (element *Mouse) Draw (destination canvas.Canvas) { func (element *Mouse) Draw (destination artist.Canvas) {
bounds := element.entity.Bounds() bounds := element.entity.Bounds()
accent := element.theme.Color ( accent := element.entity.Theme().Color (
tomo.ColorAccent, tomo.ColorAccent,
tomo.State { }) tomo.State { },
mouseCase)
shapes.FillColorRectangle(destination, accent, bounds) shapes.FillColorRectangle(destination, accent, bounds)
shapes.StrokeColorRectangle ( shapes.StrokeColorRectangle (
destination, destination,
artist.Hex(0x000000FF), artutil.Hex(0x000000FF),
bounds, 1) bounds, 1)
shapes.ColorLine ( shapes.ColorLine (
destination, artist.Hex(0xFFFFFFFF), 1, destination, artutil.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, 1)), bounds.Min.Add(image.Pt(1, 1)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2))) bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
shapes.ColorLine ( 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(1, bounds.Dy() - 2)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1))) bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
if element.pressed { if element.pressed {
midpoint := bounds.Min.Add(bounds.Max.Sub(bounds.Min).Div(2)) midpoint := bounds.Min.Add(bounds.Max.Sub(bounds.Min).Div(2))
shapes.ColorLine ( shapes.ColorLine (
destination, artist.Hex(0x000000FF), 1, destination, artutil.Hex(0x000000FF), 1,
midpoint, element.lastMousePos) midpoint, element.lastMousePos)
} }
} }
// SetTheme sets the element's theme. func (element *Mouse) HandleThemeChange (new tomo.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
element.entity.Invalidate() 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/data"
import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist" 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/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/textmanip" import "git.tebibyte.media/sashakoshka/tomo/textmanip"
import "git.tebibyte.media/sashakoshka/tomo/fixedutil" import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" 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 { var textBoxCase = tomo.C("tomo", "textBox")
tomo.FocusableEntity
tomo.ScrollableEntity
tomo.LayoutEntity
}
// TextBox is a single-line text input. // TextBox is a single-line text input.
type TextBox struct { type TextBox struct {
entity textBoxEntity entity tomo.Entity
enabled bool enabled bool
lastClick time.Time lastClick time.Time
@ -36,9 +29,6 @@ type TextBox struct {
placeholderDrawer textdraw.Drawer placeholderDrawer textdraw.Drawer
valueDrawer textdraw.Drawer valueDrawer textdraw.Drawer
config config.Wrapped
theme theme.Wrapped
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool) onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
onChange func () onChange func ()
onEnter func () onEnter func ()
@ -50,15 +40,14 @@ type TextBox struct {
// text. // text.
func NewTextBox (placeholder, value string) (element *TextBox) { func NewTextBox (placeholder, value string) (element *TextBox) {
element = &TextBox { enabled: true } element = &TextBox { enabled: true }
element.theme.Case = tomo.C("tomo", "textBox") element.entity = tomo.GetBackend().NewEntity(element)
element.entity = tomo.NewEntity(element).(textBoxEntity)
element.placeholder = placeholder element.placeholder = placeholder
element.placeholderDrawer.SetFace (element.theme.FontFace ( element.placeholderDrawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular, tomo.FontStyleRegular,
tomo.FontSizeNormal)) tomo.FontSizeNormal, textBoxCase))
element.valueDrawer.SetFace (element.theme.FontFace ( element.valueDrawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular, tomo.FontStyleRegular,
tomo.FontSizeNormal)) tomo.FontSizeNormal, textBoxCase))
element.placeholderDrawer.SetText([]rune(placeholder)) element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize() element.updateMinimumSize()
element.SetValue(value) element.SetValue(value)
@ -71,19 +60,19 @@ func (element *TextBox) Entity () tomo.Entity {
} }
// Draw causes the element to draw to the specified destination canvas. // 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() bounds := element.entity.Bounds()
state := element.state() state := element.state()
pattern := element.theme.Pattern(tomo.PatternInput, state) pattern := element.entity.Theme().Pattern(tomo.PatternInput, state, textBoxCase)
padding := element.theme.Padding(tomo.PatternInput) padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
innerCanvas := canvas.Cut(destination, padding.Apply(bounds)) innerCanvas := artist.Cut(destination, padding.Apply(bounds))
pattern.Draw(destination, bounds) pattern.Draw(destination, bounds)
offset := element.textOffset() offset := element.textOffset()
if element.entity.Focused() && !element.dot.Empty() { if element.entity.Focused() && !element.dot.Empty() {
// draw selection bounds // draw selection bounds
accent := element.theme.Color(tomo.ColorAccent, state) accent := element.entity.Theme().Color(tomo.ColorAccent, state, textBoxCase)
canon := element.dot.Canon() canon := element.dot.Canon()
foff := fixedutil.Pt(offset) foff := fixedutil.Pt(offset)
start := element.valueDrawer.PositionAt(canon.Start).Add(foff) start := element.valueDrawer.PositionAt(canon.Start).Add(foff)
@ -101,9 +90,9 @@ func (element *TextBox) Draw (destination canvas.Canvas) {
if len(element.text) == 0 { if len(element.text) == 0 {
// draw placeholder // draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds() textBounds := element.placeholderDrawer.LayoutBounds()
foreground := element.theme.Color ( foreground := element.entity.Theme().Color (
tomo.ColorForeground, tomo.ColorForeground,
tomo.State { Disabled: true }) tomo.State { Disabled: true }, textBoxCase)
element.placeholderDrawer.Draw ( element.placeholderDrawer.Draw (
innerCanvas, innerCanvas,
foreground, foreground,
@ -111,7 +100,7 @@ func (element *TextBox) Draw (destination canvas.Canvas) {
} else { } else {
// draw input value // draw input value
textBounds := element.valueDrawer.LayoutBounds() textBounds := element.valueDrawer.LayoutBounds()
foreground := element.theme.Color(tomo.ColorForeground, state) foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
element.valueDrawer.Draw ( element.valueDrawer.Draw (
innerCanvas, innerCanvas,
foreground, foreground,
@ -120,7 +109,7 @@ func (element *TextBox) Draw (destination canvas.Canvas) {
if element.entity.Focused() && element.dot.Empty() { if element.entity.Focused() && element.dot.Empty() {
// draw cursor // draw cursor
foreground := element.theme.Color(tomo.ColorForeground, state) foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
cursorPosition := fixedutil.RoundPt ( cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End)) element.valueDrawer.PositionAt(element.dot.End))
shapes.ColorLine ( shapes.ColorLine (
@ -156,7 +145,7 @@ func (element *TextBox) HandleMouseDown (
runeIndex := element.atPosition(position) runeIndex := element.atPosition(position)
if runeIndex == -1 { return } 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.dragging = 2
element.dot = textmanip.WordAround(element.text, runeIndex) element.dot = textmanip.WordAround(element.text, runeIndex)
} else { } else {
@ -214,7 +203,7 @@ func (element *TextBox) HandleMotion (position image.Point) {
} }
func (element *TextBox) textOffset () 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() bounds := element.entity.Bounds()
innerBounds := padding.Apply(bounds) innerBounds := padding.Apply(bounds)
textHeight := element.valueDrawer.LineHeight().Round() textHeight := element.valueDrawer.LineHeight().Round()
@ -476,27 +465,17 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
return true, false return true, false
} }
// SetTheme sets the element's theme. func (element *TextBox) HandleThemeChange () {
func (element *TextBox) SetTheme (new tomo.Theme) { face := element.entity.Theme().FontFace (
if new == element.theme.Theme { return }
element.theme.Theme = new
face := element.theme.FontFace (
tomo.FontStyleRegular, tomo.FontStyleRegular,
tomo.FontSizeNormal) tomo.FontSizeNormal,
textBoxCase)
element.placeholderDrawer.SetFace(face) element.placeholderDrawer.SetFace(face)
element.valueDrawer.SetFace(face) element.valueDrawer.SetFace(face)
element.updateMinimumSize() element.updateMinimumSize()
element.entity.Invalidate() 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) { func (element *TextBox) contextMenu (position image.Point) {
window := element.entity.Window() window := element.entity.Window()
menu, err := window.NewMenu(image.Rectangle { position, position }) menu, err := window.NewMenu(image.Rectangle { position, position })
@ -540,12 +519,12 @@ func (element *TextBox) runOnChange () {
} }
func (element *TextBox) scrollViewportWidth () (width int) { 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() return padding.Apply(element.entity.Bounds()).Dx()
} }
func (element *TextBox) scrollToCursor () { 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 := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min) bounds = bounds.Sub(bounds.Min)
bounds.Max.X -= element.valueDrawer.Em().Round() bounds.Max.X -= element.valueDrawer.Em().Round()
@ -568,7 +547,7 @@ func (element *TextBox) scrollToCursor () {
func (element *TextBox) updateMinimumSize () { func (element *TextBox) updateMinimumSize () {
textBounds := element.placeholderDrawer.LayoutBounds() textBounds := element.placeholderDrawer.LayoutBounds()
padding := element.theme.Padding(tomo.PatternInput) padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
element.entity.SetMinimumSize ( element.entity.SetMinimumSize (
padding.Horizontal() + textBounds.Dx(), padding.Horizontal() + textBounds.Dx(),
padding.Vertical() + padding.Vertical() +

View File

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

View File

@ -1,16 +1,21 @@
package tomo package tomo
import "image" 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 // Entity is a handle given to elements by the backend. Extended entity
// entities may be assigned to elements that support different capabilities. // interfaces are defined in the ability module.
type Entity interface { type Entity interface {
// Invalidate marks the element's current visual as invalid. At the end // Invalidate marks the element's current visual as invalid. At the end
// of every event, the backend will ask all invalid entities to redraw // of every event, the backend will ask all invalid entities to redraw
// themselves. // themselves.
Invalidate () 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 // Bounds returns the bounds of the element to be used for drawing and
// layout. // layout.
Bounds () image.Rectangle Bounds () image.Rectangle
@ -28,23 +33,9 @@ type Entity interface {
// labels. If there is no parent element (that is, the element is // labels. If there is no parent element (that is, the element is
// directly inside of the window), the backend will draw a default // directly inside of the window), the backend will draw a default
// background pattern. // background pattern.
DrawBackground (canvas.Canvas) DrawBackground (artist.Canvas)
}
// LayoutEntity is given to elements that support the Layoutable interface. // --- Behaviors relating to parenting ---
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
// Adopt adds an element as a child. // Adopt adds an element as a child.
Adopt (child Element) Adopt (child Element)
@ -76,11 +67,8 @@ type ContainerEntity interface {
// ChildMinimumSize returns the minimum size of the child at the // ChildMinimumSize returns the minimum size of the child at the
// specified index. // specified index.
ChildMinimumSize (index int) (width, height int) ChildMinimumSize (index int) (width, height int)
}
// FocusableEntity is given to elements that support the Focusable interface. // --- Behaviors relating to input focus ---
type FocusableEntity interface {
Entity
// Focused returns whether the element currently has input focus. // Focused returns whether the element currently has input focus.
Focused () bool Focused () bool
@ -96,29 +84,16 @@ type FocusableEntity interface {
// FocusPrevious causes the focus to move to the next element. If this // FocusPrevious causes the focus to move to the next element. If this
// succeeds, the element will recieve a HandleUnfocus call. // succeeds, the element will recieve a HandleUnfocus call.
FocusPrevious () FocusPrevious ()
}
// SelectableEntity is given to elements that support the Selectable interface.
type SelectableEntity interface {
Entity
// Selected returns whether this element is currently selected. // Selected returns whether this element is currently selected.
Selected () bool Selected () bool
}
// FlexibleEntity is given to elements that support the Flexible interface. // --- Behaviors relating to scrolling --- //
type FlexibleEntity interface {
Entity
// NotifyFlexibleHeightChange notifies the system that the parameters // NotifyFlexibleHeightChange notifies the system that the parameters
// affecting the element's flexible height have changed. This method is // affecting the element's flexible height have changed. This method is
// expected to be called by flexible elements when their content changes. // expected to be called by flexible elements when their content changes.
NotifyFlexibleHeightChange () NotifyFlexibleHeightChange ()
}
// ScrollableEntity is given to elements that support the Scrollable interface.
type ScrollableEntity interface {
Entity
// NotifyScrollBoundsChange notifies the system that the element's // NotifyScrollBoundsChange notifies the system that the element's
// scroll content bounds or viewport bounds have changed. This is // scroll content bounds or viewport bounds have changed. This is
@ -126,4 +101,14 @@ type ScrollableEntity interface {
// supported scroll axes, their scroll position (either autonomously or // supported scroll axes, their scroll position (either autonomously or
// as a result of a call to ScrollTo()), or their content size. // as a result of a call to ScrollTo()), or their content size.
NotifyScrollBoundsChange () 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 _ "image/jpeg"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
var validImageTypes = []data.Mime { var validImageTypes = []data.Mime {
data.M("image", "png"), data.M("image", "png"),
@ -21,8 +17,15 @@ var validImageTypes = []data.Mime {
data.M("image", "jpeg"), data.M("image", "jpeg"),
} }
func run () { func main () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0)) 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") window.SetTitle("Clipboard")
container := elements.NewVBox(elements.SpaceBoth) container := elements.NewVBox(elements.SpaceBoth)
@ -114,8 +117,9 @@ func run () {
container.Adopt(controlRow) container.Adopt(controlRow)
window.Adopt(container) window.Adopt(container)
window.OnClose(tomo.Stop) window.OnClose(nasin.Stop)
window.Show() window.Show()
return nil
} }
func imageWindow (parent tomo.Window, image image.Image) { func imageWindow (parent tomo.Window, image image.Image) {

View File

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

View File

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

View File

@ -3,19 +3,23 @@ package main
import "os" import "os"
import "path/filepath" import "path/filepath"
import "git.tebibyte.media/sashakoshka/tomo" 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"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () { func main () {
tomo.Run(run) nasin.Run(Application { })
} }
func run () { type Application struct { }
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 384, 384))
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 384, 384))
if err != nil { return err }
window.SetTitle("File browser") window.SetTitle("File browser")
container := elements.NewVBox(elements.SpaceBoth) container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container) window.Adopt(container)
homeDir, _ := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { return err }
controlBar := elements.NewHBox(elements.SpaceNone) controlBar := elements.NewHBox(elements.SpaceNone)
backButton := elements.NewButton("Back") backButton := elements.NewButton("Back")
@ -78,6 +82,7 @@ func run () {
elements.NewScroll(elements.ScrollVertical, directoryView)) elements.NewScroll(elements.ScrollVertical, directoryView))
container.Adopt(statusBar) container.Adopt(statusBar)
window.OnClose(tomo.Stop) window.OnClose(nasin.Stop)
window.Show() 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 package main
import "os"
import "time" import "time"
import "git.tebibyte.media/sashakoshka/tomo" 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"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun" import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () { func main () {
tomo.Run(run) nasin.Run(Application { })
os.Exit(0)
} }
func run () { type Application struct { }
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216))
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 200, 216))
if err != nil { return err }
window.SetTitle("Clock") window.SetTitle("Clock")
window.SetApplicationName("TomoClock") window.SetApplicationName("TomoClock")
container := elements.NewVBox(elements.SpaceBoth) container := elements.NewVBox(elements.SpaceBoth)
@ -24,9 +25,10 @@ func run () {
container.AdoptExpand(clock) container.AdoptExpand(clock)
container.Adopt(label) container.Adopt(label)
window.OnClose(tomo.Stop) window.OnClose(nasin.Stop)
window.Show() window.Show()
go tick(label, clock) go tick(label, clock)
return nil
} }
func formatTime () (timeString string) { func formatTime () (timeString string) {
@ -35,7 +37,7 @@ func formatTime () (timeString string) {
func tick (label *elements.Label, clock *fun.AnalogClock) { func tick (label *elements.Label, clock *fun.AnalogClock) {
for { for {
tomo.Do (func () { nasin.Do (func () {
label.SetText(formatTime()) label.SetText(formatTime())
clock.SetTime(time.Now()) 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 package main
import "git.tebibyte.media/sashakoshka/tomo" 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"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () { func main () {
tomo.Run(run) nasin.Run(Application { })
} }
func run () { type Application struct { }
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0))
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 360, 0))
if err != nil { return err }
window.SetTitle("Icons") window.SetTitle("Icons")
container := elements.NewVBox(elements.SpaceBoth) container := elements.NewVBox(elements.SpaceBoth)
@ -26,11 +29,12 @@ func run () {
closeButton := elements.NewButton("Yes verynice") closeButton := elements.NewButton("Yes verynice")
closeButton.SetIcon(tomo.IconYes) closeButton.SetIcon(tomo.IconYes)
closeButton.OnClick(tomo.Stop) closeButton.OnClick(window.Close)
container.Adopt(closeButton) container.Adopt(closeButton)
window.OnClose(tomo.Stop) window.OnClose(nasin.Stop)
window.Show() window.Show()
return nil
} }
func icons (min, max tomo.Icon) (container *elements.Box) { func icons (min, max tomo.Icon) (container *elements.Box) {

View File

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

View File

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

View File

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

View File

@ -3,15 +3,18 @@ package main
import "fmt" import "fmt"
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo" 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"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () { func main () {
tomo.Run(run) nasin.Run(Application { })
} }
func run () { type Application struct { }
window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256))
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(200, 200, 256, 256))
if err != nil { return err }
window.SetTitle("Main") window.SetTitle("Main")
container := elements.NewVBox ( container := elements.NewVBox (
@ -19,13 +22,14 @@ func run () {
elements.NewLabel("Main window")) elements.NewLabel("Main window"))
window.Adopt(container) window.Adopt(container)
window.OnClose(tomo.Stop) window.OnClose(nasin.Stop)
window.Show() window.Show()
createPanel(window, 0, tomo.Bounds(-64, 20, 0, 0)) createPanel(window, 0, tomo.Bounds(-64, 20, 0, 0))
createPanel(window, 1, tomo.Bounds(200, 20, 0, 0)) createPanel(window, 1, tomo.Bounds(200, 20, 0, 0))
createPanel(window, 2, tomo.Bounds(-64, 180, 0, 0)) createPanel(window, 2, tomo.Bounds(-64, 180, 0, 0))
createPanel(window, 3, tomo.Bounds(200, 180, 0, 0)) createPanel(window, 3, tomo.Bounds(200, 180, 0, 0))
return nil
} }
func createPanel (parent tomo.MainWindow, id int, bounds image.Rectangle) { 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 package main
import "git.tebibyte.media/sashakoshka/tomo" 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/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () { func main () {
tomo.Run(run) nasin.Run(Application { })
} }
func run () { type Application struct { }
window, err := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { panic(err.Error()) } func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { return err }
window.SetTitle("Dialog Boxes") window.SetTitle("Dialog Boxes")
container := elements.NewVBox(elements.SpaceBoth) container := elements.NewVBox(elements.SpaceBoth)
@ -76,9 +78,10 @@ func run () {
container.Adopt(menuButton) container.Adopt(menuButton)
cancelButton := elements.NewButton("No thank you.") cancelButton := elements.NewButton("No thank you.")
cancelButton.OnClick(tomo.Stop) cancelButton.OnClick(nasin.Stop)
container.Adopt(cancelButton) container.Adopt(cancelButton)
window.OnClose(tomo.Stop) window.OnClose(nasin.Stop)
window.Show() window.Show()
return nil
} }

View File

@ -2,16 +2,19 @@ package main
import "time" import "time"
import "git.tebibyte.media/sashakoshka/tomo" 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/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () { func main () {
tomo.Run(run) nasin.Run(Application { })
} }
func run () { type Application struct { }
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { return err }
window.SetTitle("Approaching") window.SetTitle("Approaching")
container := elements.NewVBox(elements.SpaceBoth) container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container) window.Adopt(container)
@ -23,19 +26,20 @@ func run () {
button.SetEnabled(false) button.SetEnabled(false)
container.Adopt(button) container.Adopt(button)
window.OnClose(tomo.Stop) window.OnClose(nasin.Stop)
window.Show() window.Show()
go fill(window, bar) go fill(window, bar)
return nil
} }
func fill (window tomo.Window, bar *elements.ProgressBar) { func fill (window tomo.Window, bar *elements.ProgressBar) {
for progress := 0.0; progress < 1.0; progress += 0.01 { for progress := 0.0; progress < 1.0; progress += 0.01 {
time.Sleep(time.Second / 24) time.Sleep(time.Second / 24)
tomo.Do (func () { nasin.Do (func () {
bar.SetProgress(progress) bar.SetProgress(progress)
}) })
} }
tomo.Do (func () { nasin.Do (func () {
popups.NewDialog ( popups.NewDialog (
popups.DialogKindInfo, popups.DialogKindInfo,
window, 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 package main
import "git.tebibyte.media/sashakoshka/tomo" 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"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () { func main () {
tomo.Run(run) nasin.Run(Application { })
} }
func run () { type Application struct { }
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { return err }
window.SetTitle("Spaced Out") window.SetTitle("Spaced Out")
container := elements.NewVBox ( container := elements.NewVBox (
@ -21,6 +24,7 @@ func run () {
container.Adopt(elements.NewLabel("This is at the bottom")) container.Adopt(elements.NewLabel("This is at the bottom"))
window.Adopt(container) window.Adopt(container)
window.OnClose(tomo.Stop) window.OnClose(nasin.Stop)
window.Show() 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 // initializeKeymapInformation grabs keyboard mapping information from the X
// server. // server.
func (backend *Backend) initializeKeymapInformation () { func (backend *backend) initializeKeymapInformation () {
keybind.Initialize(backend.connection) keybind.Initialize(backend.connection)
backend.modifierMasks.capsLock = backend.keysymToMask(0xFFE5) backend.modifierMasks.capsLock = backend.keysymToMask(0xFFE5)
backend.modifierMasks.shiftLock = backend.keysymToMask(0xFFE6) 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 // keysymToKeycode converts an X keysym to an X keycode, instead of the other
// way around. // way around.
func (backend *Backend) keysymToKeycode ( func (backend *backend) keysymToKeycode (
symbol xproto.Keysym, symbol xproto.Keysym,
) ( ) (
code xproto.Keycode, code xproto.Keycode,
@ -148,7 +148,7 @@ func (backend *Backend) keysymToKeycode (
} }
// keysymToMask returns the X modmask for a given modifier key. // keysymToMask returns the X modmask for a given modifier key.
func (backend *Backend) keysymToMask ( func (backend *backend) keysymToMask (
symbol xproto.Keysym, symbol xproto.Keysym,
) ( ) (
mask uint16, mask uint16,
@ -164,7 +164,7 @@ func (backend *Backend) keysymToMask (
// fleshed out version of some of the logic found in xgbutil/keybind/encoding.go // 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 // to get a full keycode to keysym conversion, but eliminates redundant work by
// going straight to a tomo keycode. // going straight to a tomo keycode.
func (backend *Backend) keycodeToKey ( func (backend *backend) keycodeToKey (
keycode xproto.Keycode, keycode xproto.Keycode,
state uint16, state uint16,
) ( ) (

View File

@ -2,9 +2,11 @@ package x
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo" 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 { type entity struct {
backend *backend
window *window window *window
parent *entity parent *entity
children []*entity children []*entity
@ -17,15 +19,11 @@ type entity struct {
selected bool selected bool
layoutInvalid bool layoutInvalid bool
isContainer bool
} }
func (backend *Backend) NewEntity (owner tomo.Element) tomo.Entity { func (backend *backend) NewEntity (owner tomo.Element) tomo.Entity {
entity := &entity { element: owner } entity := &entity { element: owner, backend: backend }
if _, ok := owner.(tomo.Container); ok {
entity.isContainer = true
entity.InvalidateLayout() entity.InvalidateLayout()
}
return entity return entity
} }
@ -44,7 +42,7 @@ func (ent *entity) unlink () {
ent.parent = nil ent.parent = nil
ent.window = nil ent.window = nil
if element, ok := ent.element.(tomo.Selectable); ok { if element, ok := ent.element.(ability.Selectable); ok {
ent.selected = false ent.selected = false
element.HandleSelectionChange() 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 entity
} }
return nil 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 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) callback(parent, entity.element)
} }
entity.parent.forMouseTargetContainers(callback) entity.parent.forMouseTargetContainers(callback)
@ -156,18 +154,19 @@ func (entity *entity) SetMinimumSize (width, height int) {
entity.window.setMinimumSize(width, height) entity.window.setMinimumSize(width, height)
} }
} else { } else {
entity.parent.element.(tomo.Container). entity.parent.element.(ability.Container).
HandleChildMinimumSizeChange(entity.element) HandleChildMinimumSizeChange(entity.element)
} }
} }
func (entity *entity) DrawBackground (destination canvas.Canvas) { func (entity *entity) DrawBackground (destination artist.Canvas) {
if entity.parent != nil { if entity.parent != nil {
entity.parent.element.(tomo.Container).DrawBackground(destination) entity.parent.element.(ability.Container).DrawBackground(destination)
} else if entity.window != nil { } else if entity.window != nil {
entity.window.system.theme.Pattern ( entity.backend.theme.Pattern (
tomo.PatternBackground, tomo.PatternBackground,
tomo.State { }).Draw ( tomo.State { },
tomo.C("tomo", "window")).Draw (
destination, destination,
entity.window.canvas.Bounds()) entity.window.canvas.Bounds())
} }
@ -177,7 +176,7 @@ func (entity *entity) DrawBackground (destination canvas.Canvas) {
func (entity *entity) InvalidateLayout () { func (entity *entity) InvalidateLayout () {
if entity.window == nil { return } if entity.window == nil { return }
if !entity.isContainer { return } if _, ok := entity.element.(ability.Layoutable); !ok { return }
entity.layoutInvalid = true entity.layoutInvalid = true
entity.window.system.anyLayoutInvalid = 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) { func (entity *entity) SelectChild (index int, selected bool) {
child := entity.children[index] 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 } if child.selected == selected { return }
child.selected = selected child.selected = selected
element.HandleSelectionChange() element.HandleSelectionChange()
@ -275,9 +274,9 @@ func (entity *entity) Selected () bool {
func (entity *entity) NotifyFlexibleHeightChange () { func (entity *entity) NotifyFlexibleHeightChange () {
if entity.parent == nil { return } 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 ( parent.HandleChildFlexibleHeightChange (
entity.element.(tomo.Flexible)) entity.element.(ability.Flexible))
} }
} }
@ -285,8 +284,20 @@ func (entity *entity) NotifyFlexibleHeightChange () {
func (entity *entity) NotifyScrollBoundsChange () { func (entity *entity) NotifyScrollBoundsChange () {
if entity.parent == nil { return } 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 ( 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 "image"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/ability"
import "github.com/jezek/xgbutil" import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto" import "github.com/jezek/xgb/xproto"
@ -134,7 +135,7 @@ func (window *window) handleKeyPress (
} else if key == input.KeyEscape && window.shy { } else if key == input.KeyEscape && window.shy {
window.Close() window.Close()
} else if window.focused != nil { } 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) } if ok { focused.HandleKeyDown(key, modifiers) }
} }
} }
@ -169,7 +170,7 @@ func (window *window) handleKeyRelease (
modifiers.NumberPad = numberPad modifiers.NumberPad = numberPad
if window.focused != nil { if window.focused != nil {
focused, ok := window.focused.element.(tomo.KeyboardTarget) focused, ok := window.focused.element.(ability.KeyboardTarget)
if ok { focused.HandleKeyUp(key, modifiers) } if ok { focused.HandleKeyUp(key, modifiers) }
} }
} }
@ -191,7 +192,7 @@ func (window *window) handleButtonPress (
} else if scrolling { } else if scrolling {
underneath := window.system.scrollTargetChildAt(point) underneath := window.system.scrollTargetChildAt(point)
if underneath != nil { if underneath != nil {
if child, ok := underneath.element.(tomo.ScrollTarget); ok { if child, ok := underneath.element.(ability.ScrollTarget); ok {
sum := scrollSum { } sum := scrollSum { }
sum.add(buttonEvent.Detail, window, buttonEvent.State) sum.add(buttonEvent.Detail, window, buttonEvent.State)
window.compressScrollSum(buttonEvent, &sum) window.compressScrollSum(buttonEvent, &sum)
@ -203,12 +204,12 @@ func (window *window) handleButtonPress (
} else { } else {
underneath := window.system.childAt(point) underneath := window.system.childAt(point)
window.system.drags[buttonEvent.Detail] = underneath window.system.drags[buttonEvent.Detail] = underneath
if child, ok := underneath.element.(tomo.MouseTarget); ok { if child, ok := underneath.element.(ability.MouseTarget); ok {
child.HandleMouseDown ( child.HandleMouseDown (
point, input.Button(buttonEvent.Detail), point, input.Button(buttonEvent.Detail),
modifiers) modifiers)
} }
callback := func (container tomo.MouseTargetContainer, child tomo.Element) { callback := func (container ability.MouseTargetContainer, child tomo.Element) {
container.HandleChildMouseDown ( container.HandleChildMouseDown (
point, input.Button(buttonEvent.Detail), point, input.Button(buttonEvent.Detail),
modifiers, child) modifiers, child)
@ -229,7 +230,7 @@ func (window *window) handleButtonRelease (
dragging := window.system.drags[buttonEvent.Detail] dragging := window.system.drags[buttonEvent.Detail]
if dragging != nil { if dragging != nil {
if child, ok := dragging.element.(tomo.MouseTarget); ok { if child, ok := dragging.element.(ability.MouseTarget); ok {
child.HandleMouseUp ( child.HandleMouseUp (
image.Pt ( image.Pt (
int(buttonEvent.EventX), int(buttonEvent.EventX),
@ -237,7 +238,7 @@ func (window *window) handleButtonRelease (
input.Button(buttonEvent.Detail), input.Button(buttonEvent.Detail),
modifiers) modifiers)
} }
callback := func (container tomo.MouseTargetContainer, child tomo.Element) { callback := func (container ability.MouseTargetContainer, child tomo.Element) {
container.HandleChildMouseUp ( container.HandleChildMouseUp (
image.Pt ( image.Pt (
int(buttonEvent.EventX), int(buttonEvent.EventX),
@ -262,7 +263,7 @@ func (window *window) handleMotionNotify (
handled := false handled := false
for _, child := range window.system.drags { for _, child := range window.system.drags {
if child == nil { continue } 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)) child.HandleMotion(image.Pt(x, y))
handled = true handled = true
} }
@ -270,7 +271,7 @@ func (window *window) handleMotionNotify (
if !handled { if !handled {
child := window.system.childAt(image.Pt(x, y)) 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)) child.HandleMotion(image.Pt(x, y))
} }
} }

View File

@ -1,10 +1,8 @@
package x package x
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/ability"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type entitySet map[*entity] struct { } type entitySet map[*entity] struct { }
@ -24,10 +22,7 @@ func (set entitySet) Add (entity *entity) {
type system struct { type system struct {
child *entity child *entity
focused *entity focused *entity
canvas canvas.BasicCanvas canvas artist.BasicCanvas
theme theme.Wrapped
config config.Wrapped
invalidateIgnore bool invalidateIgnore bool
drawingInvalid entitySet drawingInvalid entitySet
@ -42,21 +37,19 @@ func (system *system) initialize () {
system.drawingInvalid = make(entitySet) system.drawingInvalid = make(entitySet)
} }
func (system *system) SetTheme (theme tomo.Theme) { func (system *system) handleThemeChange () {
system.theme.Theme = theme
system.propagate (func (entity *entity) bool { system.propagate (func (entity *entity) bool {
if child, ok := system.child.element.(tomo.Themeable); ok { if child, ok := system.child.element.(ability.Themeable); ok {
child.SetTheme(theme) child.HandleThemeChange()
} }
return true return true
}) })
} }
func (system *system) SetConfig (config tomo.Config) { func (system *system) handleConfigChange () {
system.config.Config = config
system.propagate (func (entity *entity) bool { system.propagate (func (entity *entity) bool {
if child, ok := system.child.element.(tomo.Configurable); ok { if child, ok := system.child.element.(ability.Configurable); ok {
child.SetConfig(config) child.HandleConfigChange()
} }
return true return true
}) })
@ -66,10 +59,10 @@ func (system *system) focus (entity *entity) {
previous := system.focused previous := system.focused
system.focused = entity system.focused = entity
if previous != nil { if previous != nil {
previous.element.(tomo.Focusable).HandleFocusChange() previous.element.(ability.Focusable).HandleFocusChange()
} }
if entity != nil { 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 { system.propagateAlt (func (entity *entity) bool {
if found { if found {
// looking for the next element to select // looking for the next element to select
child, ok := entity.element.(tomo.Focusable) child, ok := entity.element.(ability.Focusable)
if ok && child.Enabled() { if ok && child.Enabled() {
// found it // found it
entity.Focus() entity.Focus()
@ -106,7 +99,7 @@ func (system *system) focusPrevious () {
return false return false
} }
child, ok := entity.element.(tomo.Focusable) child, ok := entity.element.(ability.Focusable)
if ok && child.Enabled() { behind = entity } if ok && child.Enabled() { behind = entity }
return true return true
}) })
@ -137,9 +130,7 @@ func (system *system) resizeChildToFit () {
system.child.bounds = system.canvas.Bounds() system.child.bounds = system.canvas.Bounds()
system.child.clippedBounds = system.child.bounds system.child.clippedBounds = system.child.bounds
system.child.Invalidate() system.child.Invalidate()
if system.child.isContainer {
system.child.InvalidateLayout() system.child.InvalidateLayout()
}
} }
func (system *system) afterEvent () { func (system *system) afterEvent () {
@ -153,7 +144,7 @@ func (system *system) afterEvent () {
func (system *system) layout (entity *entity, force bool) { func (system *system) layout (entity *entity, force bool) {
if entity == nil { return } if entity == nil { return }
if entity.layoutInvalid == true || force { if entity.layoutInvalid == true || force {
if element, ok := entity.element.(tomo.Layoutable); ok { if element, ok := entity.element.(ability.Layoutable); ok {
element.Layout() element.Layout()
entity.layoutInvalid = false entity.layoutInvalid = false
force = true force = true
@ -176,7 +167,7 @@ func (system *system) draw () {
for entity := range system.drawingInvalid { for entity := range system.drawingInvalid {
if entity.clippedBounds.Empty() { continue } if entity.clippedBounds.Empty() { continue }
entity.element.Draw (canvas.Cut ( entity.element.Draw (artist.Cut (
system.canvas, system.canvas,
entity.clippedBounds)) entity.clippedBounds))
finalBounds = finalBounds.Union(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 "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist"
type mainWindow struct { *window } type mainWindow struct { *window }
type menuWindow struct { *window } type menuWindow struct { *window }
type window struct { type window struct {
system system
backend *Backend backend *backend
xWindow *xwindow.Window xWindow *xwindow.Window
xCanvas *xgraphics.Image xCanvas *xgraphics.Image
@ -40,7 +40,7 @@ type window struct {
onClose func () onClose func ()
} }
func (backend *Backend) NewWindow ( func (backend *backend) NewWindow (
bounds image.Rectangle, bounds image.Rectangle,
) ( ) (
output tomo.MainWindow, output tomo.MainWindow,
@ -53,7 +53,7 @@ func (backend *Backend) NewWindow (
return output, err return output, err
} }
func (backend *Backend) newWindow ( func (backend *backend) newWindow (
bounds image.Rectangle, bounds image.Rectangle,
override bool, override bool,
) ( ) (
@ -67,7 +67,6 @@ func (backend *Backend) newWindow (
window.system.initialize() window.system.initialize()
window.system.pushFunc = window.pasteAndPush window.system.pushFunc = window.pasteAndPush
window.theme.Case = tomo.C("tomo", "window")
window.xWindow, err = xwindow.Generate(backend.connection) window.xWindow, err = xwindow.Generate(backend.connection)
if err != nil { return } if err != nil { return }
@ -122,9 +121,6 @@ func (backend *Backend) newWindow (
xevent.SelectionRequestFun(window.handleSelectionRequest). xevent.SelectionRequestFun(window.handleSelectionRequest).
Connect(backend.connection, window.xWindow.Id) Connect(backend.connection, window.xWindow.Id)
window.SetTheme(backend.theme)
window.SetConfig(backend.config)
window.metrics.bounds = bounds window.metrics.bounds = bounds
window.setMinimumSize(8, 8) window.setMinimumSize(8, 8)
@ -418,7 +414,7 @@ func (window *window) pasteAndPush (region image.Rectangle) {
} }
func (window *window) paste (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() data, stride := canvas.Buffer()
bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds()) bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds())

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