reorganize #17
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
20
README.md
20
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).
|
||||
|
241
ability/element.go
Normal file
241
ability/element.go
Normal file
@ -0,0 +1,241 @@
|
||||
// Package ability defines extended interfaces that elements can support.
|
||||
package ability
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
// Layoutable represents an element that needs to perform layout calculations
|
||||
// before it can draw itself.
|
||||
type Layoutable interface {
|
||||
tomo.Element
|
||||
|
||||
// Layout causes this element to perform a layout operation.
|
||||
Layout ()
|
||||
}
|
||||
|
||||
// Container represents an element capable of containing child elements.
|
||||
type Container interface {
|
||||
tomo.Element
|
||||
Layoutable
|
||||
|
||||
// DrawBackground causes the element to draw its background pattern to
|
||||
// the specified canvas. The bounds of this canvas specify the area that
|
||||
// is actually drawn to, while the Entity bounds specify the actual area
|
||||
// of the element.
|
||||
DrawBackground (artist.Canvas)
|
||||
|
||||
// HandleChildMinimumSizeChange is called when a child's minimum size is
|
||||
// changed.
|
||||
HandleChildMinimumSizeChange (child tomo.Element)
|
||||
}
|
||||
|
||||
// Enableable represents an element that can be enabled and disabled. Disabled
|
||||
// elements typically appear greyed out.
|
||||
type Enableable interface {
|
||||
tomo.Element
|
||||
|
||||
// Enabled returns whether or not the element is enabled.
|
||||
Enabled () bool
|
||||
|
||||
// SetEnabled sets whether or not the element is enabled.
|
||||
SetEnabled (bool)
|
||||
}
|
||||
|
||||
// Focusable represents an element that has keyboard navigation support.
|
||||
type Focusable interface {
|
||||
tomo.Element
|
||||
Enableable
|
||||
|
||||
// HandleFocusChange is called when the element is focused or unfocused.
|
||||
HandleFocusChange ()
|
||||
}
|
||||
|
||||
// Selectable represents an element that can be selected. This includes things
|
||||
// like list items, files, etc. The difference between this and Focusable is
|
||||
// that multiple Selectable elements may be selected at the same time, whereas
|
||||
// only one Focusable element may be focused at the same time. Containers who's
|
||||
// purpose is to contain selectable elements can determine when to select them
|
||||
// by implementing MouseTargetContainer and listening for HandleChildMouseDown
|
||||
// events.
|
||||
type Selectable interface {
|
||||
tomo.Element
|
||||
Enableable
|
||||
|
||||
// HandleSelectionChange is called when the element is selected or
|
||||
// deselected.
|
||||
HandleSelectionChange ()
|
||||
}
|
||||
|
||||
// KeyboardTarget represents an element that can receive keyboard input.
|
||||
type KeyboardTarget interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleKeyDown is called when a key is pressed down or repeated while
|
||||
// this element has keyboard focus. It is important to note that not
|
||||
// every key down event is guaranteed to be paired with exactly one key
|
||||
// up event. This is the reason a list of modifier keys held down at the
|
||||
// time of the key press is given.
|
||||
HandleKeyDown (key input.Key, modifiers input.Modifiers)
|
||||
|
||||
// HandleKeyUp is called when a key is released while this element has
|
||||
// keyboard focus.
|
||||
HandleKeyUp (key input.Key, modifiers input.Modifiers)
|
||||
}
|
||||
|
||||
// MouseTarget represents an element that can receive mouse events.
|
||||
type MouseTarget interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleMouseDown is called when a mouse button is pressed down on this
|
||||
// element.
|
||||
HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers)
|
||||
|
||||
// HandleMouseUp is called when a mouse button is released that was
|
||||
// originally pressed down on this element.
|
||||
HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers)
|
||||
}
|
||||
|
||||
// MouseTargetContainer represents an element that wants to know when one
|
||||
// of its children is clicked. Children do not have to implement MouseTarget for
|
||||
// a container satisfying MouseTargetContainer to be notified that they have
|
||||
// been clicked.
|
||||
type MouseTargetContainer interface {
|
||||
Container
|
||||
|
||||
// HandleMouseDown is called when a mouse button is pressed down on a
|
||||
// child element.
|
||||
HandleChildMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element)
|
||||
|
||||
// HandleMouseUp is called when a mouse button is released that was
|
||||
// originally pressed down on a child element.
|
||||
HandleChildMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element)
|
||||
}
|
||||
|
||||
// MotionTarget represents an element that can receive mouse motion events.
|
||||
type MotionTarget interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleMotion is called when the mouse is moved over this element,
|
||||
// or the mouse is moving while being held down and originally pressed
|
||||
// down on this element.
|
||||
HandleMotion (position image.Point)
|
||||
}
|
||||
|
||||
// ScrollTarget represents an element that can receive mouse scroll events.
|
||||
type ScrollTarget interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleScroll is called when the mouse is scrolled. The X and Y
|
||||
// direction of the scroll event are passed as deltaX and deltaY.
|
||||
HandleScroll (
|
||||
position image.Point,
|
||||
deltaX, deltaY float64,
|
||||
modifiers input.Modifiers)
|
||||
}
|
||||
|
||||
// Flexible represents an element who's preferred minimum height can change in
|
||||
// response to its width.
|
||||
type Flexible interface {
|
||||
tomo.Element
|
||||
|
||||
// FlexibleHeightFor returns what the element's minimum height would be
|
||||
// if resized to a specified width. This does not actually alter the
|
||||
// state of the element in any way, but it may perform significant work,
|
||||
// so it should be called sparingly.
|
||||
//
|
||||
// It is reccomended that parent containers check for this interface and
|
||||
// take this method's value into account in order to support things like
|
||||
// flow layouts and text wrapping, but it is not absolutely necessary.
|
||||
// The element's MinimumSize method will still return the absolute
|
||||
// minimum size that the element may be resized to.
|
||||
//
|
||||
// It is important to note that if a parent container checks for
|
||||
// flexible chilren, it itself will likely need to be either scrollable
|
||||
// or flexible.
|
||||
FlexibleHeightFor (width int) int
|
||||
}
|
||||
|
||||
// FlexibleContainer represents an element that is capable of containing
|
||||
// flexible children.
|
||||
type FlexibleContainer interface {
|
||||
Container
|
||||
|
||||
// HandleChildFlexibleHeightChange is called when the parameters
|
||||
// affecting a child's flexible height are changed.
|
||||
HandleChildFlexibleHeightChange (child Flexible)
|
||||
}
|
||||
|
||||
// Scrollable represents an element that can be scrolled. It acts as a viewport
|
||||
// through which its contents can be observed.
|
||||
type Scrollable interface {
|
||||
tomo.Element
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
ScrollContentBounds () image.Rectangle
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
ScrollViewportBounds () image.Rectangle
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
ScrollTo (position image.Point)
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
ScrollAxes () (horizontal, vertical bool)
|
||||
}
|
||||
|
||||
// ScrollableContainer represents an element that is capable of containing
|
||||
// scrollable children.
|
||||
type ScrollableContainer interface {
|
||||
Container
|
||||
|
||||
// HandleChildScrollBoundsChange is called when the content bounds,
|
||||
// viewport bounds, or scroll axes of a child are changed.
|
||||
HandleChildScrollBoundsChange (child Scrollable)
|
||||
}
|
||||
|
||||
// Collapsible represents an element who's minimum width and height can be
|
||||
// manually resized. Scrollable elements should implement this if possible.
|
||||
type Collapsible interface {
|
||||
tomo.Element
|
||||
|
||||
// Collapse collapses the element's minimum width and height. A value of
|
||||
// zero for either means that the element's normal value is used.
|
||||
Collapse (width, height int)
|
||||
}
|
||||
|
||||
// Themeable represents an element that can modify its appearance to fit within
|
||||
// a theme.
|
||||
type Themeable interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleThemeChange is called whenever the theme is changed.
|
||||
HandleThemeChange ()
|
||||
}
|
||||
|
||||
// Configurable represents an element that can modify its behavior to fit within
|
||||
// a set of configuration parameters.
|
||||
type Configurable interface {
|
||||
tomo.Element
|
||||
|
||||
// HandleConfigChange is called whenever configuration parameters are
|
||||
// changed.
|
||||
HandleConfigChange ()
|
||||
}
|
63
artist/artutil/util.go
Normal file
63
artist/artutil/util.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Package artutil provides utility functions for working with graphical types
|
||||
// defined in artist, canvas, and image.
|
||||
package artutil
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||
|
||||
// Fill fills the destination canvas with the given pattern.
|
||||
func Fill (destination artist.Canvas, source artist.Pattern) (updated image.Rectangle) {
|
||||
source.Draw(destination, destination.Bounds())
|
||||
return destination.Bounds()
|
||||
}
|
||||
|
||||
// DrawClip lets you draw several subsets of a pattern at once.
|
||||
func DrawClip (
|
||||
destination artist.Canvas,
|
||||
source artist.Pattern,
|
||||
bounds image.Rectangle,
|
||||
subsets ...image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
for _, subset := range subsets {
|
||||
source.Draw(artist.Cut(destination, subset), bounds)
|
||||
updatedRegion = updatedRegion.Union(subset)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DrawShatter is like an inverse of DrawClip, drawing nothing in the areas
|
||||
// specified by "rocks".
|
||||
func DrawShatter (
|
||||
destination artist.Canvas,
|
||||
source artist.Pattern,
|
||||
bounds image.Rectangle,
|
||||
rocks ...image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
tiles := shatter.Shatter(bounds, rocks...)
|
||||
return DrawClip(destination, source, bounds, tiles...)
|
||||
}
|
||||
|
||||
// AllocateSample returns a new canvas containing the result of a pattern. The
|
||||
// resulting canvas can be sourced from shape drawing functions. I beg of you
|
||||
// please do not call this every time you need to draw a shape with a pattern on
|
||||
// it because that is horrible and cruel to the computer.
|
||||
func AllocateSample (source artist.Pattern, width, height int) artist.Canvas {
|
||||
allocated := artist.NewBasicCanvas(width, height)
|
||||
Fill(allocated, source)
|
||||
return allocated
|
||||
}
|
||||
|
||||
// Hex creates a color.RGBA value from an RGBA integer value.
|
||||
func Hex (color uint32) (c color.RGBA) {
|
||||
c.A = uint8(color)
|
||||
c.B = uint8(color >> 8)
|
||||
c.G = uint8(color >> 16)
|
||||
c.R = uint8(color >> 24)
|
||||
return
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package canvas
|
||||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/draw"
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
41
backend.go
41
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)
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
// Package all links most common backends.
|
||||
package all
|
||||
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
@ -1,3 +0,0 @@
|
||||
// Package backends contains sub-packages that register backends with tomo when
|
||||
// linked into a program.
|
||||
package backends
|
@ -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
|
BIN
default/theme/assets/default.png
Normal file
BIN
default/theme/assets/default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 350 B |
@ -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.
|
||||
|
@ -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 { }
|
||||
// }
|
@ -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
|
||||
}
|
244
element.go
244
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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
@ -9,7 +9,7 @@ type scratchEntry struct {
|
||||
}
|
||||
|
||||
type container struct {
|
||||
entity tomo.ContainerEntity
|
||||
entity tomo.Entity
|
||||
scratch map[tomo.Element] scratchEntry
|
||||
minimumSize func ()
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 () {
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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(),
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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() +
|
||||
|
@ -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 {
|
||||
|
59
entity.go
59
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
|
||||
}
|
||||
|
@ -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."
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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())
|
||||
})
|
||||
|
@ -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()
|
||||
}
|
@ -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) {
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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."
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -2,16 +2,19 @@ package main
|
||||
|
||||
import "time"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/nasin"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||
type Application struct { }
|
||||
|
||||
func (Application) Init () error {
|
||||
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||
if err != nil { return err }
|
||||
window.SetTitle("Approaching")
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
window.Adopt(container)
|
||||
@ -23,19 +26,20 @@ func run () {
|
||||
button.SetEnabled(false)
|
||||
container.Adopt(button)
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
go fill(window, bar)
|
||||
return nil
|
||||
}
|
||||
|
||||
func fill (window tomo.Window, bar *elements.ProgressBar) {
|
||||
for progress := 0.0; progress < 1.0; progress += 0.01 {
|
||||
time.Sleep(time.Second / 24)
|
||||
tomo.Do (func () {
|
||||
nasin.Do (func () {
|
||||
bar.SetProgress(progress)
|
||||
})
|
||||
}
|
||||
tomo.Do (func () {
|
||||
nasin.Do (func () {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindInfo,
|
||||
window,
|
||||
|
Binary file not shown.
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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.")
|
||||
}
|
@ -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())
|
||||
}
|
@ -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)
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import "io"
|
||||
import "image"
|
||||
import "image/color"
|
||||
|
||||
type Textures []Texture
|
||||
|
||||
type Texture struct {
|
||||
Data []color.RGBA
|
||||
Stride int
|
||||
}
|
||||
|
||||
func (texture Textures) At (wall int, offset Vector) color.RGBA {
|
||||
wall --
|
||||
if wall < 0 || wall >= len(texture) { return color.RGBA { } }
|
||||
image := texture[wall]
|
||||
|
||||
xOffset := int(offset.X * float64(image.Stride))
|
||||
yOffset := int(offset.Y * float64(len(image.Data) / image.Stride))
|
||||
|
||||
index := xOffset + yOffset * image.Stride
|
||||
if index < 0 { return color.RGBA { } }
|
||||
if index >= len(image.Data) { return color.RGBA { } }
|
||||
return image.Data[index]
|
||||
}
|
||||
|
||||
func TextureFrom (source io.Reader) (texture Texture, err error) {
|
||||
sourceImage, _, err := image.Decode(source)
|
||||
if err != nil { return }
|
||||
bounds := sourceImage.Bounds()
|
||||
texture.Stride = bounds.Dx()
|
||||
texture.Data = make([]color.RGBA, bounds.Dx() * bounds.Dy())
|
||||
|
||||
index := 0
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
r, g, b, a := sourceImage.At(x, y).RGBA()
|
||||
texture.Data[index] = color.RGBA {
|
||||
R: uint8(r >> 8),
|
||||
G: uint8(g >> 8),
|
||||
B: uint8(b >> 8),
|
||||
A: uint8(a >> 8),
|
||||
}
|
||||
index ++
|
||||
}}
|
||||
return texture, nil
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB |
@ -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.`
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128))
|
||||
window.SetTitle("vertical stack")
|
||||
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
|
||||
label := elements.NewLabelWrapped("it is a label hehe")
|
||||
button := elements.NewButton("drawing pad")
|
||||
okButton := elements.NewButton("OK")
|
||||
button.OnClick (func () {
|
||||
container.DisownAll()
|
||||
container.Adopt(elements.NewLabel("Draw here (not really):"))
|
||||
container.AdoptExpand(testing.NewMouse())
|
||||
container.Adopt(okButton)
|
||||
okButton.Focus()
|
||||
})
|
||||
okButton.OnClick(tomo.Stop)
|
||||
|
||||
container.AdoptExpand(label)
|
||||
container.Adopt(button, okButton)
|
||||
window.Adopt(container)
|
||||
|
||||
okButton.Focus()
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
79
nasin/application.go
Normal file
79
nasin/application.go
Normal file
@ -0,0 +1,79 @@
|
||||
package nasin
|
||||
|
||||
import "image"
|
||||
import "errors"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// Application represents a Tomo/Nasin application.
|
||||
type Application interface {
|
||||
Init () error
|
||||
}
|
||||
|
||||
// Run initializes Tomo and Nasin, and runs the given application. This function
|
||||
// will block until the application exits or a fatal error occurrs.
|
||||
func Run (application Application) {
|
||||
loadPlugins()
|
||||
|
||||
backend, err := instantiateBackend()
|
||||
if err != nil {
|
||||
println("nasin: cannot start application:", err.Error())
|
||||
return
|
||||
}
|
||||
backend.SetTheme(theme)
|
||||
tomo.SetBackend(backend)
|
||||
|
||||
if application == nil { panic("nasin: nil application") }
|
||||
err = application.Init()
|
||||
if err != nil {
|
||||
println("nasin: backend exited with error:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = backend.Run()
|
||||
if err != nil {
|
||||
println("nasin: backend exited with error:", err.Error())
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Stop stops the event loop
|
||||
func Stop () {
|
||||
assertBackend()
|
||||
tomo.GetBackend().Stop()
|
||||
}
|
||||
|
||||
// Do executes the specified callback within the main thread as soon as
|
||||
// possible.
|
||||
func Do (callback func()) {
|
||||
assertBackend()
|
||||
tomo.GetBackend().Do(callback)
|
||||
}
|
||||
|
||||
// NewWindow creates a new window within the specified bounding rectangle. The
|
||||
// position on screen may be overridden by the backend or operating system.
|
||||
func NewWindow (bounds image.Rectangle) (tomo.MainWindow, error) {
|
||||
assertBackend()
|
||||
return tomo.GetBackend().NewWindow(bounds)
|
||||
}
|
||||
|
||||
func assertBackend () {
|
||||
if tomo.GetBackend() == nil {
|
||||
panic("nasin: no running tomo backend")
|
||||
}
|
||||
}
|
||||
|
||||
func instantiateBackend () (backend tomo.Backend, err error) {
|
||||
// find a suitable backend
|
||||
for _, factory := range factories {
|
||||
backend, err = factory()
|
||||
if err == nil && backend != nil { return }
|
||||
}
|
||||
|
||||
// if none were found, but there was no error produced, produce an error
|
||||
if err == nil {
|
||||
return nil, errors.New("no available tomo backends")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
44
nasin/doc.go
Normal file
44
nasin/doc.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Package nasin provides a higher-level framework for Tomo applications. Nasin
|
||||
// also automatically handles themes, backend instantiation, and plugin support.
|
||||
//
|
||||
// Backends and themes are typically loaded through plugins. For now, plugins
|
||||
// are only supported on UNIX-like systems, but this will change in the future.
|
||||
// Nasin will attempt to load all ".so" files in these directories as plugins:
|
||||
//
|
||||
// - /usr/lib/nasin/plugins
|
||||
// - /usr/local/lib/nasin/plugins
|
||||
// - $HOME/.local/lib/nasin/plugins
|
||||
//
|
||||
// It will also attempt to load all ".so" files in the directory specified by
|
||||
// the NASIN_PLUGIN_PATH environment variable.
|
||||
//
|
||||
// Plugins must export the following functions at minimum:
|
||||
//
|
||||
// + Expects() tomo.Version
|
||||
// + Name() string
|
||||
// + Description() string
|
||||
//
|
||||
// Expects() must return the version of Tomo/Nasin it was built for. Nasin will
|
||||
// automatically figure out if the plugin has a compatible ABI with the current
|
||||
// version and refuse to load it if not. Name() and Description() return a short
|
||||
// plugin name and a description of what a plugin does, respectively. Plugins
|
||||
// must not attempt to interact with Tomo/Nasin within their init functions.
|
||||
//
|
||||
// If a plugin provides a backend, it must export this function:
|
||||
//
|
||||
// NewBackend() (tomo.Backend, error)
|
||||
//
|
||||
// This function must attempt to initialize the backend, and return it if
|
||||
// successful. Otherwise, it should clean up all resources and return an error
|
||||
// explaining what caused the backend to fail to initialize. The first backend
|
||||
// that does not throw an error will be used.
|
||||
//
|
||||
// If a plugin provides a theme, it must export this function:
|
||||
//
|
||||
// NewTheme() tomo.Theme
|
||||
//
|
||||
// This just creates a new theme and returns it.
|
||||
//
|
||||
// For information on how to create plugins with Go, visit:
|
||||
// https://pkg.go.dev/plugin
|
||||
package nasin
|
84
nasin/plugin.go
Normal file
84
nasin/plugin.go
Normal file
@ -0,0 +1,84 @@
|
||||
package nasin
|
||||
|
||||
import "os"
|
||||
// TODO: possibly fork the official plugin module and add support for other
|
||||
// operating systems? perhaps enhance the Lookup function with
|
||||
// the generic extract function we have here for extra type safety goodness.
|
||||
import "plugin"
|
||||
import "path/filepath"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
type backendFactory func () (tomo.Backend, error)
|
||||
var factories []backendFactory
|
||||
var theme tomo.Theme
|
||||
|
||||
var pluginPaths []string
|
||||
|
||||
func loadPlugins () {
|
||||
for _, dir := range pluginPaths {
|
||||
if dir != "" {
|
||||
loadPluginsFrom(dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadPluginsFrom (dir string) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
// its no big deal if one of the dirs doesn't exist
|
||||
if err != nil { return }
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() { continue }
|
||||
if filepath.Ext(entry.Name()) != ".so" { continue }
|
||||
pluginPath := filepath.Join(dir, entry.Name())
|
||||
loadPlugin(pluginPath)
|
||||
}
|
||||
}
|
||||
|
||||
func loadPlugin (path string) {
|
||||
die := func (reason string) {
|
||||
println("nasin: could not load plugin at", path + ":", reason)
|
||||
}
|
||||
|
||||
plugin, err := plugin.Open(path)
|
||||
if err != nil {
|
||||
die(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// check for and obtain basic plugin functions
|
||||
expects, ok := extract[func () tomo.Version](plugin, "Expects")
|
||||
if !ok { die("does not implement Expects() tomo.Version"); return }
|
||||
name, ok := extract[func () string](plugin, "Name")
|
||||
if !ok { die("does not implement Name() string"); return }
|
||||
_, ok = extract[func () string](plugin, "Description")
|
||||
if !ok { die("does not implement Description() string"); return }
|
||||
|
||||
// check for version compatibility
|
||||
pluginVersion := expects()
|
||||
currentVersion := tomo.CurrentVersion()
|
||||
if !pluginVersion.CompatibleABI(currentVersion) {
|
||||
die (
|
||||
"plugin (" + pluginVersion.String() +
|
||||
") incompatible with tomo/nasin version (" +
|
||||
currentVersion.String() + ")")
|
||||
return
|
||||
}
|
||||
|
||||
// if it's a backend plugin...
|
||||
newBackend, ok := extract[func () (tomo.Backend, error)](plugin, "NewBackend")
|
||||
if ok { factories = append(factories, newBackend) }
|
||||
|
||||
// if it's a theme plugin...
|
||||
newTheme, ok := extract[func () tomo.Theme](plugin, "NewTheme")
|
||||
if ok { theme = newTheme() }
|
||||
|
||||
println("nasin: loaded plugin", name())
|
||||
}
|
||||
|
||||
func extract[T any] (plugin *plugin.Plugin, name string) (value T, ok bool) {
|
||||
symbol, err := plugin.Lookup(name)
|
||||
if err != nil { return }
|
||||
value, ok = symbol.(T)
|
||||
return
|
||||
}
|
22
nasin/unix.go
Normal file
22
nasin/unix.go
Normal file
@ -0,0 +1,22 @@
|
||||
//go:build linux || darwin || freebsd
|
||||
|
||||
package nasin
|
||||
|
||||
import "os"
|
||||
import "strings"
|
||||
import "path/filepath"
|
||||
|
||||
func init () {
|
||||
pathVariable := os.Getenv("NASIN_PLUGIN_PATH")
|
||||
pluginPaths = strings.Split(pathVariable, ":")
|
||||
pluginPaths = append (
|
||||
pluginPaths,
|
||||
"/usr/lib/nasin/plugins",
|
||||
"/usr/local/lib/nasin/plugins")
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
pluginPaths = append (
|
||||
pluginPaths,
|
||||
filepath.Join(homeDir, ".local/lib/nasin/plugins"))
|
||||
}
|
||||
}
|
21
plugins/wintergreen/main.go
Normal file
21
plugins/wintergreen/main.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Plugin wintergreen provides a calm, bluish green theme.
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/plugins/wintergreen/wintergreen"
|
||||
|
||||
func Expects () tomo.Version {
|
||||
return tomo.Version { 0, 0, 0 }
|
||||
}
|
||||
|
||||
func Name () string {
|
||||
return "Wintergreen"
|
||||
}
|
||||
|
||||
func Description () string {
|
||||
return "A calm, bluish green theme."
|
||||
}
|
||||
|
||||
func NewTheme () (tomo.Theme) {
|
||||
return wintergreen.Theme { }
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
2
plugins/wintergreen/wintergreen/doc.go
Normal file
2
plugins/wintergreen/wintergreen/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package wintergreen contains the wintergreen theme.
|
||||
package wintergreen
|
314
plugins/wintergreen/wintergreen/wintergreen.go
Normal file
314
plugins/wintergreen/wintergreen/wintergreen.go
Normal file
@ -0,0 +1,314 @@
|
||||
package wintergreen
|
||||
|
||||
import "image"
|
||||
import "bytes"
|
||||
import _ "embed"
|
||||
import _ "image/png"
|
||||
import "image/color"
|
||||
import "golang.org/x/image/font"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
|
||||
import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
|
||||
|
||||
//go:embed assets/wintergreen.png
|
||||
var defaultAtlasBytes []byte
|
||||
var defaultAtlas artist.Canvas
|
||||
var defaultTextures [17][9]artist.Pattern
|
||||
//go:embed assets/wintergreen-icons-small.png
|
||||
var defaultIconsSmallAtlasBytes []byte
|
||||
var defaultIconsSmall [640]binaryIcon
|
||||
//go:embed assets/wintergreen-icons-large.png
|
||||
var defaultIconsLargeAtlasBytes []byte
|
||||
var defaultIconsLarge [640]binaryIcon
|
||||
|
||||
func atlasCell (col, row int, border artist.Inset) {
|
||||
bounds := image.Rect(0, 0, 16, 16).Add(image.Pt(col, row).Mul(16))
|
||||
defaultTextures[col][row] = patterns.Border {
|
||||
Canvas: artist.Cut(defaultAtlas, bounds),
|
||||
Inset: border,
|
||||
}
|
||||
}
|
||||
|
||||
func atlasCol (col int, border artist.Inset) {
|
||||
for index, _ := range defaultTextures[col] {
|
||||
atlasCell(col, index, border)
|
||||
}
|
||||
}
|
||||
|
||||
type binaryIcon struct {
|
||||
data []bool
|
||||
stride int
|
||||
}
|
||||
|
||||
func (icon binaryIcon) Draw (destination artist.Canvas, color color.RGBA, at image.Point) {
|
||||
bounds := icon.Bounds().Add(at).Intersect(destination.Bounds())
|
||||
point := image.Point { }
|
||||
data, stride := destination.Buffer()
|
||||
|
||||
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
|
||||
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
|
||||
srcPoint := point.Sub(at)
|
||||
srcIndex := srcPoint.X + srcPoint.Y * icon.stride
|
||||
dstIndex := point.X + point.Y * stride
|
||||
if icon.data[srcIndex] {
|
||||
data[dstIndex] = color
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
func (icon binaryIcon) Bounds () image.Rectangle {
|
||||
return image.Rect(0, 0, icon.stride, len(icon.data) / icon.stride)
|
||||
}
|
||||
|
||||
func binaryIconFrom (source image.Image, clip image.Rectangle) (icon binaryIcon) {
|
||||
bounds := source.Bounds().Intersect(clip)
|
||||
if bounds.Empty() { return }
|
||||
|
||||
icon.stride = bounds.Dx()
|
||||
icon.data = make([]bool, bounds.Dx() * bounds.Dy())
|
||||
|
||||
point := image.Point { }
|
||||
dstIndex := 0
|
||||
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
|
||||
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
|
||||
r, g, b, a := source.At(point.X, point.Y).RGBA()
|
||||
if a > 0x8000 && (r + g + b) / 3 < 0x8000 {
|
||||
icon.data[dstIndex] = true
|
||||
}
|
||||
dstIndex ++
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
func init () {
|
||||
defaultAtlasImage, _, _ := image.Decode(bytes.NewReader(defaultAtlasBytes))
|
||||
defaultAtlas = artist.FromImage(defaultAtlasImage)
|
||||
|
||||
// PatternDead
|
||||
atlasCol(0, artist.Inset { })
|
||||
// PatternRaised
|
||||
atlasCol(1, artist.Inset { 6, 6, 6, 6 })
|
||||
// PatternSunken
|
||||
atlasCol(2, artist.Inset { 4, 4, 4, 4 })
|
||||
// PatternPinboard
|
||||
atlasCol(3, artist.Inset { 2, 2, 2, 2 })
|
||||
// PatternButton
|
||||
atlasCol(4, artist.Inset { 6, 6, 6, 6 })
|
||||
// PatternInput
|
||||
atlasCol(5, artist.Inset { 4, 4, 4, 4 })
|
||||
// PatternGutter
|
||||
atlasCol(6, artist.Inset { 7, 7, 7, 7 })
|
||||
// PatternHandle
|
||||
atlasCol(7, artist.Inset { 3, 3, 3, 3 })
|
||||
// PatternLine
|
||||
atlasCol(8, artist.Inset { 1, 1, 1, 1 })
|
||||
// PatternMercury
|
||||
atlasCol(13, artist.Inset { 2, 2, 2, 2 })
|
||||
// PatternTableHead:
|
||||
atlasCol(14, artist.Inset { 4, 4, 4, 4 })
|
||||
// PatternTableCell:
|
||||
atlasCol(15, artist.Inset { 4, 4, 4, 4 })
|
||||
// PatternLamp:
|
||||
atlasCol(16, artist.Inset { 4, 3, 4, 3 })
|
||||
|
||||
// PatternButton: basic.checkbox
|
||||
atlasCol(9, artist.Inset { 3, 3, 3, 3 })
|
||||
// PatternRaised: basic.listEntry
|
||||
atlasCol(10, artist.Inset { 3, 3, 3, 3 })
|
||||
// PatternRaised: fun.flatKey
|
||||
atlasCol(11, artist.Inset { 3, 3, 5, 3 })
|
||||
// PatternRaised: fun.sharpKey
|
||||
atlasCol(12, artist.Inset { 3, 3, 4, 3 })
|
||||
|
||||
// set up small icons
|
||||
defaultIconsSmallAtlasImage, _, _ := image.Decode (
|
||||
bytes.NewReader(defaultIconsSmallAtlasBytes))
|
||||
point := image.Point { }
|
||||
iconIndex := 0
|
||||
for point.Y = 0; point.Y < 20; point.Y ++ {
|
||||
for point.X = 0; point.X < 32; point.X ++ {
|
||||
defaultIconsSmall[iconIndex] = binaryIconFrom (
|
||||
defaultIconsSmallAtlasImage,
|
||||
image.Rect(0, 0, 16, 16).Add(point.Mul(16)))
|
||||
iconIndex ++
|
||||
}}
|
||||
|
||||
// set up large icons
|
||||
defaultIconsLargeAtlasImage, _, _ := image.Decode (
|
||||
bytes.NewReader(defaultIconsLargeAtlasBytes))
|
||||
point = image.Point { }
|
||||
iconIndex = 0
|
||||
for point.Y = 0; point.Y < 8; point.Y ++ {
|
||||
for point.X = 0; point.X < 32; point.X ++ {
|
||||
defaultIconsLarge[iconIndex] = binaryIconFrom (
|
||||
defaultIconsLargeAtlasImage,
|
||||
image.Rect(0, 0, 32, 32).Add(point.Mul(32)))
|
||||
iconIndex ++
|
||||
}}
|
||||
iconIndex = 384
|
||||
for point.Y = 8; point.Y < 12; point.Y ++ {
|
||||
for point.X = 0; point.X < 32; point.X ++ {
|
||||
defaultIconsLarge[iconIndex] = binaryIconFrom (
|
||||
defaultIconsLargeAtlasImage,
|
||||
image.Rect(0, 0, 32, 32).Add(point.Mul(32)))
|
||||
iconIndex ++
|
||||
}}
|
||||
}
|
||||
|
||||
type Theme struct { }
|
||||
|
||||
func (Theme) FontFace (style tomo.FontStyle, size tomo.FontSize, c tomo.Case) font.Face {
|
||||
switch style {
|
||||
case tomo.FontStyleBold:
|
||||
return defaultfont.FaceBold
|
||||
case tomo.FontStyleItalic:
|
||||
return defaultfont.FaceItalic
|
||||
case tomo.FontStyleBoldItalic:
|
||||
return defaultfont.FaceBoldItalic
|
||||
default:
|
||||
return defaultfont.FaceRegular
|
||||
}
|
||||
}
|
||||
|
||||
func (Theme) Icon (id tomo.Icon, size tomo.IconSize, c tomo.Case) artist.Icon {
|
||||
if size == tomo.IconSizeLarge {
|
||||
if id < 0 || int(id) >= len(defaultIconsLarge) {
|
||||
return nil
|
||||
} else {
|
||||
return defaultIconsLarge[id]
|
||||
}
|
||||
} else {
|
||||
if id < 0 || int(id) >= len(defaultIconsSmall) {
|
||||
return nil
|
||||
} else {
|
||||
return defaultIconsSmall[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (Theme) MimeIcon (data.Mime, tomo.IconSize, tomo.Case) artist.Icon {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Theme) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.Pattern {
|
||||
offset := 0; switch {
|
||||
case state.Disabled: offset = 1
|
||||
case state.Pressed && state.On: offset = 4
|
||||
case state.Focused && state.On: offset = 6
|
||||
case state.On: offset = 2
|
||||
case state.Pressed: offset = 3
|
||||
case state.Focused: offset = 5
|
||||
}
|
||||
|
||||
switch id {
|
||||
case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF)
|
||||
case tomo.PatternDead: return defaultTextures[0][offset]
|
||||
case tomo.PatternRaised: return defaultTextures[1][offset]
|
||||
case tomo.PatternSunken: return defaultTextures[2][offset]
|
||||
case tomo.PatternPinboard: return defaultTextures[3][offset]
|
||||
case tomo.PatternButton:
|
||||
switch {
|
||||
case c.Match("tomo", "checkbox", ""):
|
||||
return defaultTextures[9][offset]
|
||||
case c.Match("tomo", "piano", "flatKey"):
|
||||
return defaultTextures[11][offset]
|
||||
case c.Match("tomo", "piano", "sharpKey"):
|
||||
return defaultTextures[12][offset]
|
||||
default:
|
||||
return defaultTextures[4][offset]
|
||||
}
|
||||
case tomo.PatternInput: return defaultTextures[5][offset]
|
||||
case tomo.PatternGutter: return defaultTextures[6][offset]
|
||||
case tomo.PatternHandle: return defaultTextures[7][offset]
|
||||
case tomo.PatternLine: return defaultTextures[8][offset]
|
||||
case tomo.PatternMercury: return defaultTextures[13][offset]
|
||||
case tomo.PatternTableHead: return defaultTextures[14][offset]
|
||||
case tomo.PatternTableCell: return defaultTextures[15][offset]
|
||||
case tomo.PatternLamp: return defaultTextures[16][offset]
|
||||
default: return patterns.Uhex(0xFF00FFFF)
|
||||
}
|
||||
}
|
||||
|
||||
func (Theme) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA {
|
||||
if state.Disabled { return artutil.Hex(0x444444FF) }
|
||||
|
||||
return artutil.Hex (map[tomo.Color] uint32 {
|
||||
tomo.ColorBlack: 0x272d24FF,
|
||||
tomo.ColorRed: 0x8c4230FF,
|
||||
tomo.ColorGreen: 0x69905fFF,
|
||||
tomo.ColorYellow: 0x9a973dFF,
|
||||
tomo.ColorBlue: 0x3d808fFF,
|
||||
tomo.ColorPurple: 0x8c608bFF,
|
||||
tomo.ColorCyan: 0x3d8f84FF,
|
||||
tomo.ColorWhite: 0xaea894FF,
|
||||
tomo.ColorBrightBlack: 0x4f5142FF,
|
||||
tomo.ColorBrightRed: 0xbd6f59FF,
|
||||
tomo.ColorBrightGreen: 0x8dad84FF,
|
||||
tomo.ColorBrightYellow: 0xe2c558FF,
|
||||
tomo.ColorBrightBlue: 0x77b1beFF,
|
||||
tomo.ColorBrightPurple: 0xc991c8FF,
|
||||
tomo.ColorBrightCyan: 0x74c7b7FF,
|
||||
tomo.ColorBrightWhite: 0xcfd7d2FF,
|
||||
|
||||
tomo.ColorForeground: 0x000000FF,
|
||||
tomo.ColorMidground: 0x97A09BFF,
|
||||
tomo.ColorBackground: 0xAAAAAAFF,
|
||||
tomo.ColorShadow: 0x445754FF,
|
||||
tomo.ColorShine: 0xCFD7D2FF,
|
||||
tomo.ColorAccent: 0x408090FF,
|
||||
} [id])
|
||||
}
|
||||
|
||||
func (Theme) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
|
||||
switch id {
|
||||
case tomo.PatternSunken:
|
||||
if c.Match("tomo", "progressBar", "") {
|
||||
return artist.I(2, 1, 1, 2)
|
||||
} else if c.Match("tomo", "list", "") {
|
||||
return artist.I(2)
|
||||
} else if c.Match("tomo", "flowList", "") {
|
||||
return artist.I(2)
|
||||
} else {
|
||||
return artist.I(8)
|
||||
}
|
||||
case tomo.PatternPinboard:
|
||||
if c.Match("tomo", "piano", "") {
|
||||
return artist.I(2)
|
||||
} else {
|
||||
return artist.I(8)
|
||||
}
|
||||
case tomo.PatternTableCell: return artist.I(5)
|
||||
case tomo.PatternTableHead: return artist.I(5)
|
||||
case tomo.PatternGutter: return artist.I(0)
|
||||
case tomo.PatternLine: return artist.I(1)
|
||||
case tomo.PatternMercury: return artist.I(5)
|
||||
case tomo.PatternLamp: return artist.I(5, 5, 5, 6)
|
||||
default: return artist.I(8)
|
||||
}
|
||||
}
|
||||
|
||||
func (Theme) Margin (id tomo.Pattern, c tomo.Case) image.Point {
|
||||
switch id {
|
||||
case tomo.PatternSunken:
|
||||
if c.Match("tomo", "list", "") {
|
||||
return image.Pt(-1, -1)
|
||||
} else if c.Match("tomo", "flowList", "") {
|
||||
return image.Pt(-1, -1)
|
||||
} else {
|
||||
return image.Pt(8, 8)
|
||||
}
|
||||
default: return image.Pt(8, 8)
|
||||
}
|
||||
}
|
||||
|
||||
func (Theme) Hints (pattern tomo.Pattern, c tomo.Case) (hints tomo.Hints) {
|
||||
return
|
||||
}
|
||||
|
||||
func (Theme) Sink (pattern tomo.Pattern, c tomo.Case) image.Point {
|
||||
return image.Point { 1, 1 }
|
||||
}
|
21
plugins/x/main.go
Normal file
21
plugins/x/main.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Plugin x provides the X11 backend as a plugin.
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/plugins/x/x"
|
||||
|
||||
func Expects () tomo.Version {
|
||||
return tomo.Version { 0, 0, 0 }
|
||||
}
|
||||
|
||||
func Name () string {
|
||||
return "X"
|
||||
}
|
||||
|
||||
func Description () string {
|
||||
return "Provides an X11 backend."
|
||||
}
|
||||
|
||||
func NewBackend () (tomo.Backend, error) {
|
||||
return x.NewBackend()
|
||||
}
|
@ -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,
|
||||
) (
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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)
|
@ -13,14 +13,14 @@ import "github.com/jezek/xgbutil/mousebind"
|
||||
import "github.com/jezek/xgbutil/xgraphics"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
type mainWindow struct { *window }
|
||||
type menuWindow struct { *window }
|
||||
type window struct {
|
||||
system
|
||||
|
||||
backend *Backend
|
||||
backend *backend
|
||||
xWindow *xwindow.Window
|
||||
xCanvas *xgraphics.Image
|
||||
|
||||
@ -40,7 +40,7 @@ type window struct {
|
||||
onClose func ()
|
||||
}
|
||||
|
||||
func (backend *Backend) NewWindow (
|
||||
func (backend *backend) NewWindow (
|
||||
bounds image.Rectangle,
|
||||
) (
|
||||
output tomo.MainWindow,
|
||||
@ -53,7 +53,7 @@ func (backend *Backend) NewWindow (
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (backend *Backend) newWindow (
|
||||
func (backend *backend) newWindow (
|
||||
bounds image.Rectangle,
|
||||
override bool,
|
||||
) (
|
||||
@ -67,7 +67,6 @@ func (backend *Backend) newWindow (
|
||||
|
||||
window.system.initialize()
|
||||
window.system.pushFunc = window.pasteAndPush
|
||||
window.theme.Case = tomo.C("tomo", "window")
|
||||
|
||||
window.xWindow, err = xwindow.Generate(backend.connection)
|
||||
if err != nil { return }
|
||||
@ -121,9 +120,6 @@ func (backend *Backend) newWindow (
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.SelectionRequestFun(window.handleSelectionRequest).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
|
||||
window.SetTheme(backend.theme)
|
||||
window.SetConfig(backend.config)
|
||||
|
||||
window.metrics.bounds = bounds
|
||||
window.setMinimumSize(8, 8)
|
||||
@ -418,7 +414,7 @@ func (window *window) pasteAndPush (region image.Rectangle) {
|
||||
}
|
||||
|
||||
func (window *window) paste (region image.Rectangle) {
|
||||
canvas := canvas.Cut(window.canvas, region)
|
||||
canvas := artist.Cut(window.canvas, region)
|
||||
data, stride := canvas.Buffer()
|
||||
bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds())
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user