diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build diff --git a/README.md b/README.md index 719cabf..9f4ec00 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,19 @@ Please note: Tomo is in early development. Some features may not work properly, and its API may change without notice. Tomo is a GUI toolkit written in pure Go with minimal external dependencies. It -makes use of Go's unique language features to do more with less. It is also -easily extendable with custom backends and elements. +makes use of Go's unique language features to do more with less. -You can find out more about how to use it by visiting the examples directory, -or pull up its documentation by running `godoc` within the repository. You can -also view it on the web on - [pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/sashakoshka/tomo) (although +Nasin is an application framework that runs on top of Tomo. It supports plugins +which can extend any application with backends, themes, etc. + +## Usage + +Before you start using Tomo, you need to install a backend plugin. Currently, +there is only an X backend. You can run ./scripts/install-backends.sh to install +it. It will be placed in `~/.local/lib/nasin/plugins`. + +You can find out more about how to use Tomo and Nasin by visiting the examples +directory, or pull up the documentation by running `godoc` within the +repository. You can also view it on the web on +[pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/sashakoshka/tomo) (although it may be slightly out of date). diff --git a/ability/element.go b/ability/element.go new file mode 100644 index 0000000..3f55e60 --- /dev/null +++ b/ability/element.go @@ -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 () +} diff --git a/artist/artutil/util.go b/artist/artutil/util.go new file mode 100644 index 0000000..68d115c --- /dev/null +++ b/artist/artutil/util.go @@ -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 +} diff --git a/canvas/canvas.go b/artist/canvas.go similarity index 99% rename from canvas/canvas.go rename to artist/canvas.go index 1661b9c..96296f1 100644 --- a/canvas/canvas.go +++ b/artist/canvas.go @@ -1,4 +1,4 @@ -package canvas +package artist import "image" import "image/draw" diff --git a/artist/color.go b/artist/color.go deleted file mode 100644 index 1729447..0000000 --- a/artist/color.go +++ /dev/null @@ -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 -} diff --git a/artist/icon.go b/artist/icon.go index 399d1c9..9e36d47 100644 --- a/artist/icon.go +++ b/artist/icon.go @@ -2,12 +2,11 @@ package artist import "image" import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/canvas" type Icon interface { // Draw draws the icon to the destination canvas at the specified point, // using the specified color (if the icon is monochrome). - Draw (destination canvas.Canvas, color color.RGBA, at image.Point) + Draw (destination Canvas, color color.RGBA, at image.Point) // Bounds returns the bounds of the icon. Bounds () image.Rectangle diff --git a/artist/pattern.go b/artist/pattern.go index c2caa5c..0e428bb 100644 --- a/artist/pattern.go +++ b/artist/pattern.go @@ -1,8 +1,6 @@ package artist import "image" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/shatter" // Pattern is capable of drawing to a canvas within the bounds of a given // clipping rectangle. @@ -11,51 +9,5 @@ type Pattern interface { // specified bounds. The given bounds can be smaller or larger than the // bounds of the destination canvas. The destination canvas can be cut // using canvas.Cut() to draw only a specific subset of a pattern. - Draw (destination canvas.Canvas, bounds image.Rectangle) + Draw (destination Canvas, bounds image.Rectangle) } - -// Fill fills the destination canvas with the given pattern. -func Fill (destination canvas.Canvas, source Pattern) (updated image.Rectangle) { - source.Draw(destination, destination.Bounds()) - return destination.Bounds() -} - -// DrawClip lets you draw several subsets of a pattern at once. -func DrawClip ( - destination canvas.Canvas, - source Pattern, - bounds image.Rectangle, - subsets ...image.Rectangle, -) ( - updatedRegion image.Rectangle, -) { - for _, subset := range subsets { - source.Draw(canvas.Cut(destination, subset), bounds) - updatedRegion = updatedRegion.Union(subset) - } - return -} - -// DrawShatter is like an inverse of DrawClip, drawing nothing in the areas -// specified by "rocks". -func DrawShatter ( - destination canvas.Canvas, - source Pattern, - bounds image.Rectangle, - rocks ...image.Rectangle, -) ( - updatedRegion image.Rectangle, -) { - tiles := shatter.Shatter(bounds, rocks...) - return DrawClip(destination, source, bounds, tiles...) -} - -// AllocateSample returns a new canvas containing the result of a pattern. The -// resulting canvas can be sourced from shape drawing functions. I beg of you -// please do not call this every time you need to draw a shape with a pattern on -// it because that is horrible and cruel to the computer. -func AllocateSample (source Pattern, width, height int) canvas.Canvas { - allocated := canvas.NewBasicCanvas(width, height) - Fill(allocated, source) - return allocated -} diff --git a/artist/patterns/border.go b/artist/patterns/border.go index eafda13..b86962f 100644 --- a/artist/patterns/border.go +++ b/artist/patterns/border.go @@ -1,7 +1,6 @@ package patterns import "image" -import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" // Border is a pattern that behaves similarly to border-image in CSS. It divides @@ -31,20 +30,20 @@ import "git.tebibyte.media/sashakoshka/tomo/artist" // This pattern can be used to make a static image texture into something that // responds well to being resized. type Border struct { - canvas.Canvas + artist.Canvas artist.Inset } // Draw draws the border pattern onto the destination canvas within the given // bounds. -func (pattern Border) Draw (destination canvas.Canvas, bounds image.Rectangle) { +func (pattern Border) Draw (destination artist.Canvas, bounds image.Rectangle) { drawBounds := bounds.Canon().Intersect(destination.Bounds()) if drawBounds.Empty() { return } srcSections := nonasect(pattern.Bounds(), pattern.Inset) srcTextures := [9]Texture { } for index, section := range srcSections { - srcTextures[index].Canvas = canvas.Cut(pattern, section) + srcTextures[index].Canvas = artist.Cut(pattern, section) } dstSections := nonasect(bounds, pattern.Inset) diff --git a/artist/patterns/texture.go b/artist/patterns/texture.go index 395942b..72869a7 100644 --- a/artist/patterns/texture.go +++ b/artist/patterns/texture.go @@ -1,18 +1,18 @@ package patterns import "image" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" // Texture is a pattern that tiles the content of a canvas both horizontally and // vertically. type Texture struct { - canvas.Canvas + artist.Canvas } // Draw tiles the pattern's canvas within the given bounds. The minimum // point of the pattern's canvas will be lined up with the minimum point of the // bounding rectangle. -func (pattern Texture) Draw (destination canvas.Canvas, bounds image.Rectangle) { +func (pattern Texture) Draw (destination artist.Canvas, bounds image.Rectangle) { dstBounds := bounds.Canon().Intersect(destination.Bounds()) if dstBounds.Empty() { return } diff --git a/artist/patterns/uniform.go b/artist/patterns/uniform.go index 173367a..06b9bbb 100644 --- a/artist/patterns/uniform.go +++ b/artist/patterns/uniform.go @@ -2,19 +2,19 @@ package patterns import "image" import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" +import "git.tebibyte.media/sashakoshka/tomo/artist/artutil" // Uniform is a pattern that draws a solid color. type Uniform color.RGBA // Draw fills the bounding rectangle with the pattern's color. -func (pattern Uniform) Draw (destination canvas.Canvas, bounds image.Rectangle) { +func (pattern Uniform) Draw (destination artist.Canvas, bounds image.Rectangle) { shapes.FillColorRectangle(destination, color.RGBA(pattern), bounds) } // Uhex creates a new Uniform pattern from an RGBA integer value. func Uhex (color uint32) (uniform Uniform) { - return Uniform(artist.Hex(color)) + return Uniform(artutil.Hex(color)) } diff --git a/artist/shapes/ellipse.go b/artist/shapes/ellipse.go index c30041d..f5fae0d 100644 --- a/artist/shapes/ellipse.go +++ b/artist/shapes/ellipse.go @@ -3,7 +3,7 @@ package shapes import "math" import "image" import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" // TODO: redo fill ellipse, stroke ellipse, etc. so that it only takes in // destination and source, using the bounds of destination as the bounds of the @@ -11,8 +11,8 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" // of both canvases. func FillEllipse ( - destination canvas.Canvas, - source canvas.Canvas, + destination artist.Canvas, + source artist.Canvas, bounds image.Rectangle, ) ( updatedRegion image.Rectangle, @@ -42,8 +42,8 @@ func FillEllipse ( } func StrokeEllipse ( - destination canvas.Canvas, - source canvas.Canvas, + destination artist.Canvas, + source artist.Canvas, bounds image.Rectangle, weight int, ) { @@ -170,7 +170,7 @@ func (context ellipsePlottingContext) plotEllipse () { // FillColorEllipse fills an ellipse within the destination canvas with a solid // color. func FillColorEllipse ( - destination canvas.Canvas, + destination artist.Canvas, color color.RGBA, bounds image.Rectangle, ) ( @@ -196,7 +196,7 @@ func FillColorEllipse ( // StrokeColorEllipse is similar to FillColorEllipse, but it draws an inset // outline of an ellipse instead. func StrokeColorEllipse ( - destination canvas.Canvas, + destination artist.Canvas, color color.RGBA, bounds image.Rectangle, weight int, diff --git a/artist/shapes/line.go b/artist/shapes/line.go index 1d81f94..4593e4b 100644 --- a/artist/shapes/line.go +++ b/artist/shapes/line.go @@ -2,12 +2,12 @@ package shapes import "image" import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" // ColorLine draws a line from one point to another with the specified weight // and color. func ColorLine ( - destination canvas.Canvas, + destination artist.Canvas, color color.RGBA, weight int, min image.Point, diff --git a/artist/shapes/rectangle.go b/artist/shapes/rectangle.go index 968e00c..6282233 100644 --- a/artist/shapes/rectangle.go +++ b/artist/shapes/rectangle.go @@ -2,14 +2,14 @@ package shapes import "image" import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/shatter" // TODO: return updatedRegion for all routines in this package func FillRectangle ( - destination canvas.Canvas, - source canvas.Canvas, + destination artist.Canvas, + source artist.Canvas, bounds image.Rectangle, ) ( updatedRegion image.Rectangle, @@ -38,8 +38,8 @@ func FillRectangle ( } func StrokeRectangle ( - destination canvas.Canvas, - source canvas.Canvas, + destination artist.Canvas, + source artist.Canvas, bounds image.Rectangle, weight int, ) ( @@ -55,8 +55,8 @@ func StrokeRectangle ( // FillRectangleShatter is like FillRectangle, but it does not draw in areas // specified in "rocks". func FillRectangleShatter ( - destination canvas.Canvas, - source canvas.Canvas, + destination artist.Canvas, + source artist.Canvas, bounds image.Rectangle, rocks ...image.Rectangle, ) ( @@ -65,7 +65,7 @@ func FillRectangleShatter ( tiles := shatter.Shatter(bounds, rocks...) for _, tile := range tiles { FillRectangle ( - canvas.Cut(destination, tile), + artist.Cut(destination, tile), source, tile) updatedRegion = updatedRegion.Union(tile) } @@ -75,7 +75,7 @@ func FillRectangleShatter ( // FillColorRectangle fills a rectangle within the destination canvas with a // solid color. func FillColorRectangle ( - destination canvas.Canvas, + destination artist.Canvas, color color.RGBA, bounds image.Rectangle, ) ( @@ -97,7 +97,7 @@ func FillColorRectangle ( // FillColorRectangleShatter is like FillColorRectangle, but it does not draw in // areas specified in "rocks". func FillColorRectangleShatter ( - destination canvas.Canvas, + destination artist.Canvas, color color.RGBA, bounds image.Rectangle, rocks ...image.Rectangle, @@ -115,7 +115,7 @@ func FillColorRectangleShatter ( // StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset // outline of the given rectangle instead. func StrokeColorRectangle ( - destination canvas.Canvas, + destination artist.Canvas, color color.RGBA, bounds image.Rectangle, weight int, diff --git a/backend.go b/backend.go index 238a877..b7d1bee 100644 --- a/backend.go +++ b/backend.go @@ -1,7 +1,6 @@ package tomo import "image" -import "errors" // Backend represents a connection to a display server, or something similar. // It is capable of managing an event loop, and creating windows. @@ -32,33 +31,21 @@ type Backend interface { SetConfig (Config) } -// BackendFactory represents a function capable of constructing a backend -// struct. Any connections should be initialized within this function. If there -// any errors encountered during this process, the function should immediately -// stop, clean up any resources, and return an error. -type BackendFactory func () (backend Backend, err error) +var backend Backend -// RegisterBackend registers a backend factory. When an application calls -// tomo.Run(), the first registered backend that does not throw an error will be -// used. -func RegisterBackend (factory BackendFactory) { - factories = append(factories, factory) +// GetBackend returns the currently running backend. +func GetBackend () Backend { + return backend } -var factories []BackendFactory - -func instantiateBackend () (backend Backend, err error) { - // find a suitable backend - for _, factory := range factories { - backend, err = factory() - if err == nil && backend != nil { return } - } - - // if none were found, but there was no error produced, produce an - // error - if err == nil { - err = errors.New("no available backends") - } - - return +// SetBackend sets the currently running backend. The backend can only be set +// onceā€”if there already is one then this function will do nothing. +func SetBackend (b Backend) { + if backend != nil { return } + backend = b +} + +// Bounds creates a rectangle from an x, y, width, and height. +func Bounds (x, y, width, height int) image.Rectangle { + return image.Rect(x, y, x + width, y + height) } diff --git a/backends/all/all.go b/backends/all/all.go deleted file mode 100644 index fcb293b..0000000 --- a/backends/all/all.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package all links most common backends. -package all - -import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" diff --git a/backends/doc.go b/backends/doc.go deleted file mode 100644 index d32bd15..0000000 --- a/backends/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package backends contains sub-packages that register backends with tomo when -// linked into a program. -package backends diff --git a/canvas/doc.go b/canvas/doc.go deleted file mode 100644 index 084d0cb..0000000 --- a/canvas/doc.go +++ /dev/null @@ -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 diff --git a/default/theme/assets/default.png b/default/theme/assets/default.png new file mode 100644 index 0000000..227331a Binary files /dev/null and b/default/theme/assets/default.png differ diff --git a/default/theme/default.go b/default/theme/default.go index 3c9f750..efa3000 100644 --- a/default/theme/default.go +++ b/default/theme/default.go @@ -9,14 +9,14 @@ import "golang.org/x/image/font" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist/artutil" import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font" import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" -//go:embed assets/wintergreen.png +//go:embed assets/default.png var defaultAtlasBytes []byte -var defaultAtlas canvas.Canvas -var defaultTextures [17][9]artist.Pattern +var defaultAtlas artist.Canvas +var defaultTextures [7][7]artist.Pattern //go:embed assets/wintergreen-icons-small.png var defaultIconsSmallAtlasBytes []byte var defaultIconsSmall [640]binaryIcon @@ -25,9 +25,9 @@ var defaultIconsLargeAtlasBytes []byte var defaultIconsLarge [640]binaryIcon func atlasCell (col, row int, border artist.Inset) { - bounds := image.Rect(0, 0, 16, 16).Add(image.Pt(col, row).Mul(16)) + bounds := image.Rect(0, 0, 8, 8).Add(image.Pt(col, row).Mul(8)) defaultTextures[col][row] = patterns.Border { - Canvas: canvas.Cut(defaultAtlas, bounds), + Canvas: artist.Cut(defaultAtlas, bounds), Inset: border, } } @@ -43,7 +43,7 @@ type binaryIcon struct { stride int } -func (icon binaryIcon) Draw (destination canvas.Canvas, color color.RGBA, at image.Point) { +func (icon binaryIcon) Draw (destination artist.Canvas, color color.RGBA, at image.Point) { bounds := icon.Bounds().Add(at).Intersect(destination.Bounds()) point := image.Point { } data, stride := destination.Buffer() @@ -85,43 +85,15 @@ func binaryIconFrom (source image.Image, clip image.Rectangle) (icon binaryIcon) func init () { defaultAtlasImage, _, _ := image.Decode(bytes.NewReader(defaultAtlasBytes)) - defaultAtlas = canvas.FromImage(defaultAtlasImage) + defaultAtlas = artist.FromImage(defaultAtlasImage) - // PatternDead - atlasCol(0, artist.Inset { }) - // PatternRaised - atlasCol(1, artist.Inset { 6, 6, 6, 6 }) - // PatternSunken - atlasCol(2, artist.Inset { 4, 4, 4, 4 }) - // PatternPinboard - atlasCol(3, artist.Inset { 2, 2, 2, 2 }) - // PatternButton - atlasCol(4, artist.Inset { 6, 6, 6, 6 }) - // PatternInput - atlasCol(5, artist.Inset { 4, 4, 4, 4 }) - // PatternGutter - atlasCol(6, artist.Inset { 7, 7, 7, 7 }) - // PatternHandle - atlasCol(7, artist.Inset { 3, 3, 3, 3 }) - // PatternLine - atlasCol(8, artist.Inset { 1, 1, 1, 1 }) - // PatternMercury - atlasCol(13, artist.Inset { 2, 2, 2, 2 }) - // PatternTableHead: - atlasCol(14, artist.Inset { 4, 4, 4, 4 }) - // PatternTableCell: - atlasCol(15, artist.Inset { 4, 4, 4, 4 }) - // PatternLamp: - atlasCol(16, artist.Inset { 4, 3, 4, 3 }) - - // PatternButton: basic.checkbox - atlasCol(9, artist.Inset { 3, 3, 3, 3 }) - // PatternRaised: basic.listEntry - atlasCol(10, artist.Inset { 3, 3, 3, 3 }) - // PatternRaised: fun.flatKey - atlasCol(11, artist.Inset { 3, 3, 5, 3 }) - // PatternRaised: fun.sharpKey - atlasCol(12, artist.Inset { 3, 3, 4, 3 }) + atlasCol(0, artist.I(0)) + atlasCol(1, artist.I(3)) + atlasCol(2, artist.I(1)) + atlasCol(3, artist.I(1)) + atlasCol(4, artist.I(1)) + atlasCol(5, artist.I(3)) + atlasCol(6, artist.I(1)) // set up small icons defaultIconsSmallAtlasImage, _, _ := image.Decode ( @@ -217,33 +189,23 @@ func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.P case tomo.PatternRaised: return defaultTextures[1][offset] case tomo.PatternSunken: return defaultTextures[2][offset] case tomo.PatternPinboard: return defaultTextures[3][offset] - case tomo.PatternButton: - switch { - case c.Match("tomo", "checkbox", ""): - return defaultTextures[9][offset] - case c.Match("tomo", "piano", "flatKey"): - return defaultTextures[11][offset] - case c.Match("tomo", "piano", "sharpKey"): - return defaultTextures[12][offset] - default: - return defaultTextures[4][offset] - } - case tomo.PatternInput: return defaultTextures[5][offset] - case tomo.PatternGutter: return defaultTextures[6][offset] - case tomo.PatternHandle: return defaultTextures[7][offset] - case tomo.PatternLine: return defaultTextures[8][offset] - case tomo.PatternMercury: return defaultTextures[13][offset] - case tomo.PatternTableHead: return defaultTextures[14][offset] - case tomo.PatternTableCell: return defaultTextures[15][offset] - case tomo.PatternLamp: return defaultTextures[16][offset] - default: return patterns.Uhex(0xFF00FFFF) + case tomo.PatternButton: return defaultTextures[1][offset] + case tomo.PatternInput: return defaultTextures[2][offset] + case tomo.PatternGutter: return defaultTextures[2][offset] + case tomo.PatternHandle: return defaultTextures[3][offset] + case tomo.PatternLine: return defaultTextures[0][offset] + case tomo.PatternMercury: return defaultTextures[4][offset] + case tomo.PatternTableHead: return defaultTextures[5][offset] + case tomo.PatternTableCell: return defaultTextures[5][offset] + case tomo.PatternLamp: return defaultTextures[6][offset] + default: return patterns.Uhex(0xFF00FFFF) } } func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA { - if state.Disabled { return artist.Hex(0x444444FF) } + if state.Disabled { return artutil.Hex(0x444444FF) } - return artist.Hex (map[tomo.Color] uint32 { + return artutil.Hex (map[tomo.Color] uint32 { tomo.ColorBlack: 0x272d24FF, tomo.ColorRed: 0x8c4230FF, tomo.ColorGreen: 0x69905fFF, @@ -262,56 +224,26 @@ func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA { tomo.ColorBrightWhite: 0xcfd7d2FF, tomo.ColorForeground: 0x000000FF, - tomo.ColorMidground: 0x97A09BFF, + tomo.ColorMidground: 0x656565FF, tomo.ColorBackground: 0xAAAAAAFF, - tomo.ColorShadow: 0x445754FF, - tomo.ColorShine: 0xCFD7D2FF, - tomo.ColorAccent: 0x408090FF, + tomo.ColorShadow: 0x000000FF, + tomo.ColorShine: 0xFFFFFFFF, + tomo.ColorAccent: 0xff3300FF, } [id]) } // Padding returns the default padding value for the given pattern. func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset { switch id { - case tomo.PatternSunken: - if c.Match("tomo", "progressBar", "") { - return artist.I(2, 1, 1, 2) - } else if c.Match("tomo", "list", "") { - return artist.I(2) - } else if c.Match("tomo", "flowList", "") { - return artist.I(2) - } else { - return artist.I(8) - } - case tomo.PatternPinboard: - if c.Match("tomo", "piano", "") { - return artist.I(2) - } else { - return artist.I(8) - } - case tomo.PatternTableCell: return artist.I(5) - case tomo.PatternTableHead: return artist.I(5) - case tomo.PatternGutter: return artist.I(0) - case tomo.PatternLine: return artist.I(1) - case tomo.PatternMercury: return artist.I(5) - case tomo.PatternLamp: return artist.I(5, 5, 5, 6) - default: return artist.I(8) + case tomo.PatternGutter: return artist.I(0) + case tomo.PatternLine: return artist.I(1) + default: return artist.I(6) } } // Margin returns the default margin value for the given pattern. func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point { - switch id { - case tomo.PatternSunken: - if c.Match("tomo", "list", "") { - return image.Pt(-1, -1) - } else if c.Match("tomo", "flowList", "") { - return image.Pt(-1, -1) - } else { - return image.Pt(8, 8) - } - default: return image.Pt(8, 8) - } + return image.Pt(6, 6) } // Hints returns rendering optimization hints for a particular pattern. diff --git a/default/theme/parse.go b/default/theme/parse.go deleted file mode 100644 index 990246d..0000000 --- a/default/theme/parse.go +++ /dev/null @@ -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 { } -// } diff --git a/default/theme/wrapped.go b/default/theme/wrapped.go deleted file mode 100644 index 24a72fd..0000000 --- a/default/theme/wrapped.go +++ /dev/null @@ -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 -} diff --git a/element.go b/element.go index a92a4f7..6a8d72d 100644 --- a/element.go +++ b/element.go @@ -1,251 +1,15 @@ package tomo -import "image" -import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" -// Element represents a basic on-screen object. +// Element represents a basic on-screen object. Extended element interfaces are +// defined in the ability module. type Element interface { // Draw causes the element to draw to the specified canvas. The bounds // of this canvas specify the area that is actually drawn to, while the // Entity bounds specify the actual area of the element. - Draw (canvas.Canvas) + Draw (artist.Canvas) // Entity returns this element's entity. Entity () Entity } - -// Layoutable represents an element that needs to perform layout calculations -// before it can draw itself. -type Layoutable interface { - Element - - // Layout causes this element to perform a layout operation. - Layout () -} - -// Container represents an element capable of containing child elements. -type Container interface { - Element - Layoutable - - // DrawBackground causes the element to draw its background pattern to - // the specified canvas. The bounds of this canvas specify the area that - // is actually drawn to, while the Entity bounds specify the actual area - // of the element. - DrawBackground (canvas.Canvas) - - // HandleChildMinimumSizeChange is called when a child's minimum size is - // changed. - HandleChildMinimumSizeChange (child Element) -} - -// Enableable represents an element that can be enabled and disabled. Disabled -// elements typically appear greyed out. -type Enableable interface { - Element - - // Enabled returns whether or not the element is enabled. - Enabled () bool - - // SetEnabled sets whether or not the element is enabled. - SetEnabled (bool) -} - -// Focusable represents an element that has keyboard navigation support. -type Focusable interface { - Element - Enableable - - // HandleFocusChange is called when the element is focused or unfocused. - HandleFocusChange () -} - -// Selectable represents an element that can be selected. This includes things -// like list items, files, etc. The difference between this and Focusable is -// that multiple Selectable elements may be selected at the same time, whereas -// only one Focusable element may be focused at the same time. Containers who's -// purpose is to contain selectable elements can determine when to select them -// by implementing MouseTargetContainer and listening for HandleChildMouseDown -// events. -type Selectable interface { - Element - Enableable - - // HandleSelectionChange is called when the element is selected or - // deselected. - HandleSelectionChange () -} - -// KeyboardTarget represents an element that can receive keyboard input. -type KeyboardTarget interface { - Element - - // HandleKeyDown is called when a key is pressed down or repeated while - // this element has keyboard focus. It is important to note that not - // every key down event is guaranteed to be paired with exactly one key - // up event. This is the reason a list of modifier keys held down at the - // time of the key press is given. - HandleKeyDown (key input.Key, modifiers input.Modifiers) - - // HandleKeyUp is called when a key is released while this element has - // keyboard focus. - HandleKeyUp (key input.Key, modifiers input.Modifiers) -} - -// MouseTarget represents an element that can receive mouse events. -type MouseTarget interface { - Element - - // HandleMouseDown is called when a mouse button is pressed down on this - // element. - HandleMouseDown ( - position image.Point, - button input.Button, - modifiers input.Modifiers) - - // HandleMouseUp is called when a mouse button is released that was - // originally pressed down on this element. - HandleMouseUp ( - position image.Point, - button input.Button, - modifiers input.Modifiers) -} - -// MouseTargetContainer represents an element that wants to know when one -// of its children is clicked. Children do not have to implement MouseTarget for -// a container satisfying MouseTargetContainer to be notified that they have -// been clicked. -type MouseTargetContainer interface { - Container - - // HandleMouseDown is called when a mouse button is pressed down on a - // child element. - HandleChildMouseDown ( - position image.Point, - button input.Button, - modifiers input.Modifiers, - child Element) - - // HandleMouseUp is called when a mouse button is released that was - // originally pressed down on a child element. - HandleChildMouseUp ( - position image.Point, - button input.Button, - modifiers input.Modifiers, - child Element) -} - -// MotionTarget represents an element that can receive mouse motion events. -type MotionTarget interface { - Element - - // HandleMotion is called when the mouse is moved over this element, - // or the mouse is moving while being held down and originally pressed - // down on this element. - HandleMotion (position image.Point) -} - -// ScrollTarget represents an element that can receive mouse scroll events. -type ScrollTarget interface { - Element - - // HandleScroll is called when the mouse is scrolled. The X and Y - // direction of the scroll event are passed as deltaX and deltaY. - HandleScroll ( - position image.Point, - deltaX, deltaY float64, - modifiers input.Modifiers) -} - -// Flexible represents an element who's preferred minimum height can change in -// response to its width. -type Flexible interface { - Element - - // FlexibleHeightFor returns what the element's minimum height would be - // if resized to a specified width. This does not actually alter the - // state of the element in any way, but it may perform significant work, - // so it should be called sparingly. - // - // It is reccomended that parent containers check for this interface and - // take this method's value into account in order to support things like - // flow layouts and text wrapping, but it is not absolutely necessary. - // The element's MinimumSize method will still return the absolute - // minimum size that the element may be resized to. - // - // It is important to note that if a parent container checks for - // flexible chilren, it itself will likely need to be either scrollable - // or flexible. - FlexibleHeightFor (width int) int -} - -// FlexibleContainer represents an element that is capable of containing -// flexible children. -type FlexibleContainer interface { - Container - - // HandleChildFlexibleHeightChange is called when the parameters - // affecting a child's flexible height are changed. - HandleChildFlexibleHeightChange (child Flexible) -} - -// Scrollable represents an element that can be scrolled. It acts as a viewport -// through which its contents can be observed. -type Scrollable interface { - Element - - // ScrollContentBounds returns the full content size of the element. - ScrollContentBounds () image.Rectangle - - // ScrollViewportBounds returns the size and position of the element's - // viewport relative to ScrollBounds. - ScrollViewportBounds () image.Rectangle - - // ScrollTo scrolls the viewport to the specified point relative to - // ScrollBounds. - ScrollTo (position image.Point) - - // ScrollAxes returns the supported axes for scrolling. - ScrollAxes () (horizontal, vertical bool) -} - -// ScrollableContainer represents an element that is capable of containing -// scrollable children. -type ScrollableContainer interface { - Container - - // HandleChildScrollBoundsChange is called when the content bounds, - // viewport bounds, or scroll axes of a child are changed. - HandleChildScrollBoundsChange (child Scrollable) -} - -// Collapsible represents an element who's minimum width and height can be -// manually resized. Scrollable elements should implement this if possible. -type Collapsible interface { - Element - - // Collapse collapses the element's minimum width and height. A value of - // zero for either means that the element's normal value is used. - Collapse (width, height int) -} - -// Themeable represents an element that can modify its appearance to fit within -// a theme. -type Themeable interface { - Element - - // SetTheme sets the element's theme to something fulfilling the - // theme.Theme interface. - SetTheme (Theme) -} - -// Configurable represents an element that can modify its behavior to fit within -// a set of configuration parameters. -type Configurable interface { - Element - - // SetConfig sets the element's configuration to something fulfilling - // the config.Config interface. - SetConfig (Config) -} diff --git a/elements/box.go b/elements/box.go index cdfcc21..63677b2 100644 --- a/elements/box.go +++ b/elements/box.go @@ -2,9 +2,10 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/shatter" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +var boxCase = tomo.C("tomo", "box") // Space is a list of spacing configurations that can be passed to some // containers. @@ -27,7 +28,6 @@ func (space Space) Includes (sub Space) bool { // complex layouts. type Box struct { container - theme theme.Wrapped padding bool margin bool vertical bool @@ -39,10 +39,9 @@ func NewHBox (space Space, children ...tomo.Element) (element *Box) { padding: space.Includes(SpacePadding), margin: space.Includes(SpaceMargin), } - element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.entity = tomo.GetBackend().NewEntity(element) element.minimumSize = element.updateMinimumSize element.init() - element.theme.Case = tomo.C("tomo", "box") element.Adopt(children...) return } @@ -54,16 +53,15 @@ func NewVBox (space Space, children ...tomo.Element) (element *Box) { margin: space.Includes(SpaceMargin), vertical: true, } - element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.entity = tomo.GetBackend().NewEntity(element) element.minimumSize = element.updateMinimumSize element.init() - element.theme.Case = tomo.C("tomo", "box") element.Adopt(children...) return } // Draw causes the element to draw to the specified destination canvas. -func (element *Box) Draw (destination canvas.Canvas) { +func (element *Box) Draw (destination artist.Canvas) { rocks := make([]image.Rectangle, element.entity.CountChildren()) for index := 0; index < element.entity.CountChildren(); index ++ { rocks[index] = element.entity.Child(index).Entity().Bounds() @@ -71,14 +69,14 @@ func (element *Box) Draw (destination canvas.Canvas) { tiles := shatter.Shatter(element.entity.Bounds(), rocks...) for _, tile := range tiles { - element.entity.DrawBackground(canvas.Cut(destination, tile)) + element.entity.DrawBackground(artist.Cut(destination, tile)) } } // Layout causes this element to perform a layout operation. func (element *Box) Layout () { - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) + margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase) + padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase) bounds := element.entity.Bounds() if element.padding { bounds = padding.Apply(bounds) } @@ -128,22 +126,19 @@ func (element *Box) AdoptExpand (children ...tomo.Element) { // DrawBackground draws this element's background pattern to the specified // destination canvas. -func (element *Box) DrawBackground (destination canvas.Canvas) { +func (element *Box) DrawBackground (destination artist.Canvas) { element.entity.DrawBackground(destination) } -// SetTheme sets the element's theme. -func (element *Box) SetTheme (theme tomo.Theme) { - if theme == element.theme.Theme { return } - element.theme.Theme = theme +func (element *Box) HandleThemeChange () { element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() } func (element *Box) freeSpace () (space float64, nExpanding float64) { - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) + margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase) + padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase) var marginSize int; if element.vertical { marginSize = margin.Y @@ -176,8 +171,8 @@ func (element *Box) freeSpace () (space float64, nExpanding float64) { } func (element *Box) updateMinimumSize () { - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) + margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase) + padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase) var breadth, size int var marginSize int; if element.vertical { marginSize = margin.Y diff --git a/elements/button.go b/elements/button.go index 4968843..2aaa1c9 100644 --- a/elements/button.go +++ b/elements/button.go @@ -3,22 +3,19 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/textdraw" +var buttonCase = tomo.C("tomo", "button") + // Button is a clickable button. type Button struct { - entity tomo.FocusableEntity + entity tomo.Entity drawer textdraw.Drawer enabled bool pressed bool text string - - config config.Wrapped - theme theme.Wrapped showText bool hasIcon bool @@ -30,11 +27,11 @@ type Button struct { // NewButton creates a new button with the specified label text. func NewButton (text string) (element *Button) { element = &Button { showText: true, enabled: true } - element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) - element.theme.Case = tomo.C("tomo", "button") - element.drawer.SetFace (element.theme.FontFace ( + element.entity = tomo.GetBackend().NewEntity(element) + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) + tomo.FontSizeNormal, + buttonCase)) element.SetText(text) return } @@ -45,16 +42,16 @@ func (element *Button) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *Button) Draw (destination canvas.Canvas) { +func (element *Button) Draw (destination artist.Canvas) { state := element.state() bounds := element.entity.Bounds() - pattern := element.theme.Pattern(tomo.PatternButton, state) + pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, buttonCase) pattern.Draw(destination, bounds) - foreground := element.theme.Color(tomo.ColorForeground, state) - sink := element.theme.Sink(tomo.PatternButton) - margin := element.theme.Margin(tomo.PatternButton) + foreground := element.entity.Theme().Color(tomo.ColorForeground, state, buttonCase) + sink := element.entity.Theme().Sink(tomo.PatternButton, buttonCase) + margin := element.entity.Theme().Margin(tomo.PatternButton, buttonCase) offset := image.Pt ( bounds.Dx() / 2, @@ -69,7 +66,7 @@ func (element *Button) Draw (destination canvas.Canvas) { } if element.hasIcon { - icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) + icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase) if icon != nil { iconBounds := icon.Bounds() addedWidth := iconBounds.Dx() @@ -154,21 +151,11 @@ func (element *Button) ShowText (showText bool) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *Button) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawer.SetFace (element.theme.FontFace ( +func (element *Button) HandleThemeChange () { + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *Button) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new + tomo.FontSizeNormal, + buttonCase)) element.updateMinimumSize() element.entity.Invalidate() } @@ -223,14 +210,14 @@ func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) { } func (element *Button) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternButton) - margin := element.theme.Margin(tomo.PatternButton) + padding := element.entity.Theme().Padding(tomo.PatternButton, buttonCase) + margin := element.entity.Theme().Margin(tomo.PatternButton, buttonCase) textBounds := element.drawer.LayoutBounds() minimumSize := textBounds.Sub(textBounds.Min) if element.hasIcon { - icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) + icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase) if icon != nil { bounds := icon.Bounds() if element.showText { diff --git a/elements/cell.go b/elements/cell.go index f9da0db..2337d65 100644 --- a/elements/cell.go +++ b/elements/cell.go @@ -1,22 +1,17 @@ package elements import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" +import "git.tebibyte.media/sashakoshka/tomo/artist/artutil" -type cellEntity interface { - tomo.ContainerEntity - tomo.SelectableEntity -} +var cellCase = tomo.C("tomo", "cell") // Cell is a single-element container that satisfies tomo.Selectable. It // provides styling based on whether or not it is selected. type Cell struct { - entity cellEntity + entity tomo.Entity child tomo.Element enabled bool - theme theme.Wrapped onSelectionChange func () } @@ -26,8 +21,7 @@ type Cell struct { // method. func NewCell (child tomo.Element) (element *Cell) { element = &Cell { enabled: true } - element.theme.Case = tomo.C("tomo", "cell") - element.entity = tomo.NewEntity(element).(cellEntity) + element.entity = tomo.GetBackend().NewEntity(element) element.Adopt(child) return } @@ -38,13 +32,13 @@ func (element *Cell) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *Cell) Draw (destination canvas.Canvas) { +func (element *Cell) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() - pattern := element.theme.Pattern(tomo.PatternTableCell, element.state()) + pattern := element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase) if element.child == nil { pattern.Draw(destination, bounds) } else { - artist.DrawShatter ( + artutil.DrawShatter ( destination, pattern, bounds, element.child.Entity().Bounds()) } @@ -55,15 +49,15 @@ func (element *Cell) Layout () { if element.child == nil { return } bounds := element.entity.Bounds() - bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds) + bounds = element.entity.Theme().Padding(tomo.PatternTableCell, cellCase).Apply(bounds) element.entity.PlaceChild(0, bounds) } // DrawBackground draws this element's background pattern to the specified // destination canvas. -func (element *Cell) DrawBackground (destination canvas.Canvas) { - element.theme.Pattern(tomo.PatternTableCell, element.state()). +func (element *Cell) DrawBackground (destination artist.Canvas) { + element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase). Draw(destination, element.entity.Bounds()) } @@ -102,16 +96,6 @@ func (element *Cell) SetEnabled (enabled bool) { element.invalidateChild() } -// SetTheme sets this element's theme. -func (element *Cell) SetTheme (theme tomo.Theme) { - if theme == element.theme.Theme { return } - element.theme.Theme = theme - element.updateMinimumSize() - element.entity.Invalidate() - element.invalidateChild() - element.entity.InvalidateLayout() -} - // OnSelectionChange sets a function to be called when this element is selected // or unselected. func (element *Cell) OnSelectionChange (callback func ()) { @@ -122,6 +106,13 @@ func (element *Cell) Selected () bool { return element.entity.Selected() } +func (element *Cell) HandleThemeChange () { + element.updateMinimumSize() + element.entity.Invalidate() + element.invalidateChild() + element.entity.InvalidateLayout() +} + func (element *Cell) HandleSelectionChange () { element.entity.Invalidate() element.invalidateChild() @@ -151,7 +142,7 @@ func (element *Cell) updateMinimumSize () { width += childWidth height += childHeight } - padding := element.theme.Padding(tomo.PatternTableCell) + padding := element.entity.Theme().Padding(tomo.PatternTableCell, cellCase) width += padding.Horizontal() height += padding.Vertical() diff --git a/elements/checkbox.go b/elements/checkbox.go index 10a5124..53eb38d 100644 --- a/elements/checkbox.go +++ b/elements/checkbox.go @@ -3,14 +3,14 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" + +var checkboxCase = tomo.C("tomo", "checkbox") // Checkbox is a toggle-able checkbox with a label. type Checkbox struct { - entity tomo.FocusableEntity + entity tomo.Entity drawer textdraw.Drawer enabled bool @@ -18,20 +18,17 @@ type Checkbox struct { checked bool text string - config config.Wrapped - theme theme.Wrapped - onToggle func () } // NewCheckbox creates a new cbeckbox with the specified label text. func NewCheckbox (text string, checked bool) (element *Checkbox) { element = &Checkbox { checked: checked, enabled: true } - element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) - element.theme.Case = tomo.C("tomo", "checkbox") - element.drawer.SetFace (element.theme.FontFace ( + element.entity = tomo.GetBackend().NewEntity(element) + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) + tomo.FontSizeNormal, + checkboxCase)) element.SetText(text) return } @@ -42,7 +39,7 @@ func (element *Checkbox) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *Checkbox) Draw (destination canvas.Canvas) { +func (element *Checkbox) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) @@ -55,11 +52,11 @@ func (element *Checkbox) Draw (destination canvas.Canvas) { element.entity.DrawBackground(destination) - pattern := element.theme.Pattern(tomo.PatternButton, state) + pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, checkboxCase) pattern.Draw(destination, boxBounds) textBounds := element.drawer.LayoutBounds() - margin := element.theme.Margin(tomo.PatternBackground) + margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase) offset := bounds.Min.Add(image.Point { X: bounds.Dy() + margin.X, }) @@ -67,7 +64,7 @@ func (element *Checkbox) Draw (destination canvas.Canvas) { offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Color(tomo.ColorForeground, state) + foreground := element.entity.Theme().Color(tomo.ColorForeground, state, checkboxCase) element.drawer.Draw(destination, foreground, offset) } @@ -107,21 +104,11 @@ func (element *Checkbox) SetText (text string) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *Checkbox) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawer.SetFace (element.theme.FontFace ( +func (element *Checkbox) HandleThemeChange () { + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *Checkbox) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new + tomo.FontSizeNormal, + checkboxCase)) element.updateMinimumSize() element.entity.Invalidate() } @@ -183,7 +170,7 @@ func (element *Checkbox) updateMinimumSize () { if element.text == "" { element.entity.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) } else { - margin := element.theme.Margin(tomo.PatternBackground) + margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase) element.entity.SetMinimumSize ( textBounds.Dy() + margin.X + textBounds.Dx(), textBounds.Dy()) diff --git a/elements/combobox.go b/elements/combobox.go index aa87bc5..dbd66ca 100644 --- a/elements/combobox.go +++ b/elements/combobox.go @@ -3,11 +3,12 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/ability" import "git.tebibyte.media/sashakoshka/tomo/textdraw" +var comboBoxCase = tomo.C("tomo", "comboBox") + // Option specifies a ComboBox option. A blank option will display as "(None)". type Option string @@ -21,7 +22,7 @@ func (option Option) Title () string { // ComboBox is an input that can be one of several predetermined values. type ComboBox struct { - entity tomo.FocusableEntity + entity tomo.Entity drawer textdraw.Drawer options []Option @@ -30,9 +31,6 @@ type ComboBox struct { enabled bool pressed bool - config config.Wrapped - theme theme.Wrapped - onChange func () } @@ -40,11 +38,11 @@ type ComboBox struct { func NewComboBox (options ...Option) (element *ComboBox) { if len(options) == 0 { options = []Option { "" } } element = &ComboBox { enabled: true, options: options } - element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) - element.theme.Case = tomo.C("tomo", "comboBox") - element.drawer.SetFace (element.theme.FontFace ( + element.entity = tomo.GetBackend().NewEntity(element) + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) + tomo.FontSizeNormal, + comboBoxCase)) element.Select(options[0]) return } @@ -55,17 +53,17 @@ func (element *ComboBox) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *ComboBox) Draw (destination canvas.Canvas) { +func (element *ComboBox) Draw (destination artist.Canvas) { state := element.state() bounds := element.entity.Bounds() - pattern := element.theme.Pattern(tomo.PatternButton, state) + pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, comboBoxCase) pattern.Draw(destination, bounds) - foreground := element.theme.Color(tomo.ColorForeground, state) - sink := element.theme.Sink(tomo.PatternButton) - margin := element.theme.Margin(tomo.PatternButton) - padding := element.theme.Padding(tomo.PatternButton) + foreground := element.entity.Theme().Color(tomo.ColorForeground, state, comboBoxCase) + sink := element.entity.Theme().Sink(tomo.PatternButton, comboBoxCase) + margin := element.entity.Theme().Margin(tomo.PatternButton, comboBoxCase) + padding := element.entity.Theme().Padding(tomo.PatternButton, comboBoxCase) offset := image.Pt(0, bounds.Dy() / 2).Add(bounds.Min) @@ -74,7 +72,7 @@ func (element *ComboBox) Draw (destination canvas.Canvas) { offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - icon := element.theme.Icon(tomo.IconExpand, tomo.IconSizeSmall) + icon := element.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase) if icon != nil { iconBounds := icon.Bounds() addedWidth := iconBounds.Dx() + margin.X @@ -142,21 +140,11 @@ func (element *ComboBox) SetEnabled (enabled bool) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *ComboBox) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawer.SetFace (element.theme.FontFace ( +func (element *ComboBox) HandleThemeChange () { + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *ComboBox) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new + tomo.FontSizeNormal, + comboBoxCase)) element.updateMinimumSize() element.entity.Invalidate() } @@ -228,7 +216,7 @@ func (element *ComboBox) dropDown () { menu, err := window.NewMenu(element.entity.Bounds()) if err != nil { return } - cellToOption := make(map[tomo.Selectable] Option) + cellToOption := make(map[ability.Selectable] Option) list := NewList() for _, option := range element.options { @@ -254,13 +242,13 @@ func (element *ComboBox) dropDown () { } func (element *ComboBox) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternButton) - margin := element.theme.Margin(tomo.PatternButton) + padding := element.entity.Theme().Padding(tomo.PatternButton, comboBoxCase) + margin := element.entity.Theme().Margin(tomo.PatternButton, comboBoxCase) textBounds := element.drawer.LayoutBounds() minimumSize := textBounds.Sub(textBounds.Min) - icon := element.theme.Icon(tomo.IconExpand, tomo.IconSizeSmall) + icon := element.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase) if icon != nil { bounds := icon.Bounds() minimumSize.Max.X += bounds.Dx() diff --git a/elements/container.go b/elements/container.go index b8a7f61..df61e45 100644 --- a/elements/container.go +++ b/elements/container.go @@ -9,7 +9,7 @@ type scratchEntry struct { } type container struct { - entity tomo.ContainerEntity + entity tomo.Entity scratch map[tomo.Element] scratchEntry minimumSize func () } diff --git a/elements/directory.go b/elements/directory.go index f1b19b7..9234a2d 100644 --- a/elements/directory.go +++ b/elements/directory.go @@ -4,17 +4,14 @@ import "image" import "path/filepath" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/ability" import "git.tebibyte.media/sashakoshka/tomo/shatter" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" // TODO: base on flow implementation of list. also be able to switch to a table // variant for a more information dense view. -type directoryEntity interface { - tomo.ContainerEntity - tomo.ScrollableEntity -} +var directoryCase = tomo.C("tomo", "list") type historyEntry struct { location string @@ -25,8 +22,7 @@ type historyEntry struct { // file system. type Directory struct { container - entity directoryEntity - theme theme.Wrapped + entity tomo.Entity scroll image.Point contentBounds image.Rectangle @@ -48,8 +44,7 @@ func NewDirectory ( err error, ) { element = &Directory { } - element.theme.Case = tomo.C("tomo", "list") - element.entity = tomo.NewEntity(element).(directoryEntity) + element.entity = tomo.GetBackend().NewEntity(element) element.container.entity = element.entity element.minimumSize = element.updateMinimumSize element.init() @@ -57,7 +52,7 @@ func NewDirectory ( return } -func (element *Directory) Draw (destination canvas.Canvas) { +func (element *Directory) Draw (destination artist.Canvas) { rocks := make([]image.Rectangle, element.entity.CountChildren()) for index := 0; index < element.entity.CountChildren(); index ++ { rocks[index] = element.entity.Child(index).Entity().Bounds() @@ -65,7 +60,7 @@ func (element *Directory) Draw (destination canvas.Canvas) { tiles := shatter.Shatter(element.entity.Bounds(), rocks...) for _, tile := range tiles { - element.DrawBackground(canvas.Cut(destination, tile)) + element.DrawBackground(artist.Cut(destination, tile)) } } @@ -74,8 +69,8 @@ func (element *Directory) Layout () { element.scroll.Y = element.maxScrollHeight() } - margin := element.theme.Margin(tomo.PatternPinboard) - padding := element.theme.Padding(tomo.PatternPinboard) + margin := element.entity.Theme().Margin(tomo.PatternPinboard, directoryCase) + padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase) bounds := padding.Apply(element.entity.Bounds()) element.contentBounds = image.Rectangle { } @@ -99,7 +94,7 @@ func (element *Directory) Layout () { if width + dot.X > bounds.Max.X { nextLine() } - if typedChild, ok := child.(tomo.Flexible); ok { + if typedChild, ok := child.(ability.Flexible); ok { height = typedChild.FlexibleHeightFor(width) } if rowHeight < height { @@ -145,7 +140,7 @@ func (element *Directory) HandleChildMouseDown ( child tomo.Element, ) { element.selectNone() - if child, ok := child.(tomo.Selectable); ok { + if child, ok := child.(ability.Selectable); ok { index := element.entity.IndexOf(child) element.entity.SelectChild(index, true) } @@ -158,7 +153,7 @@ func (element *Directory) HandleChildMouseUp ( child tomo.Element, ) { } -func (element *Directory) HandleChildFlexibleHeightChange (child tomo.Flexible) { +func (element *Directory) HandleChildFlexibleHeightChange (child ability.Flexible) { element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -172,7 +167,7 @@ func (element *Directory) ScrollContentBounds () image.Rectangle { // ScrollViewportBounds returns the size and position of the element's // viewport relative to ScrollBounds. func (element *Directory) ScrollViewportBounds () image.Rectangle { - padding := element.theme.Padding(tomo.PatternPinboard) + padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase) bounds := padding.Apply(element.entity.Bounds()) bounds = bounds.Sub(bounds.Min).Add(element.scroll) return bounds @@ -204,15 +199,12 @@ func (element *Directory) ScrollAxes () (horizontal, vertical bool) { return false, true } -func (element *Directory) DrawBackground (destination canvas.Canvas) { - element.theme.Pattern(tomo.PatternPinboard, tomo.State { }). +func (element *Directory) DrawBackground (destination artist.Canvas) { + element.entity.Theme().Pattern(tomo.PatternPinboard, tomo.State { }, directoryCase). Draw(destination, element.entity.Bounds()) } -// SetTheme sets the element's theme. -func (element *Directory) SetTheme (theme tomo.Theme) { - if theme == element.theme.Theme { return } - element.theme.Theme = theme +func (element *Directory) HandleThemeChange () { element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -301,7 +293,7 @@ func (element *Directory) selectNone () { } func (element *Directory) maxScrollHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) + padding := element.entity.Theme().Padding(tomo.PatternSunken, directoryCase) viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() height = element.contentBounds.Dy() - viewportHeight if height < 0 { height = 0 } @@ -310,7 +302,7 @@ func (element *Directory) maxScrollHeight () (height int) { func (element *Directory) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternPinboard) + padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase) minimumWidth := 0 for index := 0; index < element.entity.CountChildren(); index ++ { width, height := element.entity.ChildMinimumSize(index) diff --git a/elements/document.go b/elements/document.go index 30a368a..5f92c8c 100644 --- a/elements/document.go +++ b/elements/document.go @@ -2,26 +2,21 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/ability" import "git.tebibyte.media/sashakoshka/tomo/shatter" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -type documentEntity interface { - tomo.ContainerEntity - tomo.ScrollableEntity -} +var documentCase = tomo.C("tomo", "document") // Document is a scrollable container capcable of laying out flexible child // elements. Children can be added either inline (similar to an HTML/CSS inline // element), or expanding (similar to an HTML/CSS block element). type Document struct { container - entity documentEntity + entity tomo.Entity scroll image.Point contentBounds image.Rectangle - - theme theme.Wrapped onScrollBoundsChange func () } @@ -29,8 +24,7 @@ type Document struct { // NewDocument creates a new document container. func NewDocument (children ...tomo.Element) (element *Document) { element = &Document { } - element.theme.Case = tomo.C("tomo", "document") - element.entity = tomo.NewEntity(element).(documentEntity) + element.entity = tomo.GetBackend().NewEntity(element) element.container.entity = element.entity element.minimumSize = element.updateMinimumSize element.init() @@ -39,7 +33,7 @@ func NewDocument (children ...tomo.Element) (element *Document) { } // Draw causes the element to draw to the specified destination canvas. -func (element *Document) Draw (destination canvas.Canvas) { +func (element *Document) Draw (destination artist.Canvas) { rocks := make([]image.Rectangle, element.entity.CountChildren()) for index := 0; index < element.entity.CountChildren(); index ++ { rocks[index] = element.entity.Child(index).Entity().Bounds() @@ -47,7 +41,7 @@ func (element *Document) Draw (destination canvas.Canvas) { tiles := shatter.Shatter(element.entity.Bounds(), rocks...) for _, tile := range tiles { - element.entity.DrawBackground(canvas.Cut(destination, tile)) + element.entity.DrawBackground(artist.Cut(destination, tile)) } } @@ -57,8 +51,8 @@ func (element *Document) Layout () { element.scroll.Y = element.maxScrollHeight() } - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) + margin := element.entity.Theme().Margin(tomo.PatternBackground, documentCase) + padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase) bounds := padding.Apply(element.entity.Bounds()) element.contentBounds = image.Rectangle { } @@ -89,7 +83,7 @@ func (element *Document) Layout () { if width < bounds.Dx() && entry.expand { width = bounds.Dx() } - if typedChild, ok := child.(tomo.Flexible); ok { + if typedChild, ok := child.(ability.Flexible); ok { height = typedChild.FlexibleHeightFor(width) } if rowHeight < height { @@ -130,7 +124,7 @@ func (element *Document) AdoptInline (children ...tomo.Element) { element.adopt(false, children...) } -func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) { +func (element *Document) HandleChildFlexibleHeightChange (child ability.Flexible) { element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -138,14 +132,11 @@ func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) { // DrawBackground draws this element's background pattern to the specified // destination canvas. -func (element *Document) DrawBackground (destination canvas.Canvas) { +func (element *Document) DrawBackground (destination artist.Canvas) { element.entity.DrawBackground(destination) } -// SetTheme sets the element's theme. -func (element *Document) SetTheme (theme tomo.Theme) { - if theme == element.theme.Theme { return } - element.theme.Theme = theme +func (element *Document) HandleThemeChange () { element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -159,7 +150,7 @@ func (element *Document) ScrollContentBounds () image.Rectangle { // ScrollViewportBounds returns the size and position of the element's // viewport relative to ScrollBounds. func (element *Document) ScrollViewportBounds () image.Rectangle { - padding := element.theme.Padding(tomo.PatternBackground) + padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase) bounds := padding.Apply(element.entity.Bounds()) bounds = bounds.Sub(bounds.Min).Add(element.scroll) return bounds @@ -192,7 +183,7 @@ func (element *Document) ScrollAxes () (horizontal, vertical bool) { } func (element *Document) maxScrollHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) + padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase) viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() height = element.contentBounds.Dy() - viewportHeight if height < 0 { height = 0 } @@ -200,7 +191,7 @@ func (element *Document) maxScrollHeight () (height int) { } func (element *Document) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternBackground) + padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase) minimumWidth := 0 for index := 0; index < element.entity.CountChildren(); index ++ { width, height := element.entity.ChildMinimumSize(index) diff --git a/elements/file.go b/elements/file.go index 7504944..0c3e105 100644 --- a/elements/file.go +++ b/elements/file.go @@ -6,22 +6,13 @@ import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" -type fileEntity interface { - tomo.SelectableEntity - tomo.FocusableEntity -} +var fileCase = tomo.C("files", "file") // File displays an interactive visual representation of a file within any // file system. type File struct { - entity fileEntity - - config config.Wrapped - theme theme.Wrapped + entity tomo.Entity lastClick time.Time pressed bool @@ -43,8 +34,7 @@ func NewFile ( err error, ) { element = &File { enabled: true } - element.theme.Case = tomo.C("files", "file") - element.entity = tomo.NewEntity(element).(fileEntity) + element.entity = tomo.GetBackend().NewEntity(element) err = element.SetLocation(location, within) return } @@ -55,13 +45,13 @@ func (element *File) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *File) Draw (destination canvas.Canvas) { +func (element *File) Draw (destination artist.Canvas) { // background state := element.state() bounds := element.entity.Bounds() - sink := element.theme.Sink(tomo.PatternButton) - element.theme. - Pattern(tomo.PatternButton, state). + sink := element.entity.Theme().Sink(tomo.PatternButton, fileCase) + element.entity.Theme(). + Pattern(tomo.PatternButton, state, fileCase). Draw(destination, bounds) // icon @@ -76,7 +66,7 @@ func (element *File) Draw (destination canvas.Canvas) { } icon.Draw ( destination, - element.theme.Color(tomo.ColorForeground, state), + element.entity.Theme().Color(tomo.ColorForeground, state, fileCase), bounds.Min.Add(offset)) } } @@ -185,7 +175,7 @@ func (element *File) HandleMouseUp ( if button != input.ButtonLeft { return } element.pressed = false within := position.In(element.entity.Bounds()) - if time.Since(element.lastClick) < element.config.DoubleClickDelay() { + if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() { if element.Enabled() && within && element.onChoose != nil { element.onChoose() } @@ -195,17 +185,8 @@ func (element *File) HandleMouseUp ( element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *File) SetTheme (theme tomo.Theme) { - if theme == element.theme.Theme { return } - element.theme.Theme = theme - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *File) SetConfig (config tomo.Config) { - if config == element.config.Config { return } - element.config.Config = config +func (element *File) HandleThemeChange () { + element.updateMinimumSize() element.entity.Invalidate() } @@ -219,11 +200,11 @@ func (element *File) state () tomo.State { } func (element *File) icon () artist.Icon { - return element.theme.Icon(element.iconID, tomo.IconSizeLarge) + return element.entity.Theme().Icon(element.iconID, tomo.IconSizeLarge, fileCase) } func (element *File) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternButton) + padding := element.entity.Theme().Padding(tomo.PatternButton, fileCase) icon := element.icon() if icon == nil { element.entity.SetMinimumSize ( diff --git a/elements/fun/clock.go b/elements/fun/clock.go index 0b4468d..69be434 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -5,22 +5,21 @@ import "math" import "image" import "image/color" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +var clockCase = tomo.C("tomo", "clock") // AnalogClock can display the time of day in an analog format. type AnalogClock struct { entity tomo.Entity time time.Time - theme theme.Wrapped } // NewAnalogClock creates a new analog clock that displays the specified time. func NewAnalogClock (newTime time.Time) (element *AnalogClock) { element = &AnalogClock { } - element.theme.Case = tomo.C("tomo", "clock") - element.entity = tomo.NewEntity(element) + element.entity = tomo.GetBackend().NewEntity(element) element.entity.SetMinimumSize(64, 64) return } @@ -31,18 +30,18 @@ func (element *AnalogClock) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *AnalogClock) Draw (destination canvas.Canvas) { +func (element *AnalogClock) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() state := tomo.State { } - pattern := element.theme.Pattern(tomo.PatternSunken, state) - padding := element.theme.Padding(tomo.PatternSunken) + pattern := element.entity.Theme().Pattern(tomo.PatternSunken, state, clockCase) + padding := element.entity.Theme().Padding(tomo.PatternSunken, clockCase) pattern.Draw(destination, bounds) bounds = padding.Apply(bounds) - foreground := element.theme.Color(tomo.ColorForeground, state) - accent := element.theme.Color(tomo.ColorAccent, state) + foreground := element.entity.Theme().Color(tomo.ColorForeground, state, clockCase) + accent := element.entity.Theme().Color(tomo.ColorAccent, state, clockCase) for hour := 0; hour < 12; hour ++ { element.radialLine ( @@ -67,15 +66,12 @@ func (element *AnalogClock) SetTime (newTime time.Time) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *AnalogClock) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new +func (element *AnalogClock) HandleThemeChange () { element.entity.Invalidate() } func (element *AnalogClock) radialLine ( - destination canvas.Canvas, + destination artist.Canvas, source color.RGBA, inner float64, outer float64, diff --git a/elements/fun/piano.go b/elements/fun/piano.go index 5776ed1..973fb38 100644 --- a/elements/fun/piano.go +++ b/elements/fun/piano.go @@ -3,12 +3,14 @@ package fun import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist/artutil" import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music" +var pianoCase = tomo.C("tomo", "piano") +var flatCase = tomo.C("tomo", "piano", "flatKey") +var sharpCase = tomo.C("tomo", "piano", "sharpKey") + const pianoKeyWidth = 18 type pianoKey struct { @@ -18,12 +20,7 @@ type pianoKey struct { // Piano is an element that can be used to input midi notes. type Piano struct { - entity tomo.FocusableEntity - - config config.Wrapped - theme theme.Wrapped - flatTheme theme.Wrapped - sharpTheme theme.Wrapped + entity tomo.Entity low, high music.Octave flatKeys []pianoKey @@ -49,10 +46,7 @@ func NewPiano (low, high music.Octave) (element *Piano) { keynavPressed: make(map[music.Note] bool), } - element.theme.Case = tomo.C("tomo", "piano") - element.flatTheme.Case = tomo.C("tomo", "piano", "flatKey") - element.sharpTheme.Case = tomo.C("tomo", "piano", "sharpKey") - element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) + element.entity = tomo.GetBackend().NewEntity(element) element.updateMinimumSize() return } @@ -63,7 +57,7 @@ func (element *Piano) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *Piano) Draw (destination canvas.Canvas) { +func (element *Piano) Draw (destination artist.Canvas) { element.recalculate() state := tomo.State { @@ -90,8 +84,8 @@ func (element *Piano) Draw (destination canvas.Canvas) { state) } - pattern := element.theme.Pattern(tomo.PatternPinboard, state) - artist.DrawShatter ( + pattern := element.entity.Theme().Pattern(tomo.PatternPinboard, state, pianoCase) + artutil.DrawShatter ( destination, pattern, element.entity.Bounds(), element.contentBounds) } @@ -248,26 +242,13 @@ func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *Piano) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.flatTheme.Theme = new - element.sharpTheme.Theme = new - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *Piano) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new +func (element *Piano) HandleThemeChange () { element.updateMinimumSize() element.entity.Invalidate() } func (element *Piano) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternPinboard) + padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase) element.entity.SetMinimumSize ( pianoKeyWidth * 7 * element.countOctaves() + padding.Horizontal(), @@ -290,7 +271,7 @@ func (element *Piano) recalculate () { element.flatKeys = make([]pianoKey, element.countFlats()) element.sharpKeys = make([]pianoKey, element.countSharps()) - padding := element.theme.Padding(tomo.PatternPinboard) + padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase) bounds := padding.Apply(element.entity.Bounds()) dot := bounds.Min @@ -323,23 +304,23 @@ func (element *Piano) recalculate () { } func (element *Piano) drawFlat ( - destination canvas.Canvas, + destination artist.Canvas, bounds image.Rectangle, pressed bool, state tomo.State, ) { state.Pressed = pressed - pattern := element.flatTheme.Pattern(tomo.PatternButton, state) + pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, flatCase) pattern.Draw(destination, bounds) } func (element *Piano) drawSharp ( - destination canvas.Canvas, + destination artist.Canvas, bounds image.Rectangle, pressed bool, state tomo.State, ) { state.Pressed = pressed - pattern := element.sharpTheme.Pattern(tomo.PatternButton, state) + pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, sharpCase) pattern.Draw(destination, bounds) } diff --git a/elements/icon.go b/elements/icon.go index 03f0e9c..db02085 100644 --- a/elements/icon.go +++ b/elements/icon.go @@ -2,14 +2,13 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +var iconCase = tomo.C("tomo", "icon") // Icon is an element capable of displaying a singular icon. type Icon struct { entity tomo.Entity - theme theme.Wrapped id tomo.Icon size tomo.IconSize } @@ -20,8 +19,7 @@ func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) { id: id, size: size, } - element.entity = tomo.NewEntity(element) - element.theme.Case = tomo.C("tomo", "icon") + element.entity = tomo.GetBackend().NewEntity(element) element.updateMinimumSize() return } @@ -40,23 +38,19 @@ func (element *Icon) SetIcon (id tomo.Icon, size tomo.IconSize) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *Icon) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - if element.entity == nil { return } +func (element *Icon) HandleThemeChange () { element.updateMinimumSize() element.entity.Invalidate() } // Draw causes the element to draw to the specified destination canvas. -func (element *Icon) Draw (destination canvas.Canvas) { +func (element *Icon) Draw (destination artist.Canvas) { if element.entity == nil { return } bounds := element.entity.Bounds() state := tomo.State { } - element.theme. - Pattern(tomo.PatternBackground, state). + element.entity.Theme(). + Pattern(tomo.PatternBackground, state, iconCase). Draw(destination, bounds) icon := element.icon() if icon != nil { @@ -66,13 +60,13 @@ func (element *Icon) Draw (destination canvas.Canvas) { (bounds.Dy() - iconBounds.Dy()) / 2) icon.Draw ( destination, - element.theme.Color(tomo.ColorForeground, state), + element.entity.Theme().Color(tomo.ColorForeground, state, iconCase), bounds.Min.Add(offset)) } } func (element *Icon) icon () artist.Icon { - return element.theme.Icon(element.id, element.size) + return element.entity.Theme().Icon(element.id, element.size, iconCase) } func (element *Icon) updateMinimumSize () { diff --git a/elements/image.go b/elements/image.go index 7ad3122..271a654 100644 --- a/elements/image.go +++ b/elements/image.go @@ -2,7 +2,7 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" // TODO: this element is lame need to make it better @@ -10,13 +10,13 @@ import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" // Image is an element capable of displaying an image. type Image struct { entity tomo.Entity - buffer canvas.Canvas + buffer artist.Canvas } // NewImage creates a new image element. func NewImage (image image.Image) (element *Image) { - element = &Image { buffer: canvas.FromImage(image) } - element.entity = tomo.NewEntity(element) + element = &Image { buffer: artist.FromImage(image) } + element.entity = tomo.GetBackend().NewEntity(element) bounds := element.buffer.Bounds() element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy()) return @@ -28,7 +28,7 @@ func (element *Image) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *Image) Draw (destination canvas.Canvas) { +func (element *Image) Draw (destination artist.Canvas) { if element.entity == nil { return } (patterns.Texture { Canvas: element.buffer }). Draw(destination, element.entity.Bounds()) diff --git a/elements/label.go b/elements/label.go index 432c7e4..0d28b1b 100644 --- a/elements/label.go +++ b/elements/label.go @@ -5,14 +5,14 @@ import "golang.org/x/image/math/fixed" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" + +var labelCase = tomo.C("tomo", "label") // Label is a simple text box. type Label struct { - entity tomo.FlexibleEntity + entity tomo.Entity align textdraw.Align wrap bool @@ -22,19 +22,15 @@ type Label struct { forcedColumns int forcedRows int minHeight int - - config config.Wrapped - theme theme.Wrapped } // NewLabel creates a new label. func NewLabel (text string) (element *Label) { element = &Label { } - element.theme.Case = tomo.C("tomo", "label") - element.entity = tomo.NewEntity(element).(tomo.FlexibleEntity) - element.drawer.SetFace (element.theme.FontFace ( + element.entity = tomo.GetBackend().NewEntity(element) + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) + tomo.FontSizeNormal, labelCase)) element.SetText(text) return } @@ -52,7 +48,7 @@ func (element *Label) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *Label) Draw (destination canvas.Canvas) { +func (element *Label) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() if element.wrap { @@ -63,9 +59,9 @@ func (element *Label) Draw (destination canvas.Canvas) { element.entity.DrawBackground(destination) textBounds := element.drawer.LayoutBounds() - foreground := element.theme.Color ( + foreground := element.entity.Theme().Color ( tomo.ColorForeground, - tomo.State { }) + tomo.State { }, labelCase) element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min)) } @@ -132,21 +128,10 @@ func (element *Label) SetAlign (align textdraw.Align) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *Label) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawer.SetFace (element.theme.FontFace ( +func (element *Label) HandleThemeChange () { + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *Label) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new + tomo.FontSizeNormal, labelCase)) element.updateMinimumSize() element.entity.Invalidate() } @@ -195,7 +180,7 @@ func (element *Label) updateMinimumSize () { if element.wrap { em := element.drawer.Em().Round() if em < 1 { - em = element.theme.Padding(tomo.PatternBackground)[0] + em = element.entity.Theme().Padding(tomo.PatternBackground, labelCase)[0] } width, height = em, element.drawer.LineHeight().Round() element.entity.NotifyFlexibleHeightChange() diff --git a/elements/lerpslider.go b/elements/lerpslider.go index 5e9eb59..9d62618 100644 --- a/elements/lerpslider.go +++ b/elements/lerpslider.go @@ -33,7 +33,7 @@ func NewHLerpSlider[T Numeric] (min, max T, value T) (element *LerpSlider[T]) { min: min, max: max, } - element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) + element.entity = tomo.GetBackend().NewEntity(element) element.construct() element.SetValue(value) return diff --git a/elements/list.go b/elements/list.go index 4fad816..db1c006 100644 --- a/elements/list.go +++ b/elements/list.go @@ -3,19 +3,15 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" - -type listEntity interface { - tomo.ContainerEntity - tomo.ScrollableEntity - tomo.FocusableEntity -} +import "git.tebibyte.media/sashakoshka/tomo/ability" +import "git.tebibyte.media/sashakoshka/tomo/artist/artutil" type list struct { container - entity listEntity + entity tomo.Entity + + c tomo.Case enabled bool scroll image.Point @@ -25,8 +21,6 @@ type list struct { forcedMinimumWidth int forcedMinimumHeight int - theme theme.Wrapped - onClick func () onSelectionChange func () onScrollBoundsChange func () @@ -42,8 +36,8 @@ type FlowList struct { func NewList (children ...tomo.Element) (element *List) { element = &List { } - element.theme.Case = tomo.C("tomo", "list") - element.entity = tomo.NewEntity(element).(listEntity) + element.c = tomo.C("tomo", "list") + element.entity = tomo.GetBackend().NewEntity(element) element.container.entity = element.entity element.minimumSize = element.updateMinimumSize element.init(children...) @@ -52,8 +46,8 @@ func NewList (children ...tomo.Element) (element *List) { func NewFlowList (children ...tomo.Element) (element *FlowList) { element = &FlowList { } - element.theme.Case = tomo.C("tomo", "flowList") - element.entity = tomo.NewEntity(element).(listEntity) + element.c = tomo.C("tomo", "flowList") + element.entity = tomo.GetBackend().NewEntity(element) element.container.entity = element.entity element.minimumSize = element.updateMinimumSize element.init(children...) @@ -67,14 +61,14 @@ func (element *list) init (children ...tomo.Element) { element.Adopt(children...) } -func (element *list) Draw (destination canvas.Canvas) { +func (element *list) Draw (destination artist.Canvas) { rocks := make([]image.Rectangle, element.entity.CountChildren()) for index := 0; index < element.entity.CountChildren(); index ++ { rocks[index] = element.entity.Child(index).Entity().Bounds() } - pattern := element.theme.Pattern(tomo.PatternSunken, element.state()) - artist.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...) + pattern := element.entity.Theme().Pattern(tomo.PatternSunken, element.state(), element.c) + artutil.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...) } func (element *List) Layout () { @@ -82,8 +76,8 @@ func (element *List) Layout () { element.scroll.Y = element.maxScrollHeight() } - margin := element.theme.Margin(tomo.PatternSunken) - padding := element.theme.Padding(tomo.PatternSunken) + margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c) + padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c) bounds := padding.Apply(element.entity.Bounds()) element.contentBounds = image.Rectangle { } @@ -120,8 +114,8 @@ func (element *FlowList) Layout () { element.scroll.Y = element.maxScrollHeight() } - margin := element.theme.Margin(tomo.PatternSunken) - padding := element.theme.Padding(tomo.PatternSunken) + margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c) + padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c) bounds := padding.Apply(element.entity.Bounds()) element.contentBounds = image.Rectangle { } @@ -145,7 +139,7 @@ func (element *FlowList) Layout () { if width + dot.X > bounds.Max.X { nextLine() } - if typedChild, ok := child.(tomo.Flexible); ok { + if typedChild, ok := child.(ability.Flexible); ok { height = typedChild.FlexibleHeightFor(width) } if rowHeight < height { @@ -170,14 +164,14 @@ func (element *FlowList) Layout () { } } -func (element *list) Selected () tomo.Selectable { +func (element *list) Selected () ability.Selectable { if element.selected == -1 { return nil } - child, ok := element.entity.Child(element.selected).(tomo.Selectable) + child, ok := element.entity.Child(element.selected).(ability.Selectable) if !ok { return nil } return child } -func (element *list) Select (child tomo.Selectable) { +func (element *list) Select (child ability.Selectable) { index := element.entity.IndexOf(child) if element.selected == index { return } element.selectNone() @@ -231,7 +225,7 @@ func (element *list) HandleChildMouseDown ( ) { if !element.enabled { return } element.Focus() - if child, ok := child.(tomo.Selectable); ok { + if child, ok := child.(ability.Selectable); ok { element.Select(child) } } @@ -248,7 +242,7 @@ func (element *list) HandleChildMouseUp ( } } -func (element *list) HandleChildFlexibleHeightChange (child tomo.Flexible) { +func (element *list) HandleChildFlexibleHeightChange (child ability.Flexible) { element.minimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -280,14 +274,11 @@ func (element *list) HandleKeyDown (key input.Key, modifiers input.Modifiers) { func (element *list) HandleKeyUp(key input.Key, modifiers input.Modifiers) { } -func (element *list) DrawBackground (destination canvas.Canvas) { +func (element *list) DrawBackground (destination artist.Canvas) { element.entity.DrawBackground(destination) } -// SetTheme sets the element's theme. -func (element *list) SetTheme (theme tomo.Theme) { - if theme == element.theme.Theme { return } - element.theme.Theme = theme +func (element *list) HandleThemeChange () { element.minimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -322,7 +313,7 @@ func (element *list) ScrollContentBounds () image.Rectangle { // ScrollViewportBounds returns the size and position of the element's // viewport relative to ScrollBounds. func (element *list) ScrollViewportBounds () image.Rectangle { - padding := element.theme.Padding(tomo.PatternSunken) + padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c) bounds := padding.Apply(element.entity.Bounds()) bounds = bounds.Sub(bounds.Min).Add(element.scroll) return bounds @@ -374,7 +365,7 @@ func (element *list) selectNone () { func (element *list) scrollToSelected () { if element.selected < 0 { return } target := element.entity.Child(element.selected).Entity().Bounds() - padding := element.theme.Padding(tomo.PatternSunken) + padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c) bounds := padding.Apply(element.entity.Bounds()) if target.Min.Y < bounds.Min.Y { element.scroll.Y -= bounds.Min.Y - target.Min.Y @@ -395,7 +386,7 @@ func (element *list) state () tomo.State { } func (element *list) maxScrollHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) + padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c) viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() height = element.contentBounds.Dy() - viewportHeight if height < 0 { height = 0 } @@ -403,8 +394,8 @@ func (element *list) maxScrollHeight () (height int) { } func (element *List) updateMinimumSize () { - margin := element.theme.Margin(tomo.PatternSunken) - padding := element.theme.Padding(tomo.PatternSunken) + margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c) + padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c) width := 0 height := 0 @@ -437,7 +428,7 @@ func (element *List) updateMinimumSize () { } func (element *FlowList) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternSunken) + padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c) minimumWidth := 0 for index := 0; index < element.entity.CountChildren(); index ++ { width, height := element.entity.ChildMinimumSize(index) diff --git a/elements/progressbar.go b/elements/progressbar.go index 5c96daa..c4c5308 100644 --- a/elements/progressbar.go +++ b/elements/progressbar.go @@ -2,17 +2,14 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" + +var progressBarCase = tomo.C("tomo", "progressBar") // ProgressBar displays a visual indication of how far along a task is. type ProgressBar struct { entity tomo.Entity progress float64 - - config config.Wrapped - theme theme.Wrapped } // NewProgressBar creates a new progress bar displaying the given progress @@ -21,8 +18,7 @@ func NewProgressBar (progress float64) (element *ProgressBar) { if progress < 0 { progress = 0 } if progress > 1 { progress = 1 } element = &ProgressBar { progress: progress } - element.entity = tomo.NewEntity(element) - element.theme.Case = tomo.C("tomo", "progressBar") + element.entity = tomo.GetBackend().NewEntity(element) element.updateMinimumSize() return } @@ -33,18 +29,18 @@ func (element *ProgressBar) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *ProgressBar) Draw (destination canvas.Canvas) { +func (element *ProgressBar) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() - pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { }) - padding := element.theme.Padding(tomo.PatternSunken) + pattern := element.entity.Theme().Pattern(tomo.PatternSunken, tomo.State { }, progressBarCase) + padding := element.entity.Theme().Padding(tomo.PatternSunken, progressBarCase) pattern.Draw(destination, bounds) bounds = padding.Apply(bounds) meterBounds := image.Rect ( bounds.Min.X, bounds.Min.Y, bounds.Min.X + int(float64(bounds.Dx()) * element.progress), bounds.Max.Y) - mercury := element.theme.Pattern(tomo.PatternMercury, tomo.State { }) + mercury := element.entity.Theme().Pattern(tomo.PatternMercury, tomo.State { }, progressBarCase) mercury.Draw(destination, meterBounds) } @@ -57,25 +53,14 @@ func (element *ProgressBar) SetProgress (progress float64) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *ProgressBar) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *ProgressBar) SetConfig (new tomo.Config) { - if new == nil || new == element.config.Config { return } - element.config.Config = new +func (element *ProgressBar) HandleThemeChange () { element.updateMinimumSize() element.entity.Invalidate() } func (element *ProgressBar) updateMinimumSize() { - padding := element.theme.Padding(tomo.PatternSunken) - innerPadding := element.theme.Padding(tomo.PatternMercury) + padding := element.entity.Theme().Padding(tomo.PatternSunken, progressBarCase) + innerPadding := element.entity.Theme().Padding(tomo.PatternMercury, progressBarCase) element.entity.SetMinimumSize ( padding.Horizontal() + innerPadding.Horizontal(), padding.Vertical() + innerPadding.Vertical()) diff --git a/elements/scroll.go b/elements/scroll.go index 20b18b0..cd39609 100644 --- a/elements/scroll.go +++ b/elements/scroll.go @@ -3,9 +3,10 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/ability" + +var scrollCase = tomo.C("tomo", "scroll") // ScrollMode specifies which sides of a Scroll have scroll bars. type ScrollMode int; const ( @@ -24,21 +25,17 @@ func (mode ScrollMode) Includes (sub ScrollMode) bool { // Scroll adds scroll bars to any scrollable element. It also captures scroll // wheel input. type Scroll struct { - entity tomo.ContainerEntity + entity tomo.Entity - child tomo.Scrollable + child ability.Scrollable horizontal *ScrollBar vertical *ScrollBar - - config config.Wrapped - theme theme.Wrapped } // NewScroll creates a new scroll element. -func NewScroll (mode ScrollMode, child tomo.Scrollable) (element *Scroll) { +func NewScroll (mode ScrollMode, child ability.Scrollable) (element *Scroll) { element = &Scroll { } - element.theme.Case = tomo.C("tomo", "scroll") - element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.entity = tomo.GetBackend().NewEntity(element) if mode.Includes(ScrollHorizontal) { element.horizontal = NewHScrollBar() @@ -79,15 +76,15 @@ func (element *Scroll) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *Scroll) Draw (destination canvas.Canvas) { +func (element *Scroll) Draw (destination artist.Canvas) { if element.horizontal != nil && element.vertical != nil { bounds := element.entity.Bounds() bounds.Min = image.Pt ( bounds.Max.X - element.vertical.Entity().Bounds().Dx(), bounds.Max.Y - element.horizontal.Entity().Bounds().Dy()) state := tomo.State { } - deadArea := element.theme.Pattern(tomo.PatternDead, state) - deadArea.Draw(canvas.Cut(destination, bounds), bounds) + deadArea := element.entity.Theme().Pattern(tomo.PatternDead, state, scrollCase) + deadArea.Draw(artist.Cut(destination, bounds), bounds) } } @@ -134,12 +131,12 @@ func (element *Scroll) Layout () { // DrawBackground draws this element's background pattern to the specified // destination canvas. -func (element *Scroll) DrawBackground (destination canvas.Canvas) { +func (element *Scroll) DrawBackground (destination artist.Canvas) { element.entity.DrawBackground(destination) } // Adopt sets this element's child. If nil is passed, any child is removed. -func (element *Scroll) Adopt (child tomo.Scrollable) { +func (element *Scroll) Adopt (child ability.Scrollable) { if element.child != nil { element.entity.Disown(element.entity.IndexOf(element.child)) } @@ -156,7 +153,7 @@ func (element *Scroll) Adopt (child tomo.Scrollable) { // Child returns this element's child. If there is no child, this method will // return nil. -func (element *Scroll) Child () tomo.Scrollable { +func (element *Scroll) Child () ability.Scrollable { return element.child } @@ -166,7 +163,7 @@ func (element *Scroll) HandleChildMinimumSizeChange (tomo.Element) { element.entity.InvalidateLayout() } -func (element *Scroll) HandleChildScrollBoundsChange (tomo.Scrollable) { +func (element *Scroll) HandleChildScrollBoundsChange (ability.Scrollable) { element.updateEnabled() viewportBounds := element.child.ScrollViewportBounds() contentBounds := element.child.ScrollContentBounds() @@ -189,20 +186,12 @@ func (element *Scroll) HandleScroll ( element.scrollChildBy(int(deltaX), int(deltaY)) } -// SetTheme sets the element's theme. -func (element *Scroll) SetTheme (theme tomo.Theme) { - if theme == element.theme.Theme { return } - element.theme.Theme = theme +func (element *Scroll) HandleThemeChange () { element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() } -// SetConfig sets the element's configuration. -func (element *Scroll) SetConfig (config tomo.Config) { - element.config.Config = config -} - func (element *Scroll) updateMinimumSize () { var width, height int diff --git a/elements/scrollbar.go b/elements/scrollbar.go index 2dbadfe..e3424e2 100644 --- a/elements/scrollbar.go +++ b/elements/scrollbar.go @@ -3,9 +3,7 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" // ScrollBar is an element similar to Slider, but it has special behavior that // makes it well suited for controlling the viewport position on one axis of a @@ -21,6 +19,8 @@ import "git.tebibyte.media/sashakoshka/tomo/default/config" type ScrollBar struct { entity tomo.Entity + c tomo.Case + vertical bool enabled bool dragging bool @@ -31,9 +31,6 @@ type ScrollBar struct { contentBounds image.Rectangle viewportBounds image.Rectangle - config config.Wrapped - theme theme.Wrapped - onScroll func (viewport image.Point) } @@ -43,8 +40,8 @@ func NewVScrollBar () (element *ScrollBar) { vertical: true, enabled: true, } - element.theme.Case = tomo.C("tomo", "scrollBarVertical") - element.entity = tomo.NewEntity(element).(tomo.Entity) + element.c = tomo.C("tomo", "scrollBarVertical") + element.entity = tomo.GetBackend().NewEntity(element) element.updateMinimumSize() return } @@ -54,8 +51,8 @@ func NewHScrollBar () (element *ScrollBar) { element = &ScrollBar { enabled: true, } - element.theme.Case = tomo.C("tomo", "scrollBarHorizontal") - element.entity = tomo.NewEntity(element).(tomo.Entity) + element.c = tomo.C("tomo", "scrollBarHorizontal") + element.entity = tomo.GetBackend().NewEntity(element) element.updateMinimumSize() return } @@ -66,7 +63,7 @@ func (element *ScrollBar) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *ScrollBar) Draw (destination canvas.Canvas) { +func (element *ScrollBar) Draw (destination artist.Canvas) { element.recalculate() bounds := element.entity.Bounds() @@ -74,10 +71,10 @@ func (element *ScrollBar) Draw (destination canvas.Canvas) { Disabled: !element.Enabled(), Pressed: element.dragging, } - element.theme.Pattern(tomo.PatternGutter, state).Draw ( + element.entity.Theme().Pattern(tomo.PatternGutter, state, element.c).Draw ( destination, bounds) - element.theme.Pattern(tomo.PatternHandle, state).Draw ( + element.entity.Theme().Pattern(tomo.PatternHandle, state, element.c).Draw ( destination, element.bar) } @@ -87,7 +84,7 @@ func (element *ScrollBar) HandleMouseDown ( button input.Button, modifiers input.Modifiers, ) { - velocity := element.config.ScrollVelocity() + velocity := element.entity.Config().ScrollVelocity() if position.In(element.bar) { // the mouse is pressed down within the bar's handle @@ -189,17 +186,7 @@ func (element *ScrollBar) OnScroll (callback func (viewport image.Point)) { element.onScroll = callback } -// SetTheme sets the element's theme. -func (element *ScrollBar) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *ScrollBar) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new +func (element *ScrollBar) HandleThemeChange () { element.updateMinimumSize() element.entity.Invalidate() } @@ -267,7 +254,7 @@ func (element *ScrollBar) recalculate () { func (element *ScrollBar) recalculateVertical () { bounds := element.entity.Bounds() - padding := element.theme.Padding(tomo.PatternGutter) + padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c) element.track = padding.Apply(bounds) contentBounds := element.contentBounds @@ -294,7 +281,7 @@ func (element *ScrollBar) recalculateVertical () { func (element *ScrollBar) recalculateHorizontal () { bounds := element.entity.Bounds() - padding := element.theme.Padding(tomo.PatternGutter) + padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c) element.track = padding.Apply(bounds) contentBounds := element.contentBounds @@ -320,8 +307,8 @@ func (element *ScrollBar) recalculateHorizontal () { } func (element *ScrollBar) updateMinimumSize () { - gutterPadding := element.theme.Padding(tomo.PatternGutter) - handlePadding := element.theme.Padding(tomo.PatternHandle) + gutterPadding := element.entity.Theme().Padding(tomo.PatternGutter, element.c) + handlePadding := element.entity.Theme().Padding(tomo.PatternHandle, element.c) if element.vertical { element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal(), diff --git a/elements/slider.go b/elements/slider.go index f42a181..5e67425 100644 --- a/elements/slider.go +++ b/elements/slider.go @@ -3,9 +3,7 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" // Slider is a slider control with a floating point value between zero and one. type Slider struct { @@ -23,14 +21,16 @@ func NewVSlider (value float64) (element *Slider) { func NewHSlider (value float64) (element *Slider) { element = &Slider { } element.value = value - element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) + element.entity = tomo.GetBackend().NewEntity(element) element.construct() return } type slider struct { - entity tomo.FocusableEntity - + entity tomo.Entity + + c tomo.Case + value float64 vertical bool dragging bool @@ -39,9 +39,6 @@ type slider struct { track image.Rectangle bar image.Rectangle - config config.Wrapped - theme theme.Wrapped - onSlide func () onRelease func () } @@ -49,9 +46,9 @@ type slider struct { func (element *slider) construct () { element.enabled = true if element.vertical { - element.theme.Case = tomo.C("tomo", "sliderVertical") + element.c = tomo.C("tomo", "sliderVertical") } else { - element.theme.Case = tomo.C("tomo", "sliderHorizontal") + element.c = tomo.C("tomo", "sliderHorizontal") } element.updateMinimumSize() } @@ -62,9 +59,9 @@ func (element *slider) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *slider) Draw (destination canvas.Canvas) { +func (element *slider) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() - element.track = element.theme.Padding(tomo.PatternGutter).Apply(bounds) + element.track = element.entity.Theme().Padding(tomo.PatternGutter, element.c).Apply(bounds) if element.vertical { barSize := element.track.Dx() element.bar = image.Rect(0, 0, barSize, barSize).Add(element.track.Min) @@ -86,8 +83,8 @@ func (element *slider) Draw (destination canvas.Canvas) { Focused: element.entity.Focused(), Pressed: element.dragging, } - element.theme.Pattern(tomo.PatternGutter, state).Draw(destination, bounds) - element.theme.Pattern(tomo.PatternHandle, state).Draw(destination, element.bar) + element.entity.Theme().Pattern(tomo.PatternGutter, state, element.c).Draw(destination, bounds) + element.entity.Theme().Pattern(tomo.PatternHandle, state, element.c).Draw(destination, element.bar) } // Focus gives this element input focus. @@ -211,21 +208,12 @@ func (element *slider) OnRelease (callback func ()) { element.onRelease = callback } -// SetTheme sets the element's theme. -func (element *slider) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *slider) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new +func (element *slider) HandleThemeChange () { element.updateMinimumSize() element.entity.Invalidate() } + func (element *slider) changeValue (delta float64) { element.value += delta if element.value < 0 { @@ -258,8 +246,8 @@ func (element *slider) valueFor (x, y int) (value float64) { } func (element *slider) updateMinimumSize () { - gutterPadding := element.theme.Padding(tomo.PatternGutter) - handlePadding := element.theme.Padding(tomo.PatternHandle) + gutterPadding := element.entity.Theme().Padding(tomo.PatternGutter, element.c) + handlePadding := element.entity.Theme().Padding(tomo.PatternHandle, element.c) if element.vertical { element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal(), diff --git a/elements/spacer.go b/elements/spacer.go index 9b0ff4d..1f072df 100644 --- a/elements/spacer.go +++ b/elements/spacer.go @@ -1,25 +1,20 @@ package elements import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" + +var spacerCase = tomo.C("tomo", "spacer") // Spacer can be used to put space between two elements.. type Spacer struct { entity tomo.Entity - line bool - - config config.Wrapped - theme theme.Wrapped } // NewSpacer creates a new spacer. func NewSpacer () (element *Spacer) { element = &Spacer { } - element.entity = tomo.NewEntity(element) - element.theme.Case = tomo.C("tomo", "spacer") + element.entity = tomo.GetBackend().NewEntity(element) element.updateMinimumSize() return } @@ -37,18 +32,18 @@ func (element *Spacer) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *Spacer) Draw (destination canvas.Canvas) { +func (element *Spacer) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() if element.line { - pattern := element.theme.Pattern ( + pattern := element.entity.Theme().Pattern ( tomo.PatternLine, - tomo.State { }) + tomo.State { }, spacerCase) pattern.Draw(destination, bounds) } else { - pattern := element.theme.Pattern ( + pattern := element.entity.Theme().Pattern ( tomo.PatternBackground, - tomo.State { }) + tomo.State { }, spacerCase) pattern.Draw(destination, bounds) } } @@ -61,23 +56,13 @@ func (element *Spacer) SetLine (line bool) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *Spacer) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *Spacer) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new +func (element *Spacer) HandleThemeChange () { element.entity.Invalidate() } func (element *Spacer) updateMinimumSize () { if element.line { - padding := element.theme.Padding(tomo.PatternLine) + padding := element.entity.Theme().Padding(tomo.PatternLine, spacerCase) element.entity.SetMinimumSize ( padding.Horizontal(), padding.Vertical()) diff --git a/elements/switch.go b/elements/switch.go index 45a8ef2..dd6ea85 100644 --- a/elements/switch.go +++ b/elements/switch.go @@ -3,15 +3,15 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" + +var switchCase = tomo.C("tomo", "switch") // Switch is a toggle-able on/off switch with an optional label. It is // functionally identical to Checkbox, but plays a different semantic role. type Switch struct { - entity tomo.FocusableEntity + entity tomo.Entity drawer textdraw.Drawer enabled bool @@ -19,9 +19,6 @@ type Switch struct { checked bool text string - config config.Wrapped - theme theme.Wrapped - onToggle func () } @@ -32,11 +29,10 @@ func NewSwitch (text string, on bool) (element *Switch) { text: text, enabled: true, } - element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) - element.theme.Case = tomo.C("tomo", "switch") - element.drawer.SetFace (element.theme.FontFace ( + element.entity = tomo.GetBackend().NewEntity(element) + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) + tomo.FontSizeNormal, switchCase)) element.drawer.SetText([]rune(text)) element.updateMinimumSize() return @@ -48,7 +44,7 @@ func (element *Switch) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *Switch) Draw (destination canvas.Canvas) { +func (element *Switch) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min) @@ -76,24 +72,24 @@ func (element *Switch) Draw (destination canvas.Canvas) { } } - gutterPattern := element.theme.Pattern ( - tomo.PatternGutter, state) + gutterPattern := element.entity.Theme().Pattern ( + tomo.PatternGutter, state, switchCase) gutterPattern.Draw(destination, gutterBounds) - handlePattern := element.theme.Pattern ( - tomo.PatternHandle, state) + handlePattern := element.entity.Theme().Pattern ( + tomo.PatternHandle, state, switchCase) handlePattern.Draw(destination, handleBounds) textBounds := element.drawer.LayoutBounds() offset := bounds.Min.Add(image.Point { X: bounds.Dy() * 2 + - element.theme.Margin(tomo.PatternBackground).X, + element.entity.Theme().Margin(tomo.PatternBackground, switchCase).X, }) offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Color(tomo.ColorForeground, state) + foreground := element.entity.Theme().Color(tomo.ColorForeground, state, switchCase) element.drawer.Draw(destination, foreground, offset) } @@ -186,21 +182,10 @@ func (element *Switch) SetText (text string) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *Switch) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawer.SetFace (element.theme.FontFace ( +func (element *Switch) HandleThemeChange () { + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *Switch) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new + tomo.FontSizeNormal, switchCase)) element.updateMinimumSize() element.entity.Invalidate() } @@ -214,7 +199,7 @@ func (element *Switch) updateMinimumSize () { } else { element.entity.SetMinimumSize ( lineHeight * 2 + - element.theme.Margin(tomo.PatternBackground).X + + element.entity.Theme().Margin(tomo.PatternBackground, switchCase).X + textBounds.Dx(), lineHeight) } diff --git a/elements/testing/artist.go b/elements/testing/artist.go index a7546a2..25be3a3 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -5,11 +5,11 @@ import "time" import "image" import "image/color" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/shatter" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" +import "git.tebibyte.media/sashakoshka/tomo/artist/artutil" import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font" @@ -22,7 +22,7 @@ type Artist struct { // NewArtist creates a new artist test element. func NewArtist () (element *Artist) { element = &Artist { } - element.entity = tomo.NewEntity(element) + element.entity = tomo.GetBackend().NewEntity(element) element.entity.SetMinimumSize(240, 240) return } @@ -31,7 +31,7 @@ func (element *Artist) Entity () tomo.Entity { return element.entity } -func (element *Artist) Draw (destination canvas.Canvas) { +func (element *Artist) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() patterns.Uhex(0x000000FF).Draw(destination, bounds) @@ -44,28 +44,28 @@ func (element *Artist) Draw (destination canvas.Canvas) { // 4, 0 c40 := element.cellAt(destination, 4, 0) - shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1) + shapes.StrokeColorRectangle(c40, artutil.Hex(0x888888FF), c40.Bounds(), 1) shapes.ColorLine ( - c40, artist.Hex(0xFF0000FF), 1, + c40, artutil.Hex(0xFF0000FF), 1, c40.Bounds().Min, c40.Bounds().Max) // 0, 1 c01 := element.cellAt(destination, 0, 1) - shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1) - shapes.FillColorEllipse(destination, artist.Hex(0x00FF00FF), c01.Bounds()) + shapes.StrokeColorRectangle(c01, artutil.Hex(0x888888FF), c01.Bounds(), 1) + shapes.FillColorEllipse(destination, artutil.Hex(0x00FF00FF), c01.Bounds()) // 1, 1 - 3, 1 for x := 1; x < 4; x ++ { c := element.cellAt(destination, x, 1) shapes.StrokeColorRectangle ( - destination, artist.Hex(0x888888FF), + destination, artutil.Hex(0x888888FF), c.Bounds(), 1) shapes.StrokeColorEllipse ( destination, []color.RGBA { - artist.Hex(0xFF0000FF), - artist.Hex(0x00FF00FF), - artist.Hex(0xFF00FFFF), + artutil.Hex(0xFF0000FF), + artutil.Hex(0x00FF00FF), + artutil.Hex(0xFF00FFFF), } [x - 1], c.Bounds(), x) } @@ -93,12 +93,12 @@ func (element *Artist) Draw (destination canvas.Canvas) { // 0, 2 c02 := element.cellAt(destination, 0, 2) - shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1) + shapes.StrokeColorRectangle(c02, artutil.Hex(0x888888FF), c02.Bounds(), 1) shapes.FillEllipse(c02, c41, c02.Bounds()) // 1, 2 c12 := element.cellAt(destination, 1, 2) - shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1) + shapes.StrokeColorRectangle(c12, artutil.Hex(0x888888FF), c12.Bounds(), 1) shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5) // 2, 2 @@ -135,7 +135,7 @@ func (element *Artist) Draw (destination canvas.Canvas) { patterns.Border { Canvas: element.thingy(c42), Inset: artist.Inset { 8, 8, 8, 8 }, - }.Draw(canvas.Cut(c23, c23.Bounds().Inset(16)), c23.Bounds()) + }.Draw(artist.Cut(c23, c23.Bounds().Inset(16)), c23.Bounds()) // how long did that take to render? drawTime := time.Since(drawStart) @@ -146,13 +146,13 @@ func (element *Artist) Draw (destination canvas.Canvas) { drawTime.Milliseconds(), drawTime.Microseconds()))) textDrawer.Draw ( - destination, artist.Hex(0xFFFFFFFF), + destination, artutil.Hex(0xFFFFFFFF), image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24)) } -func (element *Artist) colorLines (destination canvas.Canvas, weight int, bounds image.Rectangle) { +func (element *Artist) colorLines (destination artist.Canvas, weight int, bounds image.Rectangle) { bounds = bounds.Inset(4) - c := artist.Hex(0xFFFFFFFF) + c := artutil.Hex(0xFFFFFFFF) shapes.ColorLine(destination, c, weight, bounds.Min, bounds.Max) shapes.ColorLine ( destination, c, weight, @@ -184,24 +184,24 @@ func (element *Artist) colorLines (destination canvas.Canvas, weight int, bounds image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y)) } -func (element *Artist) cellAt (destination canvas.Canvas, x, y int) (canvas.Canvas) { +func (element *Artist) cellAt (destination artist.Canvas, x, y int) (artist.Canvas) { bounds := element.entity.Bounds() cellBounds := image.Rectangle { } cellBounds.Min = bounds.Min cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5 cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4 - return canvas.Cut (destination, cellBounds.Add (image.Pt ( + return artist.Cut (destination, cellBounds.Add (image.Pt ( x * cellBounds.Dx(), y * cellBounds.Dy()))) } -func (element *Artist) thingy (destination canvas.Canvas) (result canvas.Canvas) { +func (element *Artist) thingy (destination artist.Canvas) (result artist.Canvas) { bounds := destination.Bounds() bounds = image.Rect(0, 0, 32, 32).Add(bounds.Min) - shapes.FillColorRectangle(destination, artist.Hex(0x440000FF), bounds) - shapes.StrokeColorRectangle(destination, artist.Hex(0xFF0000FF), bounds, 1) - shapes.StrokeColorRectangle(destination, artist.Hex(0x004400FF), bounds.Inset(4), 1) - shapes.FillColorRectangle(destination, artist.Hex(0x004444FF), bounds.Inset(12)) - shapes.StrokeColorRectangle(destination, artist.Hex(0x888888FF), bounds.Inset(8), 1) - return canvas.Cut(destination, bounds) + shapes.FillColorRectangle(destination, artutil.Hex(0x440000FF), bounds) + shapes.StrokeColorRectangle(destination, artutil.Hex(0xFF0000FF), bounds, 1) + shapes.StrokeColorRectangle(destination, artutil.Hex(0x004400FF), bounds.Inset(4), 1) + shapes.FillColorRectangle(destination, artutil.Hex(0x004444FF), bounds.Inset(12)) + shapes.StrokeColorRectangle(destination, artutil.Hex(0x888888FF), bounds.Inset(8), 1) + return artist.Cut(destination, bounds) } diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 5f4a401..29be390 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -4,10 +4,10 @@ import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist/artutil" + +var mouseCase = tomo.C("tomo", "mouse") // Mouse is an element capable of testing mouse input. When the mouse is clicked // and dragged on it, it draws a trail. @@ -15,16 +15,12 @@ type Mouse struct { entity tomo.Entity pressed bool lastMousePos image.Point - - config config.Wrapped - theme theme.Wrapped } // NewMouse creates a new mouse test element. func NewMouse () (element *Mouse) { element = &Mouse { } - element.theme.Case = tomo.C("tomo", "mouse") - element.entity = tomo.NewEntity(element) + element.entity = tomo.GetBackend().NewEntity(element) element.entity.SetMinimumSize(32, 32) return } @@ -33,41 +29,34 @@ func (element *Mouse) Entity () tomo.Entity { return element.entity } -func (element *Mouse) Draw (destination canvas.Canvas) { +func (element *Mouse) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() - accent := element.theme.Color ( + accent := element.entity.Theme().Color ( tomo.ColorAccent, - tomo.State { }) + tomo.State { }, + mouseCase) shapes.FillColorRectangle(destination, accent, bounds) shapes.StrokeColorRectangle ( destination, - artist.Hex(0x000000FF), + artutil.Hex(0x000000FF), bounds, 1) shapes.ColorLine ( - destination, artist.Hex(0xFFFFFFFF), 1, + destination, artutil.Hex(0xFFFFFFFF), 1, bounds.Min.Add(image.Pt(1, 1)), bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2))) shapes.ColorLine ( - destination, artist.Hex(0xFFFFFFFF), 1, + destination, artutil.Hex(0xFFFFFFFF), 1, bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)), bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1))) if element.pressed { midpoint := bounds.Min.Add(bounds.Max.Sub(bounds.Min).Div(2)) shapes.ColorLine ( - destination, artist.Hex(0x000000FF), 1, + destination, artutil.Hex(0x000000FF), 1, midpoint, element.lastMousePos) } } -// SetTheme sets the element's theme. -func (element *Mouse) SetTheme (new tomo.Theme) { - element.theme.Theme = new - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *Mouse) SetConfig (new tomo.Config) { - element.config.Config = new +func (element *Mouse) HandleThemeChange (new tomo.Theme) { element.entity.Invalidate() } diff --git a/elements/textbox.go b/elements/textbox.go index 5444ea0..81cc5e0 100644 --- a/elements/textbox.go +++ b/elements/textbox.go @@ -7,23 +7,16 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/textmanip" import "git.tebibyte.media/sashakoshka/tomo/fixedutil" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" -type textBoxEntity interface { - tomo.FocusableEntity - tomo.ScrollableEntity - tomo.LayoutEntity -} +var textBoxCase = tomo.C("tomo", "textBox") // TextBox is a single-line text input. type TextBox struct { - entity textBoxEntity + entity tomo.Entity enabled bool lastClick time.Time @@ -36,9 +29,6 @@ type TextBox struct { placeholderDrawer textdraw.Drawer valueDrawer textdraw.Drawer - config config.Wrapped - theme theme.Wrapped - onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool) onChange func () onEnter func () @@ -50,15 +40,14 @@ type TextBox struct { // text. func NewTextBox (placeholder, value string) (element *TextBox) { element = &TextBox { enabled: true } - element.theme.Case = tomo.C("tomo", "textBox") - element.entity = tomo.NewEntity(element).(textBoxEntity) + element.entity = tomo.GetBackend().NewEntity(element) element.placeholder = placeholder - element.placeholderDrawer.SetFace (element.theme.FontFace ( + element.placeholderDrawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.valueDrawer.SetFace (element.theme.FontFace ( + tomo.FontSizeNormal, textBoxCase)) + element.valueDrawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) + tomo.FontSizeNormal, textBoxCase)) element.placeholderDrawer.SetText([]rune(placeholder)) element.updateMinimumSize() element.SetValue(value) @@ -71,19 +60,19 @@ func (element *TextBox) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *TextBox) Draw (destination canvas.Canvas) { +func (element *TextBox) Draw (destination artist.Canvas) { bounds := element.entity.Bounds() state := element.state() - pattern := element.theme.Pattern(tomo.PatternInput, state) - padding := element.theme.Padding(tomo.PatternInput) - innerCanvas := canvas.Cut(destination, padding.Apply(bounds)) + pattern := element.entity.Theme().Pattern(tomo.PatternInput, state, textBoxCase) + padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase) + innerCanvas := artist.Cut(destination, padding.Apply(bounds)) pattern.Draw(destination, bounds) offset := element.textOffset() if element.entity.Focused() && !element.dot.Empty() { // draw selection bounds - accent := element.theme.Color(tomo.ColorAccent, state) + accent := element.entity.Theme().Color(tomo.ColorAccent, state, textBoxCase) canon := element.dot.Canon() foff := fixedutil.Pt(offset) start := element.valueDrawer.PositionAt(canon.Start).Add(foff) @@ -101,9 +90,9 @@ func (element *TextBox) Draw (destination canvas.Canvas) { if len(element.text) == 0 { // draw placeholder textBounds := element.placeholderDrawer.LayoutBounds() - foreground := element.theme.Color ( + foreground := element.entity.Theme().Color ( tomo.ColorForeground, - tomo.State { Disabled: true }) + tomo.State { Disabled: true }, textBoxCase) element.placeholderDrawer.Draw ( innerCanvas, foreground, @@ -111,7 +100,7 @@ func (element *TextBox) Draw (destination canvas.Canvas) { } else { // draw input value textBounds := element.valueDrawer.LayoutBounds() - foreground := element.theme.Color(tomo.ColorForeground, state) + foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase) element.valueDrawer.Draw ( innerCanvas, foreground, @@ -120,7 +109,7 @@ func (element *TextBox) Draw (destination canvas.Canvas) { if element.entity.Focused() && element.dot.Empty() { // draw cursor - foreground := element.theme.Color(tomo.ColorForeground, state) + foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase) cursorPosition := fixedutil.RoundPt ( element.valueDrawer.PositionAt(element.dot.End)) shapes.ColorLine ( @@ -156,7 +145,7 @@ func (element *TextBox) HandleMouseDown ( runeIndex := element.atPosition(position) if runeIndex == -1 { return } - if time.Since(element.lastClick) < element.config.DoubleClickDelay() { + if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() { element.dragging = 2 element.dot = textmanip.WordAround(element.text, runeIndex) } else { @@ -214,7 +203,7 @@ func (element *TextBox) HandleMotion (position image.Point) { } func (element *TextBox) textOffset () image.Point { - padding := element.theme.Padding(tomo.PatternInput) + padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase) bounds := element.entity.Bounds() innerBounds := padding.Apply(bounds) textHeight := element.valueDrawer.LineHeight().Round() @@ -476,27 +465,17 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) { return true, false } -// SetTheme sets the element's theme. -func (element *TextBox) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - face := element.theme.FontFace ( +func (element *TextBox) HandleThemeChange () { + face := element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal) + tomo.FontSizeNormal, + textBoxCase) element.placeholderDrawer.SetFace(face) element.valueDrawer.SetFace(face) element.updateMinimumSize() element.entity.Invalidate() } -// SetConfig sets the element's configuration. -func (element *TextBox) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - element.updateMinimumSize() - element.entity.Invalidate() -} - func (element *TextBox) contextMenu (position image.Point) { window := element.entity.Window() menu, err := window.NewMenu(image.Rectangle { position, position }) @@ -540,12 +519,12 @@ func (element *TextBox) runOnChange () { } func (element *TextBox) scrollViewportWidth () (width int) { - padding := element.theme.Padding(tomo.PatternInput) + padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase) return padding.Apply(element.entity.Bounds()).Dx() } func (element *TextBox) scrollToCursor () { - padding := element.theme.Padding(tomo.PatternInput) + padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase) bounds := padding.Apply(element.entity.Bounds()) bounds = bounds.Sub(bounds.Min) bounds.Max.X -= element.valueDrawer.Em().Round() @@ -568,7 +547,7 @@ func (element *TextBox) scrollToCursor () { func (element *TextBox) updateMinimumSize () { textBounds := element.placeholderDrawer.LayoutBounds() - padding := element.theme.Padding(tomo.PatternInput) + padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase) element.entity.SetMinimumSize ( padding.Horizontal() + textBounds.Dx(), padding.Vertical() + diff --git a/elements/togglebutton.go b/elements/togglebutton.go index 8ee204d..55e0e1e 100644 --- a/elements/togglebutton.go +++ b/elements/togglebutton.go @@ -3,23 +3,20 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/textdraw" +var toggleButtonCase = tomo.C("tomo", "toggleButton") + // ToggleButton is a togglable button. type ToggleButton struct { - entity tomo.FocusableEntity + entity tomo.Entity drawer textdraw.Drawer enabled bool pressed bool on bool text string - - config config.Wrapped - theme theme.Wrapped showText bool hasIcon bool @@ -35,11 +32,11 @@ func NewToggleButton (text string, on bool) (element *ToggleButton) { enabled: true, on: on, } - element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) - element.theme.Case = tomo.C("tomo", "toggleButton") - element.drawer.SetFace (element.theme.FontFace ( + element.entity = tomo.GetBackend().NewEntity(element) + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) + tomo.FontSizeNormal, + toggleButtonCase)) element.SetText(text) return } @@ -50,13 +47,13 @@ func (element *ToggleButton) Entity () tomo.Entity { } // Draw causes the element to draw to the specified destination canvas. -func (element *ToggleButton) Draw (destination canvas.Canvas) { +func (element *ToggleButton) Draw (destination artist.Canvas) { state := element.state() bounds := element.entity.Bounds() - pattern := element.theme.Pattern(tomo.PatternButton, state) + pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, toggleButtonCase) - lampPattern := element.theme.Pattern(tomo.PatternLamp, state) - lampPadding := element.theme.Padding(tomo.PatternLamp).Horizontal() + lampPattern := element.entity.Theme().Pattern(tomo.PatternLamp, state, toggleButtonCase) + lampPadding := element.entity.Theme().Padding(tomo.PatternLamp, toggleButtonCase).Horizontal() lampBounds := bounds lampBounds.Max.X = lampBounds.Min.X + lampPadding bounds.Min.X += lampPadding @@ -64,9 +61,9 @@ func (element *ToggleButton) Draw (destination canvas.Canvas) { pattern.Draw(destination, bounds) lampPattern.Draw(destination, lampBounds) - foreground := element.theme.Color(tomo.ColorForeground, state) - sink := element.theme.Sink(tomo.PatternButton) - margin := element.theme.Margin(tomo.PatternButton) + foreground := element.entity.Theme().Color(tomo.ColorForeground, state, toggleButtonCase) + sink := element.entity.Theme().Sink(tomo.PatternButton, toggleButtonCase) + margin := element.entity.Theme().Margin(tomo.PatternButton, toggleButtonCase) offset := image.Pt ( bounds.Dx() / 2, @@ -81,7 +78,7 @@ func (element *ToggleButton) Draw (destination canvas.Canvas) { } if element.hasIcon { - icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) + icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase) if icon != nil { iconBounds := icon.Bounds() addedWidth := iconBounds.Dx() @@ -171,21 +168,10 @@ func (element *ToggleButton) ShowText (showText bool) { element.entity.Invalidate() } -// SetTheme sets the element's theme. -func (element *ToggleButton) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawer.SetFace (element.theme.FontFace ( +func (element *ToggleButton) HandleThemeChange () { + element.drawer.SetFace (element.entity.Theme().FontFace ( tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *ToggleButton) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new + tomo.FontSizeNormal, toggleButtonCase)) element.updateMinimumSize() element.entity.Invalidate() } @@ -244,15 +230,15 @@ func (element *ToggleButton) HandleKeyUp(key input.Key, modifiers input.Modifier } func (element *ToggleButton) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternButton) - margin := element.theme.Margin(tomo.PatternButton) - lampPadding := element.theme.Padding(tomo.PatternLamp) + padding := element.entity.Theme().Padding(tomo.PatternButton, toggleButtonCase) + margin := element.entity.Theme().Margin(tomo.PatternButton, toggleButtonCase) + lampPadding := element.entity.Theme().Padding(tomo.PatternLamp, toggleButtonCase) textBounds := element.drawer.LayoutBounds() minimumSize := textBounds.Sub(textBounds.Min) if element.hasIcon { - icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) + icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase) if icon != nil { bounds := icon.Bounds() if element.showText { diff --git a/entity.go b/entity.go index bdeb562..6dc279f 100644 --- a/entity.go +++ b/entity.go @@ -1,16 +1,21 @@ package tomo import "image" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" -// Entity is a handle given to elements by the backend. Different types of -// entities may be assigned to elements that support different capabilities. +// Entity is a handle given to elements by the backend. Extended entity +// interfaces are defined in the ability module. type Entity interface { // Invalidate marks the element's current visual as invalid. At the end // of every event, the backend will ask all invalid entities to redraw // themselves. Invalidate () + // InvalidateLayout marks the element's layout as invalid. At the end of + // every event, the backend will ask all invalid elements to recalculate + // their layouts. + InvalidateLayout () + // Bounds returns the bounds of the element to be used for drawing and // layout. Bounds () image.Rectangle @@ -28,23 +33,9 @@ type Entity interface { // labels. If there is no parent element (that is, the element is // directly inside of the window), the backend will draw a default // background pattern. - DrawBackground (canvas.Canvas) -} + DrawBackground (artist.Canvas) -// LayoutEntity is given to elements that support the Layoutable interface. -type LayoutEntity interface { - Entity - - // InvalidateLayout marks the element's layout as invalid. At the end of - // every event, the backend will ask all invalid elements to recalculate - // their layouts. - InvalidateLayout () -} - -// ContainerEntity is given to elements that support the Container interface. -type ContainerEntity interface { - Entity - LayoutEntity + // --- Behaviors relating to parenting --- // Adopt adds an element as a child. Adopt (child Element) @@ -76,11 +67,8 @@ type ContainerEntity interface { // ChildMinimumSize returns the minimum size of the child at the // specified index. ChildMinimumSize (index int) (width, height int) -} -// FocusableEntity is given to elements that support the Focusable interface. -type FocusableEntity interface { - Entity + // --- Behaviors relating to input focus --- // Focused returns whether the element currently has input focus. Focused () bool @@ -96,34 +84,31 @@ type FocusableEntity interface { // FocusPrevious causes the focus to move to the next element. If this // succeeds, the element will recieve a HandleUnfocus call. FocusPrevious () -} - -// SelectableEntity is given to elements that support the Selectable interface. -type SelectableEntity interface { - Entity // Selected returns whether this element is currently selected. Selected () bool -} -// FlexibleEntity is given to elements that support the Flexible interface. -type FlexibleEntity interface { - Entity + // --- Behaviors relating to scrolling --- // // NotifyFlexibleHeightChange notifies the system that the parameters // affecting the element's flexible height have changed. This method is // expected to be called by flexible elements when their content changes. NotifyFlexibleHeightChange () -} -// ScrollableEntity is given to elements that support the Scrollable interface. -type ScrollableEntity interface { - Entity - // NotifyScrollBoundsChange notifies the system that the element's // scroll content bounds or viewport bounds have changed. This is // expected to be called by scrollable elements when they change their // supported scroll axes, their scroll position (either autonomously or // as a result of a call to ScrollTo()), or their content size. NotifyScrollBoundsChange () + + // --- Behaviors relating to themeing --- + + // Theme returns the currently active theme. When this value changes, + // the HandleThemeChange method of the element is called. + Theme () Theme + + // Config returns the currently active config. When this value changes, + // the HandleThemeChange method of the element is called. + Config () Config } diff --git a/examples/align/main.go b/examples/align/main.go deleted file mode 100644 index 1857b42..0000000 --- a/examples/align/main.go +++ /dev/null @@ -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." diff --git a/examples/button/main.go b/examples/button/main.go deleted file mode 100644 index d5d7cc3..0000000 --- a/examples/button/main.go +++ /dev/null @@ -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() -} diff --git a/examples/checkbox/main.go b/examples/checkbox/main.go deleted file mode 100644 index 549e54d..0000000 --- a/examples/checkbox/main.go +++ /dev/null @@ -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() -} diff --git a/examples/clipboard/main.go b/examples/clipboard/main.go index 4102686..8eb2539 100644 --- a/examples/clipboard/main.go +++ b/examples/clipboard/main.go @@ -7,13 +7,9 @@ import _ "image/gif" import _ "image/jpeg" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" - -func main () { - tomo.Run(run) -} var validImageTypes = []data.Mime { data.M("image", "png"), @@ -21,8 +17,15 @@ var validImageTypes = []data.Mime { data.M("image", "jpeg"), } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0)) +func main () { + nasin.Run(Application { }) +} + +type Application struct { } + +func (Application) Init () error { + window, err:= nasin.NewWindow(tomo.Bounds(0, 0, 256, 0)) + if err != nil { return err } window.SetTitle("Clipboard") container := elements.NewVBox(elements.SpaceBoth) @@ -114,8 +117,9 @@ func run () { container.Adopt(controlRow) window.Adopt(container) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } func imageWindow (parent tomo.Window, image image.Image) { diff --git a/examples/documentContainer/main.go b/examples/document/main.go similarity index 84% rename from examples/documentContainer/main.go rename to examples/document/main.go index 146137b..2ecd67b 100644 --- a/examples/documentContainer/main.go +++ b/examples/document/main.go @@ -4,22 +4,25 @@ import "os" import "image" import _ "image/png" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 383, 360)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 383, 360)) + if err != nil { return err } window.SetTitle("Document Container") file, err := os.Open("assets/banner.png") - if err != nil { panic(err.Error()); return } + if err != nil { return err } logo, _, err := image.Decode(file) file.Close() - if err != nil { panic(err.Error()); return } + if err != nil { return err } document := elements.NewDocument() document.Adopt ( @@ -56,6 +59,7 @@ func run () { } window.Adopt(elements.NewScroll(elements.ScrollVertical, document)) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } diff --git a/examples/drawing/main.go b/examples/drawing/main.go index 4c8db43..00e47bb 100644 --- a/examples/drawing/main.go +++ b/examples/drawing/main.go @@ -1,18 +1,22 @@ package main import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/elements/testing" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/ezprof/ez" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 480, 360)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 480, 360)) + if err != nil { return err } window.Adopt(testing.NewArtist()) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() ez.Prof() + return nil } diff --git a/examples/fileBrowser/main.go b/examples/fileBrowser/main.go index 842df94..2c1bf19 100644 --- a/examples/fileBrowser/main.go +++ b/examples/fileBrowser/main.go @@ -3,19 +3,23 @@ package main import "os" import "path/filepath" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 384, 384)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 384, 384)) + if err != nil { return err } window.SetTitle("File browser") container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) - homeDir, _ := os.UserHomeDir() + homeDir, err := os.UserHomeDir() + if err != nil { return err } controlBar := elements.NewHBox(elements.SpaceNone) backButton := elements.NewButton("Back") @@ -78,6 +82,7 @@ func run () { elements.NewScroll(elements.ScrollVertical, directoryView)) container.Adopt(statusBar) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } diff --git a/examples/flow/main.go b/examples/flow/main.go deleted file mode 100644 index dac69cc..0000000 --- a/examples/flow/main.go +++ /dev/null @@ -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() -} diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index bfbb508..7994031 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -1,19 +1,20 @@ package main -import "os" import "time" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/fun" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) - os.Exit(0) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 200, 216)) + if err != nil { return err } window.SetTitle("Clock") window.SetApplicationName("TomoClock") container := elements.NewVBox(elements.SpaceBoth) @@ -24,9 +25,10 @@ func run () { container.AdoptExpand(clock) container.Adopt(label) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() go tick(label, clock) + return nil } func formatTime () (timeString string) { @@ -35,7 +37,7 @@ func formatTime () (timeString string) { func tick (label *elements.Label, clock *fun.AnalogClock) { for { - tomo.Do (func () { + nasin.Do (func () { label.SetText(formatTime()) clock.SetTime(time.Now()) }) diff --git a/examples/hbox/main.go b/examples/hbox/main.go deleted file mode 100644 index b469ae7..0000000 --- a/examples/hbox/main.go +++ /dev/null @@ -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() -} diff --git a/examples/icons/main.go b/examples/icons/main.go index 01ed2f5..a9e2417 100644 --- a/examples/icons/main.go +++ b/examples/icons/main.go @@ -1,15 +1,18 @@ package main import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 360, 0)) + if err != nil { return err } window.SetTitle("Icons") container := elements.NewVBox(elements.SpaceBoth) @@ -26,11 +29,12 @@ func run () { closeButton := elements.NewButton("Yes verynice") closeButton.SetIcon(tomo.IconYes) - closeButton.OnClick(tomo.Stop) + closeButton.OnClick(window.Close) container.Adopt(closeButton) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } func icons (min, max tomo.Icon) (container *elements.Box) { diff --git a/examples/image/image.go b/examples/image/image.go index 0dc2100..daa9b60 100644 --- a/examples/image/image.go +++ b/examples/image/image.go @@ -6,23 +6,25 @@ import "bytes" import _ "image/png" import "github.com/jezek/xgbutil/gopher" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) +type Application struct { } + +func (Application) Init () error { + window, _ := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Tomo Logo") file, err := os.Open("assets/banner.png") - if err != nil { fatalError(window, err); return } + if err != nil { return err } logo, _, err := image.Decode(file) file.Close() - if err != nil { fatalError(window, err); return } + if err != nil { return err } container := elements.NewVBox(elements.SpaceBoth) logoImage := elements.NewImage(logo) @@ -42,8 +44,9 @@ func run () { button.Focus() - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } func fatalError (window tomo.Window, err error) { @@ -54,7 +57,7 @@ func fatalError (window tomo.Window, err error) { err.Error(), popups.Button { Name: "OK", - OnPress: tomo.Stop, + OnPress: nasin.Stop, }) } diff --git a/examples/input/main.go b/examples/input/main.go index 088e08f..ce2cf13 100644 --- a/examples/input/main.go +++ b/examples/input/main.go @@ -1,16 +1,19 @@ package main import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0)) + if err != nil { return err } window.SetTitle("Enter Details") container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) @@ -59,6 +62,7 @@ func run () { elements.NewLabel("Purpose:"), purpose, elements.NewLine(), button) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } diff --git a/examples/label/main.go b/examples/label/main.go index 96dc6a8..034bc92 100644 --- a/examples/label/main.go +++ b/examples/label/main.go @@ -1,19 +1,23 @@ package main import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 480, 360)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 480, 360)) + if err != nil { return err } window.SetTitle("example label") window.Adopt(elements.NewLabelWrapped(text)) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } const text = "Resize the window to see the text wrap:\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Fermentum et sollicitudin ac orci phasellus egestas tellus rutrum. Aliquam vestibulum morbi blandit cursus risus at ultrices mi. Gravida dictum fusce ut placerat. Cursus metus aliquam eleifend mi in nulla posuere. Sit amet nulla facilisi morbi tempus iaculis urna id. Amet volutpat consequat mauris nunc congue nisi vitae. Varius duis at consectetur lorem donec massa sapien faucibus et. Vitae elementum curabitur vitae nunc sed velit dignissim. In hac habitasse platea dictumst quisque sagittis purus. Enim nulla aliquet porttitor lacus luctus accumsan tortor. Lectus magna fringilla urna porttitor rhoncus dolor purus non.\n\nNon pulvinar neque laoreet suspendisse. Viverra adipiscing at in tellus integer. Vulputate dignissim suspendisse in est ante. Purus in mollis nunc sed id semper. In est ante in nibh mauris cursus. Risus pretium quam vulputate dignissim suspendisse in est. Blandit aliquam etiam erat velit scelerisque in dictum. Lectus quam id leo in. Odio tempor orci dapibus ultrices in iaculis. Pharetra sit amet aliquam id. Elit ut aliquam purus sit. Egestas dui id ornare arcu odio ut sem nulla pharetra. Massa tempor nec feugiat nisl pretium fusce id. Dui accumsan sit amet nulla facilisi morbi. A lacus vestibulum sed arcu non odio euismod. Nam libero justo laoreet sit amet cursus. Mattis rhoncus urna neque viverra justo nec. Mauris augue neque gravida in fermentum et sollicitudin ac. Vulputate mi sit amet mauris. Ut sem nulla pharetra diam sit amet." diff --git a/examples/list/main.go b/examples/list/main.go index eb09fef..e709f2e 100644 --- a/examples/list/main.go +++ b/examples/list/main.go @@ -1,17 +1,21 @@ package main import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/popups" +import "git.tebibyte.media/sashakoshka/tomo/ability" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/testing" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 300, 0)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 300, 0)) + if err != nil { return err } window.SetTitle("List Sidebar") container := elements.NewHBox(elements.SpaceBoth) @@ -44,7 +48,7 @@ func run () { elements.NewCheckbox("Bone", false)) art := testing.NewArtist() - makePage := func (name string, callback func ()) tomo.Selectable { + makePage := func (name string, callback func ()) ability.Selectable { cell := elements.NewCell(elements.NewLabel(name)) cell.OnSelectionChange (func () { if cell.Selected() { callback() } @@ -63,6 +67,7 @@ func run () { container.Adopt(list) turnPage(intro) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } diff --git a/examples/panels/main.go b/examples/panels/main.go index b28b204..66ce8a1 100644 --- a/examples/panels/main.go +++ b/examples/panels/main.go @@ -3,15 +3,18 @@ package main import "fmt" import "image" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(200, 200, 256, 256)) + if err != nil { return err } window.SetTitle("Main") container := elements.NewVBox ( @@ -19,13 +22,14 @@ func run () { elements.NewLabel("Main window")) window.Adopt(container) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() createPanel(window, 0, tomo.Bounds(-64, 20, 0, 0)) createPanel(window, 1, tomo.Bounds(200, 20, 0, 0)) createPanel(window, 2, tomo.Bounds(-64, 180, 0, 0)) createPanel(window, 3, tomo.Bounds(200, 180, 0, 0)) + return nil } func createPanel (parent tomo.MainWindow, id int, bounds image.Rectangle) { diff --git a/examples/piano/main.go b/examples/piano/main.go deleted file mode 100644 index 2783bc9..0000000 --- a/examples/piano/main.go +++ /dev/null @@ -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 -} diff --git a/examples/popups/main.go b/examples/popups/main.go index 4b2c83a..d1ba9cc 100644 --- a/examples/popups/main.go +++ b/examples/popups/main.go @@ -1,17 +1,19 @@ package main import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, err := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) - if err != nil { panic(err.Error()) } +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0)) + if err != nil { return err } window.SetTitle("Dialog Boxes") container := elements.NewVBox(elements.SpaceBoth) @@ -76,9 +78,10 @@ func run () { container.Adopt(menuButton) cancelButton := elements.NewButton("No thank you.") - cancelButton.OnClick(tomo.Stop) + cancelButton.OnClick(nasin.Stop) container.Adopt(cancelButton) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } diff --git a/examples/progress/main.go b/examples/progress/main.go index 0fad66b..6be1f32 100644 --- a/examples/progress/main.go +++ b/examples/progress/main.go @@ -2,16 +2,19 @@ package main import "time" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0)) + if err != nil { return err } window.SetTitle("Approaching") container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) @@ -23,19 +26,20 @@ func run () { button.SetEnabled(false) container.Adopt(button) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() go fill(window, bar) + return nil } func fill (window tomo.Window, bar *elements.ProgressBar) { for progress := 0.0; progress < 1.0; progress += 0.01 { time.Sleep(time.Second / 24) - tomo.Do (func () { + nasin.Do (func () { bar.SetProgress(progress) }) } - tomo.Do (func () { + nasin.Do (func () { popups.NewDialog ( popups.DialogKindInfo, window, diff --git a/examples/raycaster/ambience.mp3 b/examples/raycaster/ambience.mp3 deleted file mode 100644 index 2ff42b9..0000000 Binary files a/examples/raycaster/ambience.mp3 and /dev/null differ diff --git a/examples/raycaster/game.go b/examples/raycaster/game.go deleted file mode 100644 index 35d804a..0000000 --- a/examples/raycaster/game.go +++ /dev/null @@ -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() - } - } -} diff --git a/examples/raycaster/main.go b/examples/raycaster/main.go deleted file mode 100644 index f848c99..0000000 --- a/examples/raycaster/main.go +++ /dev/null @@ -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.") -} diff --git a/examples/raycaster/ray.go b/examples/raycaster/ray.go deleted file mode 100644 index bb913e8..0000000 --- a/examples/raycaster/ray.go +++ /dev/null @@ -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()) -} diff --git a/examples/raycaster/raycaster.go b/examples/raycaster/raycaster.go deleted file mode 100644 index 4360ef4..0000000 --- a/examples/raycaster/raycaster.go +++ /dev/null @@ -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) -} diff --git a/examples/raycaster/texture.go b/examples/raycaster/texture.go deleted file mode 100644 index 21c8a78..0000000 --- a/examples/raycaster/texture.go +++ /dev/null @@ -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 -} diff --git a/examples/raycaster/wall.png b/examples/raycaster/wall.png deleted file mode 100644 index 0d41d7a..0000000 Binary files a/examples/raycaster/wall.png and /dev/null differ diff --git a/examples/scroll/main.go b/examples/scroll/main.go deleted file mode 100644 index f9a226f..0000000 --- a/examples/scroll/main.go +++ /dev/null @@ -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.` diff --git a/examples/spacer/main.go b/examples/spacer/main.go index a537a37..4221ac2 100644 --- a/examples/spacer/main.go +++ b/examples/spacer/main.go @@ -1,15 +1,18 @@ package main import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/nasin" import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { - tomo.Run(run) + nasin.Run(Application { }) } -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) +type Application struct { } + +func (Application) Init () error { + window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0)) + if err != nil { return err } window.SetTitle("Spaced Out") container := elements.NewVBox ( @@ -21,6 +24,7 @@ func run () { container.Adopt(elements.NewLabel("This is at the bottom")) window.Adopt(container) - window.OnClose(tomo.Stop) + window.OnClose(nasin.Stop) window.Show() + return nil } diff --git a/examples/switch/main.go b/examples/switch/main.go deleted file mode 100644 index 42cfbe5..0000000 --- a/examples/switch/main.go +++ /dev/null @@ -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() -} diff --git a/examples/table/main.go b/examples/table/main.go deleted file mode 100644 index 23592f6..0000000 --- a/examples/table/main.go +++ /dev/null @@ -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() -} diff --git a/examples/vbox/main.go b/examples/vbox/main.go deleted file mode 100644 index 3c6bfd9..0000000 --- a/examples/vbox/main.go +++ /dev/null @@ -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() -} diff --git a/nasin/application.go b/nasin/application.go new file mode 100644 index 0000000..31a2844 --- /dev/null +++ b/nasin/application.go @@ -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 +} diff --git a/nasin/doc.go b/nasin/doc.go new file mode 100644 index 0000000..7662aa8 --- /dev/null +++ b/nasin/doc.go @@ -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 diff --git a/nasin/plugin.go b/nasin/plugin.go new file mode 100644 index 0000000..0411845 --- /dev/null +++ b/nasin/plugin.go @@ -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 +} diff --git a/nasin/unix.go b/nasin/unix.go new file mode 100644 index 0000000..0ea10b7 --- /dev/null +++ b/nasin/unix.go @@ -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")) + } +} diff --git a/plugins/wintergreen/main.go b/plugins/wintergreen/main.go new file mode 100644 index 0000000..6d1bcc4 --- /dev/null +++ b/plugins/wintergreen/main.go @@ -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 { } +} diff --git a/plugins/wintergreen/wintergreen/assets/wintergreen-icons-large.png b/plugins/wintergreen/wintergreen/assets/wintergreen-icons-large.png new file mode 100644 index 0000000..c4dae36 Binary files /dev/null and b/plugins/wintergreen/wintergreen/assets/wintergreen-icons-large.png differ diff --git a/plugins/wintergreen/wintergreen/assets/wintergreen-icons-small.png b/plugins/wintergreen/wintergreen/assets/wintergreen-icons-small.png new file mode 100644 index 0000000..203de55 Binary files /dev/null and b/plugins/wintergreen/wintergreen/assets/wintergreen-icons-small.png differ diff --git a/default/theme/assets/wintergreen.png b/plugins/wintergreen/wintergreen/assets/wintergreen.png similarity index 100% rename from default/theme/assets/wintergreen.png rename to plugins/wintergreen/wintergreen/assets/wintergreen.png diff --git a/plugins/wintergreen/wintergreen/doc.go b/plugins/wintergreen/wintergreen/doc.go new file mode 100644 index 0000000..7ac40d8 --- /dev/null +++ b/plugins/wintergreen/wintergreen/doc.go @@ -0,0 +1,2 @@ +// Package wintergreen contains the wintergreen theme. +package wintergreen diff --git a/plugins/wintergreen/wintergreen/wintergreen.go b/plugins/wintergreen/wintergreen/wintergreen.go new file mode 100644 index 0000000..380a506 --- /dev/null +++ b/plugins/wintergreen/wintergreen/wintergreen.go @@ -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 } +} diff --git a/plugins/x/main.go b/plugins/x/main.go new file mode 100644 index 0000000..bce6b24 --- /dev/null +++ b/plugins/x/main.go @@ -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() +} diff --git a/backends/x/doc.go b/plugins/x/x/doc.go similarity index 100% rename from backends/x/doc.go rename to plugins/x/x/doc.go diff --git a/backends/x/encoding.go b/plugins/x/x/encoding.go similarity index 98% rename from backends/x/encoding.go rename to plugins/x/x/encoding.go index cf5a404..296163f 100644 --- a/backends/x/encoding.go +++ b/plugins/x/x/encoding.go @@ -112,7 +112,7 @@ var keypadCodeTable = map[xproto.Keysym] input.Key { // initializeKeymapInformation grabs keyboard mapping information from the X // server. -func (backend *Backend) initializeKeymapInformation () { +func (backend *backend) initializeKeymapInformation () { keybind.Initialize(backend.connection) backend.modifierMasks.capsLock = backend.keysymToMask(0xFFE5) backend.modifierMasks.shiftLock = backend.keysymToMask(0xFFE6) @@ -127,7 +127,7 @@ func (backend *Backend) initializeKeymapInformation () { // keysymToKeycode converts an X keysym to an X keycode, instead of the other // way around. -func (backend *Backend) keysymToKeycode ( +func (backend *backend) keysymToKeycode ( symbol xproto.Keysym, ) ( code xproto.Keycode, @@ -148,7 +148,7 @@ func (backend *Backend) keysymToKeycode ( } // keysymToMask returns the X modmask for a given modifier key. -func (backend *Backend) keysymToMask ( +func (backend *backend) keysymToMask ( symbol xproto.Keysym, ) ( mask uint16, @@ -164,7 +164,7 @@ func (backend *Backend) keysymToMask ( // fleshed out version of some of the logic found in xgbutil/keybind/encoding.go // to get a full keycode to keysym conversion, but eliminates redundant work by // going straight to a tomo keycode. -func (backend *Backend) keycodeToKey ( +func (backend *backend) keycodeToKey ( keycode xproto.Keycode, state uint16, ) ( diff --git a/backends/x/entity.go b/plugins/x/x/entity.go similarity index 80% rename from backends/x/entity.go rename to plugins/x/x/entity.go index b8a097b..7718e57 100644 --- a/backends/x/entity.go +++ b/plugins/x/x/entity.go @@ -2,9 +2,11 @@ package x import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/ability" type entity struct { + backend *backend window *window parent *entity children []*entity @@ -17,15 +19,11 @@ type entity struct { selected bool layoutInvalid bool - isContainer bool } -func (backend *Backend) NewEntity (owner tomo.Element) tomo.Entity { - entity := &entity { element: owner } - if _, ok := owner.(tomo.Container); ok { - entity.isContainer = true - entity.InvalidateLayout() - } +func (backend *backend) NewEntity (owner tomo.Element) tomo.Entity { + entity := &entity { element: owner, backend: backend } + entity.InvalidateLayout() return entity } @@ -44,7 +42,7 @@ func (ent *entity) unlink () { ent.parent = nil ent.window = nil - if element, ok := ent.element.(tomo.Selectable); ok { + if element, ok := ent.element.(ability.Selectable); ok { ent.selected = false element.HandleSelectionChange() } @@ -111,15 +109,15 @@ func (entity *entity) scrollTargetChildAt (point image.Point) *entity { } } - if _, ok := entity.element.(tomo.ScrollTarget); ok { + if _, ok := entity.element.(ability.ScrollTarget); ok { return entity } return nil } -func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer, tomo.Element)) { +func (entity *entity) forMouseTargetContainers (callback func (ability.MouseTargetContainer, tomo.Element)) { if entity.parent == nil { return } - if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok { + if parent, ok := entity.parent.element.(ability.MouseTargetContainer); ok { callback(parent, entity.element) } entity.parent.forMouseTargetContainers(callback) @@ -156,18 +154,19 @@ func (entity *entity) SetMinimumSize (width, height int) { entity.window.setMinimumSize(width, height) } } else { - entity.parent.element.(tomo.Container). + entity.parent.element.(ability.Container). HandleChildMinimumSizeChange(entity.element) } } -func (entity *entity) DrawBackground (destination canvas.Canvas) { +func (entity *entity) DrawBackground (destination artist.Canvas) { if entity.parent != nil { - entity.parent.element.(tomo.Container).DrawBackground(destination) + entity.parent.element.(ability.Container).DrawBackground(destination) } else if entity.window != nil { - entity.window.system.theme.Pattern ( + entity.backend.theme.Pattern ( tomo.PatternBackground, - tomo.State { }).Draw ( + tomo.State { }, + tomo.C("tomo", "window")).Draw ( destination, entity.window.canvas.Bounds()) } @@ -177,7 +176,7 @@ func (entity *entity) DrawBackground (destination canvas.Canvas) { func (entity *entity) InvalidateLayout () { if entity.window == nil { return } - if !entity.isContainer { return } + if _, ok := entity.element.(ability.Layoutable); !ok { return } entity.layoutInvalid = true entity.window.system.anyLayoutInvalid = true } @@ -233,7 +232,7 @@ func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { func (entity *entity) SelectChild (index int, selected bool) { child := entity.children[index] - if element, ok := child.element.(tomo.Selectable); ok { + if element, ok := child.element.(ability.Selectable); ok { if child.selected == selected { return } child.selected = selected element.HandleSelectionChange() @@ -275,9 +274,9 @@ func (entity *entity) Selected () bool { func (entity *entity) NotifyFlexibleHeightChange () { if entity.parent == nil { return } - if parent, ok := entity.parent.element.(tomo.FlexibleContainer); ok { + if parent, ok := entity.parent.element.(ability.FlexibleContainer); ok { parent.HandleChildFlexibleHeightChange ( - entity.element.(tomo.Flexible)) + entity.element.(ability.Flexible)) } } @@ -285,8 +284,20 @@ func (entity *entity) NotifyFlexibleHeightChange () { func (entity *entity) NotifyScrollBoundsChange () { if entity.parent == nil { return } - if parent, ok := entity.parent.element.(tomo.ScrollableContainer); ok { + if parent, ok := entity.parent.element.(ability.ScrollableContainer); ok { parent.HandleChildScrollBoundsChange ( - entity.element.(tomo.Scrollable)) + entity.element.(ability.Scrollable)) } } + +// ----------- ThemeableEntity ----------- // + +func (entity *entity) Theme () tomo.Theme { + return entity.backend.theme +} + +// ----------- ConfigurableEntity ----------- // + +func (entity *entity) Config () tomo.Config { + return entity.backend.config +} diff --git a/backends/x/event.go b/plugins/x/x/event.go similarity index 94% rename from backends/x/event.go rename to plugins/x/x/event.go index b195d1a..182a607 100644 --- a/backends/x/event.go +++ b/plugins/x/x/event.go @@ -3,6 +3,7 @@ package x import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/ability" import "github.com/jezek/xgbutil" import "github.com/jezek/xgb/xproto" @@ -134,7 +135,7 @@ func (window *window) handleKeyPress ( } else if key == input.KeyEscape && window.shy { window.Close() } else if window.focused != nil { - focused, ok := window.focused.element.(tomo.KeyboardTarget) + focused, ok := window.focused.element.(ability.KeyboardTarget) if ok { focused.HandleKeyDown(key, modifiers) } } } @@ -169,7 +170,7 @@ func (window *window) handleKeyRelease ( modifiers.NumberPad = numberPad if window.focused != nil { - focused, ok := window.focused.element.(tomo.KeyboardTarget) + focused, ok := window.focused.element.(ability.KeyboardTarget) if ok { focused.HandleKeyUp(key, modifiers) } } } @@ -191,7 +192,7 @@ func (window *window) handleButtonPress ( } else if scrolling { underneath := window.system.scrollTargetChildAt(point) if underneath != nil { - if child, ok := underneath.element.(tomo.ScrollTarget); ok { + if child, ok := underneath.element.(ability.ScrollTarget); ok { sum := scrollSum { } sum.add(buttonEvent.Detail, window, buttonEvent.State) window.compressScrollSum(buttonEvent, &sum) @@ -203,12 +204,12 @@ func (window *window) handleButtonPress ( } else { underneath := window.system.childAt(point) window.system.drags[buttonEvent.Detail] = underneath - if child, ok := underneath.element.(tomo.MouseTarget); ok { + if child, ok := underneath.element.(ability.MouseTarget); ok { child.HandleMouseDown ( point, input.Button(buttonEvent.Detail), modifiers) } - callback := func (container tomo.MouseTargetContainer, child tomo.Element) { + callback := func (container ability.MouseTargetContainer, child tomo.Element) { container.HandleChildMouseDown ( point, input.Button(buttonEvent.Detail), modifiers, child) @@ -229,7 +230,7 @@ func (window *window) handleButtonRelease ( dragging := window.system.drags[buttonEvent.Detail] if dragging != nil { - if child, ok := dragging.element.(tomo.MouseTarget); ok { + if child, ok := dragging.element.(ability.MouseTarget); ok { child.HandleMouseUp ( image.Pt ( int(buttonEvent.EventX), @@ -237,7 +238,7 @@ func (window *window) handleButtonRelease ( input.Button(buttonEvent.Detail), modifiers) } - callback := func (container tomo.MouseTargetContainer, child tomo.Element) { + callback := func (container ability.MouseTargetContainer, child tomo.Element) { container.HandleChildMouseUp ( image.Pt ( int(buttonEvent.EventX), @@ -262,7 +263,7 @@ func (window *window) handleMotionNotify ( handled := false for _, child := range window.system.drags { if child == nil { continue } - if child, ok := child.element.(tomo.MotionTarget); ok { + if child, ok := child.element.(ability.MotionTarget); ok { child.HandleMotion(image.Pt(x, y)) handled = true } @@ -270,7 +271,7 @@ func (window *window) handleMotionNotify ( if !handled { child := window.system.childAt(image.Pt(x, y)) - if child, ok := child.element.(tomo.MotionTarget); ok { + if child, ok := child.element.(ability.MotionTarget); ok { child.HandleMotion(image.Pt(x, y)) } } diff --git a/backends/x/selection.go b/plugins/x/x/selection.go similarity index 100% rename from backends/x/selection.go rename to plugins/x/x/selection.go diff --git a/backends/x/selectionclaim.go b/plugins/x/x/selectionclaim.go similarity index 100% rename from backends/x/selectionclaim.go rename to plugins/x/x/selectionclaim.go diff --git a/backends/x/system.go b/plugins/x/x/system.go similarity index 77% rename from backends/x/system.go rename to plugins/x/x/system.go index fcc9e4a..4143774 100644 --- a/backends/x/system.go +++ b/plugins/x/x/system.go @@ -1,10 +1,8 @@ package x import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/ability" type entitySet map[*entity] struct { } @@ -24,11 +22,8 @@ func (set entitySet) Add (entity *entity) { type system struct { child *entity focused *entity - canvas canvas.BasicCanvas - - theme theme.Wrapped - config config.Wrapped - + canvas artist.BasicCanvas + invalidateIgnore bool drawingInvalid entitySet anyLayoutInvalid bool @@ -42,21 +37,19 @@ func (system *system) initialize () { system.drawingInvalid = make(entitySet) } -func (system *system) SetTheme (theme tomo.Theme) { - system.theme.Theme = theme +func (system *system) handleThemeChange () { system.propagate (func (entity *entity) bool { - if child, ok := system.child.element.(tomo.Themeable); ok { - child.SetTheme(theme) + if child, ok := system.child.element.(ability.Themeable); ok { + child.HandleThemeChange() } return true }) } -func (system *system) SetConfig (config tomo.Config) { - system.config.Config = config +func (system *system) handleConfigChange () { system.propagate (func (entity *entity) bool { - if child, ok := system.child.element.(tomo.Configurable); ok { - child.SetConfig(config) + if child, ok := system.child.element.(ability.Configurable); ok { + child.HandleConfigChange() } return true }) @@ -66,10 +59,10 @@ func (system *system) focus (entity *entity) { previous := system.focused system.focused = entity if previous != nil { - previous.element.(tomo.Focusable).HandleFocusChange() + previous.element.(ability.Focusable).HandleFocusChange() } if entity != nil { - entity.element.(tomo.Focusable).HandleFocusChange() + entity.element.(ability.Focusable).HandleFocusChange() } } @@ -79,7 +72,7 @@ func (system *system) focusNext () { system.propagateAlt (func (entity *entity) bool { if found { // looking for the next element to select - child, ok := entity.element.(tomo.Focusable) + child, ok := entity.element.(ability.Focusable) if ok && child.Enabled() { // found it entity.Focus() @@ -106,7 +99,7 @@ func (system *system) focusPrevious () { return false } - child, ok := entity.element.(tomo.Focusable) + child, ok := entity.element.(ability.Focusable) if ok && child.Enabled() { behind = entity } return true }) @@ -137,9 +130,7 @@ func (system *system) resizeChildToFit () { system.child.bounds = system.canvas.Bounds() system.child.clippedBounds = system.child.bounds system.child.Invalidate() - if system.child.isContainer { - system.child.InvalidateLayout() - } + system.child.InvalidateLayout() } func (system *system) afterEvent () { @@ -153,7 +144,7 @@ func (system *system) afterEvent () { func (system *system) layout (entity *entity, force bool) { if entity == nil { return } if entity.layoutInvalid == true || force { - if element, ok := entity.element.(tomo.Layoutable); ok { + if element, ok := entity.element.(ability.Layoutable); ok { element.Layout() entity.layoutInvalid = false force = true @@ -176,7 +167,7 @@ func (system *system) draw () { for entity := range system.drawingInvalid { if entity.clippedBounds.Empty() { continue } - entity.element.Draw (canvas.Cut ( + entity.element.Draw (artist.Cut ( system.canvas, entity.clippedBounds)) finalBounds = finalBounds.Union(entity.clippedBounds) diff --git a/backends/x/window.go b/plugins/x/x/window.go similarity index 97% rename from backends/x/window.go rename to plugins/x/x/window.go index ad39a79..18cbbf9 100644 --- a/backends/x/window.go +++ b/plugins/x/x/window.go @@ -13,14 +13,14 @@ import "github.com/jezek/xgbutil/mousebind" import "github.com/jezek/xgbutil/xgraphics" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" type mainWindow struct { *window } type menuWindow struct { *window } type window struct { system - backend *Backend + backend *backend xWindow *xwindow.Window xCanvas *xgraphics.Image @@ -40,7 +40,7 @@ type window struct { onClose func () } -func (backend *Backend) NewWindow ( +func (backend *backend) NewWindow ( bounds image.Rectangle, ) ( output tomo.MainWindow, @@ -53,7 +53,7 @@ func (backend *Backend) NewWindow ( return output, err } -func (backend *Backend) newWindow ( +func (backend *backend) newWindow ( bounds image.Rectangle, override bool, ) ( @@ -67,7 +67,6 @@ func (backend *Backend) newWindow ( window.system.initialize() window.system.pushFunc = window.pasteAndPush - window.theme.Case = tomo.C("tomo", "window") window.xWindow, err = xwindow.Generate(backend.connection) if err != nil { return } @@ -121,9 +120,6 @@ func (backend *Backend) newWindow ( Connect(backend.connection, window.xWindow.Id) xevent.SelectionRequestFun(window.handleSelectionRequest). Connect(backend.connection, window.xWindow.Id) - - window.SetTheme(backend.theme) - window.SetConfig(backend.config) window.metrics.bounds = bounds window.setMinimumSize(8, 8) @@ -418,7 +414,7 @@ func (window *window) pasteAndPush (region image.Rectangle) { } func (window *window) paste (region image.Rectangle) { - canvas := canvas.Cut(window.canvas, region) + canvas := artist.Cut(window.canvas, region) data, stride := canvas.Buffer() bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds()) diff --git a/backends/x/x.go b/plugins/x/x/x.go similarity index 66% rename from backends/x/x.go rename to plugins/x/x/x.go index 7928f66..c76be69 100644 --- a/backends/x/x.go +++ b/plugins/x/x/x.go @@ -1,6 +1,8 @@ package x import "git.tebibyte.media/sashakoshka/tomo" +import defaultTheme "git.tebibyte.media/sashakoshka/tomo/default/theme" +import defaultConfig "git.tebibyte.media/sashakoshka/tomo/default/config" import "github.com/jezek/xgbutil" import "github.com/jezek/xgb/xproto" @@ -8,8 +10,7 @@ import "github.com/jezek/xgbutil/xevent" import "github.com/jezek/xgbutil/keybind" import "github.com/jezek/xgbutil/mousebind" -// Backend is an instance of an X backend. -type Backend struct { +type backend struct { connection *xgbutil.XUtil doChannel chan(func ()) @@ -36,11 +37,14 @@ type Backend struct { // NewBackend instantiates an X backend. func NewBackend () (output tomo.Backend, err error) { - backend := &Backend { + backend := &backend { windows: map[xproto.Window] *window { }, doChannel: make(chan func (), 32), open: true, } + + backend.SetTheme(nil) + backend.SetConfig(nil) // connect to X backend.connection, err = xgbutil.NewConn() @@ -54,9 +58,7 @@ func NewBackend () (output tomo.Backend, err error) { return } -// Run runs the backend's event loop. This method will not exit until Stop() is -// called, or the backend experiences a fatal error. -func (backend *Backend) Run () (err error) { +func (backend *backend) Run () (err error) { backend.assert() pingBefore, pingAfter, @@ -76,8 +78,7 @@ func (backend *Backend) Run () (err error) { } } -// Stop gracefully closes the connection and stops the event loop. -func (backend *Backend) Stop () { +func (backend *backend) Stop () { backend.assert() if !backend.open { return } backend.open = false @@ -93,35 +94,35 @@ func (backend *Backend) Stop () { backend.connection.Conn().Close() } -// Do executes the specified callback within the main thread as soon as -// possible. This function can be safely called from other threads. -func (backend *Backend) Do (callback func ()) { +func (backend *backend) Do (callback func ()) { backend.assert() backend.doChannel <- callback } -// SetTheme sets the theme of all open windows. -func (backend *Backend) SetTheme (theme tomo.Theme) { +func (backend *backend) SetTheme (theme tomo.Theme) { backend.assert() - backend.theme = theme + if theme == nil { + backend.theme = defaultTheme.Default { } + } else { + backend.theme = theme + } for _, window := range backend.windows { - window.SetTheme(theme) + window.handleThemeChange() } } -// SetConfig sets the configuration of all open windows. -func (backend *Backend) SetConfig (config tomo.Config) { +func (backend *backend) SetConfig (config tomo.Config) { backend.assert() - backend.config = config + if config == nil { + backend.config = defaultConfig.Default { } + } else { + backend.config = config + } for _, window := range backend.windows { - window.SetConfig(config) + window.handleConfigChange() } } -func (backend *Backend) assert () { +func (backend *backend) assert () { if backend == nil { panic("nil backend") } } - -func init () { - tomo.RegisterBackend(NewBackend) -} diff --git a/popups/dialog.go b/popups/dialog.go index 722cc6f..b7978dd 100644 --- a/popups/dialog.go +++ b/popups/dialog.go @@ -37,7 +37,7 @@ func NewDialog ( window tomo.Window, ) { if parent == nil { - window, _ = tomo.NewWindow(image.Rectangle { }) + window, _ = tomo.GetBackend().NewWindow(image.Rectangle { }) } else { window, _ = parent.NewModal(image.Rectangle { }) } diff --git a/scripts/install-backends.sh b/scripts/install-backends.sh new file mode 100644 index 0000000..672caca --- /dev/null +++ b/scripts/install-backends.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +. scripts/plugin.sh + +echo "... installing X backend" +install x +echo ".// done" diff --git a/scripts/install-wintergreen.sh b/scripts/install-wintergreen.sh new file mode 100644 index 0000000..34681ef --- /dev/null +++ b/scripts/install-wintergreen.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +. scripts/plugin.sh + +echo "... installing Wintergreen theme" +install wintergreen +echo ".// done" diff --git a/scripts/plugin.sh b/scripts/plugin.sh new file mode 100644 index 0000000..1090c0c --- /dev/null +++ b/scripts/plugin.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +pluginInstallPath="$HOME/.local/lib/nasin/plugins" + +mkdir -p build +mkdir -p "$pluginInstallPath" + +install() { + go build -buildmode=plugin -o "build/$1.so" "./plugins/$1" && \ + cp "build/$1.so" $pluginInstallPath +} diff --git a/textdraw/drawer.go b/textdraw/drawer.go index 3ef4011..f5cbcfc 100644 --- a/textdraw/drawer.go +++ b/textdraw/drawer.go @@ -5,7 +5,7 @@ import "unicode" import "image/draw" import "image/color" import "golang.org/x/image/math/fixed" -import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" // Drawer is an extended TypeSetter that is able to draw text. Much like // TypeSetter, It has no constructor and its zero value can be used safely. @@ -13,7 +13,7 @@ type Drawer struct { TypeSetter } // Draw draws the drawer's text onto the specified canvas at the given offset. func (drawer Drawer) Draw ( - destination canvas.Canvas, + destination artist.Canvas, color color.RGBA, offset image.Point, ) ( diff --git a/tomo.go b/tomo.go deleted file mode 100644 index a8763ca..0000000 --- a/tomo.go +++ /dev/null @@ -1,63 +0,0 @@ -package tomo - -import "image" - -var backend Backend - -// Run initializes a backend, calls the callback function, and begins the event -// loop in that order. This function does not return until Stop() is called, or -// the backend experiences a fatal error. -func Run (callback func ()) (err error) { - backend, err = instantiateBackend() - if err != nil { return } - if callback != nil { callback() } - err = backend.Run() - backend = nil - return -} - -// Stop gracefully stops the event loop and shuts the backend down. Call this -// before closing your application. -func Stop () { - if backend != nil { backend.Stop() } -} - -// Do executes the specified callback within the main thread as soon as -// possible. This function can be safely called from other threads. -func Do (callback func ()) { - assertBackend() - backend.Do(callback) -} - -// NewEntity generates an entity for an element using the current backend. -func NewEntity (owner Element) Entity { - assertBackend() - return backend.NewEntity(owner) -} - -// NewWindow creates a new window using the current backend, and returns it as a -// MainWindow. If the window could not be created, an error is returned -// explaining why. -func NewWindow (bounds image.Rectangle) (window MainWindow, err error) { - assertBackend() - return backend.NewWindow(bounds) -} - -// SetTheme sets the theme of all open windows. -func SetTheme (theme Theme) { - backend.SetTheme(theme) -} - -// SetConfig sets the configuration of all open windows. -func SetConfig (config Config) { - backend.SetConfig(config) -} - -// Bounds creates a rectangle from an x, y, width, and height. -func Bounds (x, y, width, height int) image.Rectangle { - return image.Rect(x, y, x + width, y + height) -} - -func assertBackend () { - if backend == nil { panic("no backend is running") } -} diff --git a/version.go b/version.go new file mode 100644 index 0000000..f9ed8fe --- /dev/null +++ b/version.go @@ -0,0 +1,32 @@ +package tomo + +import "fmt" + +// Version represents a semantic version number. +type Version [3]int + +// TODO: when 1.0 is released, remove the notices. remember to update +// CurrentVersion too! + +// CurrentVersion returns the current Tomo/Nasin version. Note that until 1.0 is +// released, this does not mean much. +func CurrentVersion () Version { + return Version { 0, 0, 0 } +} + +// CompatibleABI returns whether or not two versions are compatible on a binary +// level. Note that until 1.0 is released, this does not mean much. +func (version Version) CompatibleABI (other Version) bool { + return version[0] == other[0] && version[1] == other[1] +} + +// CompatibleAPI returns whether or not two versions are compatible on a source +// code level. Note that until 1.0 is released, this does not mean much. +func (version Version) CompatibleAPI (other Version) bool { + return version[0] == other[0] +} + +// String returns a string representation of the version. +func (version Version) String () string { + return fmt.Sprint(version[0], ".", version[1], ".", version[2]) +}