Compare commits
320
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
794ab1b5e8 | ||
|
|
83d0b32fba | ||
|
|
ae12945676 | ||
|
|
f3185999a2 | ||
|
|
8ad14cd542 | ||
|
|
8d587ae3b4 | ||
|
|
4f8469c359 | ||
|
|
6e1369da5c | ||
|
|
abe63f4118 | ||
|
|
cd6d8f3ff6 | ||
|
|
37df313544 | ||
|
|
2ac23185e6 | ||
|
|
6241be1969 | ||
|
|
9588996bd8 | ||
|
|
2e3af402d5 | ||
|
|
69e73a7b84 | ||
|
|
b84e444697 | ||
|
|
b51eb79033 | ||
|
|
8e5ad8f385 | ||
|
|
b479ba8f0f | ||
|
|
567358bf4c | ||
|
|
10d5358390 | ||
|
|
09d360826b | ||
|
|
b3a9bba255 | ||
|
|
6acd8be05b | ||
|
|
39b8b96513 | ||
|
|
363779a947 | ||
|
|
cd8371a3f3 | ||
|
|
4c9743387b | ||
|
|
e3d194562c | ||
|
|
7472cf52d9 | ||
|
|
8d6deb3456 | ||
|
|
cc03440125 | ||
|
|
cfdebddbdd | ||
|
|
8a8e63ba05 | ||
|
|
6578480195 | ||
|
|
72fc28e223 | ||
|
|
9e754cdb59 | ||
|
|
fb25496f34 | ||
|
|
9cfbe0aa01 | ||
|
|
e0be332953 | ||
|
|
acc4148a18 | ||
|
|
3caa9c53ef | ||
|
|
4a723ff296 | ||
|
|
de4bce8c46 | ||
|
|
ca27a810c7 | ||
|
|
77e1151bd8 | ||
|
|
a2a9af3311 | ||
|
|
800ee2570f | ||
|
|
b92bdced9c | ||
|
|
85f995aa10 | ||
|
|
7b7005c068 | ||
|
|
d5d2cc1f4d | ||
|
|
dfdd721303 | ||
|
|
fe3ac7ec1e | ||
|
|
f5180efc8a | ||
|
|
e5d6e03975 | ||
|
|
990e60eea4 | ||
|
|
4e20726eff | ||
|
|
3502da814d | ||
|
|
5407e52108 | ||
|
|
7bb7111460 | ||
|
|
e41fd63f35 | ||
|
|
58e02dced8 | ||
|
|
61d3c14519 | ||
|
|
cd7a683af9 | ||
|
|
e1156d65c8 | ||
|
|
fc4b2eb36d | ||
|
|
1c0dee1b95 | ||
|
|
cc14151a14 | ||
|
|
6622799019 | ||
|
|
f88268bb0e | ||
|
|
c1046b1bcb | ||
|
|
27799a9670 | ||
|
|
2bd7d0fad5 | ||
|
|
e5619ebf07 | ||
|
|
399dda75bd | ||
|
|
53f78cb0e7 | ||
|
|
eaee284aaf | ||
|
|
4fe778c095 | ||
|
|
d4b9ffb046 | ||
|
|
580b7d2ad0 | ||
|
|
ff3802ca5e | ||
|
|
17fda82bbe | ||
|
|
0063afed8c | ||
|
|
1323a6c1ca | ||
|
|
698414ee65 | ||
|
|
dbee2ff5a9 | ||
|
|
afdecc2c8b | ||
|
|
ac58a43220 | ||
|
|
7cdc5868e5 | ||
|
|
14080b1f88 | ||
|
|
a2b1ac0c73 | ||
|
|
6276327613 | ||
|
|
d44e7b51da | ||
|
|
785cc2d908 | ||
|
|
0bf5c3b86c | ||
|
|
6b13e772a9 | ||
|
|
427b5e025d | ||
|
|
5ca3b80e8e | ||
|
|
775390e884 | ||
|
|
a7de6c7f3b | ||
|
|
7d4ddaf387 | ||
|
|
b9c8350677 | ||
|
|
ed6de3a36f | ||
|
|
e16195d274 | ||
|
|
0a21f605fb | ||
|
|
0cd7fb9be9 | ||
|
|
c0b205c6f0 | ||
|
|
1044c8299a | ||
|
|
bb50c7d7a7 | ||
|
|
9d78a599aa | ||
|
|
986315d5db | ||
|
|
9e16f7b532 | ||
|
|
ca86328506 | ||
|
|
a43f5ce595 | ||
|
|
437aef0c27 | ||
|
|
5cf0b162c0 | ||
|
|
6e4310b9ad | ||
|
|
68128c94d8 | ||
|
|
4c6f1f80e7 | ||
|
|
e931717241 | ||
|
|
bb9c5df088 | ||
|
|
407b957687 | ||
|
|
99c890e6cd | ||
|
|
b190f01a71 | ||
|
|
fa898be046 | ||
|
|
a51372bd7b | ||
|
|
670cf36c14 | ||
|
|
d67aac3d4f | ||
|
|
2987331a31 | ||
|
|
da47026d1c | ||
|
|
aed448671b | ||
|
|
6db5901247 | ||
|
|
8abb45e77a | ||
|
|
d1fcc6e37f | ||
|
|
dc077a02ab | ||
|
|
43a664009c | ||
|
|
f21a41982e | ||
|
|
34b79ee30d | ||
|
|
8db8fab14a | ||
|
|
cbdebc7f9f | ||
|
|
570853890e | ||
|
|
260e2b31b6 | ||
|
|
d633e0f5f6 | ||
|
|
f377372354 | ||
|
|
55c13ebf89 | ||
|
|
eca75c642b | ||
|
|
e38e2a47f9 | ||
|
|
b357768c36 | ||
|
|
ebefcb03b3 | ||
|
|
13518d9ba6 | ||
|
|
ff51777834 | ||
|
|
941f6f6576 | ||
|
|
603d029c50 | ||
|
|
5c2be06601 | ||
|
|
2d0a0cc073 | ||
|
|
46a4858597 | ||
|
|
6c3230c0f8 | ||
|
|
6ede0d0770 | ||
|
|
7521808872 | ||
|
|
bc72333ff0 | ||
|
|
7fee67474f | ||
|
|
9f70804420 | ||
|
|
e9dff8ad07 | ||
|
|
bd636eaa7f | ||
|
|
4e488582d0 | ||
|
|
b8bf5743b4 | ||
|
|
8c03b516e3 | ||
|
|
50d7d74097 | ||
|
|
03dfcf02bf | ||
|
|
c7cd944ae2 | ||
|
|
d1b5cd863a | ||
|
|
e7ec9ad6f3 | ||
|
|
c1e2bf46a6 | ||
|
|
ab78bc640d | ||
|
|
7b300333cf | ||
|
|
53bfc8df68 | ||
|
|
719b7b99ac | ||
|
|
e7ad588fb8 | ||
|
|
6406b70077 | ||
|
|
6456759bfc | ||
|
|
0d4104255c | ||
|
|
17422cc054 | ||
|
|
a16f3c2cd7 | ||
|
|
017543aa0f | ||
|
|
f9e5503320 | ||
|
|
8abc4defa7 | ||
|
|
fc228a13d3 | ||
|
|
1ebf5e1103 | ||
|
|
ab61615018 | ||
|
|
39dc09bc4a | ||
|
|
0aede3502b | ||
|
|
6f15ff3366 | ||
|
|
01a0fc1bd3 | ||
|
|
02a27447b9 | ||
|
|
6a3f45a2e0 | ||
|
|
3aa8495873 | ||
|
|
bdc1109bcf | ||
|
|
a2c0ff5f4c | ||
|
|
d710d13f0d | ||
|
|
fff5ad4d96 | ||
|
|
8447b06641 | ||
|
|
6a08d0f317 | ||
|
|
d3d3cddfef | ||
|
|
45021b6153 | ||
|
|
6638a471c7 | ||
|
|
6c8ff55dc1 | ||
|
|
7ec5e1ab2a | ||
|
|
14802b4b82 | ||
|
|
f74f6a43f8 | ||
|
|
68341517f7 | ||
|
|
dcc672e2bc | ||
|
|
d9bddce20b | ||
|
|
60aac053fb | ||
|
|
faf5ebb283 | ||
|
|
f37101eb9e | ||
|
|
d475e5e2ec | ||
|
|
221647a265 | ||
|
|
bf667aded9 | ||
|
|
b4befa5aa5 | ||
|
|
0f272f4835 | ||
|
|
d651570746 | ||
|
|
493c5210a7 | ||
|
|
0fd56f272c | ||
|
|
4c6e01203c | ||
|
|
b189518c92 | ||
|
|
4b788dd783 | ||
|
|
cdf805dadc | ||
|
|
6258c77f86 | ||
|
|
b90ffeb4fd | ||
|
|
a4ef28cdd0 | ||
|
|
c55925d152 | ||
|
|
11b680db63 | ||
|
|
d57db6327d | ||
|
|
0d8d2a0190 | ||
|
|
6ee3014fda | ||
|
|
0ebf0bc814 | ||
|
|
40aa1a788b | ||
|
|
bffdb000ed | ||
|
|
5ca9206f65 | ||
|
|
1239f4e03d | ||
|
|
8aaa017902 | ||
|
|
639baecee5 | ||
|
|
c1b3562d10 | ||
|
|
ef325d5161 | ||
|
|
2f60abdfa3 | ||
|
|
1a66224648 | ||
|
|
275e113e3b | ||
|
|
0015820fac | ||
|
|
f4799ba03d | ||
|
|
14ad35d85c | ||
|
|
a34e8768ab | ||
|
|
b08cbea320 | ||
|
|
9d84c50db3 | ||
|
|
99e029ae09 | ||
|
|
5149c27cf3 | ||
|
|
7ef95cc751 | ||
|
|
b09994973c | ||
|
|
37048c6759 | ||
|
|
be45f7ad71 | ||
|
|
c45268d8c1 | ||
|
|
92e5822185 | ||
|
|
d31aee1ba8 | ||
|
|
0f8affd2b2 | ||
|
|
3d28ebe4cf | ||
|
|
5afbc0e713 | ||
|
|
b7a7800370 | ||
|
|
15fa3b2497 | ||
|
|
081b005679 | ||
|
|
1be769526d | ||
|
|
51084a6cfe | ||
|
|
677dca1dbf | ||
|
|
9cc9e78504 | ||
|
|
5d4a26a877 | ||
|
|
aaa794ac04 | ||
|
|
8658ecd879 | ||
|
|
1c28613981 | ||
|
|
8e1638e054 | ||
|
|
aff9aca835 | ||
|
|
cf672824a6 | ||
|
|
04884bd8e3 | ||
|
|
305acea285 | ||
|
|
f3c1c95a57 | ||
|
|
423e6869c0 | ||
|
|
803812f9c9 | ||
|
|
c171273240 | ||
|
|
11402cfc25 | ||
|
|
7e0d64e8bd | ||
|
|
d38bd1cbf5 | ||
|
|
865dd20724 | ||
|
|
6c46fc6f7c | ||
|
|
0071994ba6 | ||
|
|
11c747e225 | ||
|
|
61bbe0e346 | ||
|
|
1e8bb56b7c | ||
|
|
ae6cf128f8 | ||
|
|
ecaad02c0b | ||
|
|
cad10a1fb1 | ||
|
|
912a3f9f66 | ||
|
|
531b0ffce9 | ||
|
|
9c12cd7e18 | ||
|
|
4f6f4e1f1a | ||
|
|
dc5ddfc0bd | ||
|
|
5fc5af92df | ||
|
|
90ce0d7281 | ||
|
|
be286fa86c | ||
|
|
252433f13d | ||
|
|
165d0835bf | ||
|
|
56e11ae1de | ||
|
|
1d9fb6024d | ||
|
|
c13cdd570d | ||
|
|
5af8d7fd97 | ||
|
|
b6eb158964 | ||
|
|
6bb5b2d79c | ||
|
|
538123dcd5 | ||
|
|
38baa97e76 | ||
|
|
e9e6e4fbe7 | ||
|
|
285cb4810f | ||
|
|
2cac2b3bd0 |
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -1,29 +1,24 @@
|
||||
# 
|
||||
# 
|
||||
|
||||
This repository is [mirrored on GitHub](https://github.com/sashakoshka/tomo).
|
||||
|
||||
Please note: Tomo is in early development. Some features may not work properly,
|
||||
and its API may change without notice.
|
||||
|
||||
Tomo is a retro-looking GUI toolkit written in pure Go. It is designed with
|
||||
these goals in mind:
|
||||
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.
|
||||
|
||||
- Modularity: the core of Tomo is mostly composed of interfaces—and the
|
||||
overwhelming majority of its code resides in pluggable modules. If you don't
|
||||
need it, then dont import it—and you can be assured it won't be there.
|
||||
- Extendability: during the design of Tomo's API, use cases such as creating
|
||||
custom backends, elements, and layouts were given just as much importance as
|
||||
normal application building. Your custom element is a first-class citizen.
|
||||
- Independence: Tomo is minimally dependent on code outside of the Go
|
||||
standard library. Because of this, the file size of a compiled Tomo application
|
||||
is typically very small.
|
||||
- Frugality: Tomo foregoes things like animations and anti-aliasing in order to
|
||||
use a minimal amount of system resources without even having to touch the GPU.
|
||||
- Consistency: Tomo's design is not only consistent within itself, but also
|
||||
with the Go standard library. If you've worked with Go in the past, Tomo will
|
||||
feel pleasantly familliar.
|
||||
Nasin is an application framework that runs on top of Tomo. It supports plugins
|
||||
which can extend any application with backends, themes, etc.
|
||||
|
||||
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).
|
||||
## 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).
|
||||
|
||||
@@ -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 ()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package artist
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
|
||||
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, color color.RGBA, at image.Point)
|
||||
|
||||
// Bounds returns the bounds of the icon.
|
||||
Bounds () image.Rectangle
|
||||
}
|
||||
@@ -15,6 +15,28 @@ type Side int; const (
|
||||
// the left at index three. These values may be negative.
|
||||
type Inset [4]int
|
||||
|
||||
// I allows you to create an inset in a CSS-ish way:
|
||||
//
|
||||
// - One argument: all sides are set to this value
|
||||
// - Two arguments: the top and bottom sides are set to the first value, and
|
||||
// the left and right sides are set to the second value.
|
||||
// - Three arguments: the top side is set by the first value, the left and
|
||||
// right sides are set by the second vaue, and the bottom side is set by the
|
||||
// third value.
|
||||
// - Four arguments: each value corresponds to a side.
|
||||
//
|
||||
// This function will panic if an argument count that isn't one of these is
|
||||
// given.
|
||||
func I (sides ...int) Inset {
|
||||
switch len(sides) {
|
||||
case 1: return Inset { sides[0], sides[0], sides[0], sides[0] }
|
||||
case 2: return Inset { sides[0], sides[1], sides[0], sides[1] }
|
||||
case 3: return Inset { sides[0], sides[1], sides[2], sides[1] }
|
||||
case 4: return Inset { sides[0], sides[1], sides[2], sides[3] }
|
||||
default: panic("I: illegal argument count.")
|
||||
}
|
||||
}
|
||||
|
||||
// Apply returns the given rectangle, shrunk on all four sides by the given
|
||||
// inset. If a measurment of the inset is negative, that side will instead be
|
||||
// expanded outward. If the rectangle's dimensions cannot be reduced any
|
||||
|
||||
+5
-59
@@ -1,67 +1,13 @@
|
||||
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.
|
||||
type Pattern interface {
|
||||
// Draw draws to destination, using the bounds of destination as a width
|
||||
// and height for things like gradients, bevels, etc. The pattern may
|
||||
// not draw outside the union of destination.Bounds() and clip. The
|
||||
// clipping rectangle effectively takes a subset of the pattern. To
|
||||
// change the bounds of the pattern itself, use canvas.Cut() on the
|
||||
// destination before passing it to Draw().
|
||||
Draw (destination canvas.Canvas, clip image.Rectangle)
|
||||
// Draw draws the pattern onto the destination canvas, using the
|
||||
// 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, bounds image.Rectangle)
|
||||
}
|
||||
|
||||
// Draw lets you use several clipping rectangles to draw a pattern.
|
||||
func Draw (
|
||||
destination canvas.Canvas,
|
||||
source Pattern,
|
||||
clips ...image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
for _, clip := range clips {
|
||||
source.Draw(destination, clip)
|
||||
updatedRegion = updatedRegion.Union(clip)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DrawBounds lets you specify an overall bounding rectangle for drawing a
|
||||
// pattern. The destination is cut to this rectangle.
|
||||
func DrawBounds (
|
||||
destination canvas.Canvas,
|
||||
source Pattern,
|
||||
bounds image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
return Draw(canvas.Cut(destination, bounds), source, bounds)
|
||||
}
|
||||
|
||||
// DrawShatter is like an inverse of Draw, drawing nothing in the areas
|
||||
// specified in "rocks".
|
||||
func DrawShatter (
|
||||
destination canvas.Canvas,
|
||||
source Pattern,
|
||||
rocks ...image.Rectangle,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
tiles := shatter.Shatter(destination.Bounds(), rocks...)
|
||||
return Draw(destination, source, 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) (allocated canvas.Canvas) {
|
||||
allocated = canvas.NewBasicCanvas(width, height)
|
||||
source.Draw(allocated, allocated.Bounds())
|
||||
return
|
||||
}
|
||||
|
||||
+10
-11
@@ -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
|
||||
@@ -24,32 +23,32 @@ import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
// sections are automatically sized around it.
|
||||
//
|
||||
// When drawn to a destination canvas, the bounds of sections 1, 3, 4, 5, and 7
|
||||
// are expanded or contracted to fit the destination's bounds. All sections
|
||||
// are rendered as if they are Texture patterns, meaning these flexible sections
|
||||
// are expanded or contracted to fit the given drawing bounds. All sections are
|
||||
// rendered as if they are Texture patterns, meaning these flexible sections
|
||||
// will repeat to fill in any empty space.
|
||||
//
|
||||
// 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 clipping
|
||||
// Draw draws the border pattern onto the destination canvas within the given
|
||||
// bounds.
|
||||
func (pattern Border) Draw (destination canvas.Canvas, clip image.Rectangle) {
|
||||
bounds := clip.Canon().Intersect(destination.Bounds())
|
||||
if bounds.Empty() { return }
|
||||
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(destination.Bounds(), pattern.Inset)
|
||||
dstSections := nonasect(bounds, pattern.Inset)
|
||||
for index, section := range dstSections {
|
||||
srcTextures[index].Draw(canvas.Cut(destination, section), clip)
|
||||
srcTextures[index].Draw(destination, section)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+56
-20
@@ -1,36 +1,72 @@
|
||||
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 clipping bounds. The minimum
|
||||
// points of the pattern's canvas and the destination canvas will be lined up.
|
||||
func (pattern Texture) Draw (destination canvas.Canvas, clip image.Rectangle) {
|
||||
realBounds := destination.Bounds()
|
||||
bounds := clip.Canon().Intersect(realBounds)
|
||||
if bounds.Empty() { return }
|
||||
|
||||
// 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 artist.Canvas, bounds image.Rectangle) {
|
||||
dstBounds := bounds.Canon().Intersect(destination.Bounds())
|
||||
if dstBounds.Empty() { return }
|
||||
|
||||
dstData, dstStride := destination.Buffer()
|
||||
srcData, srcStride := pattern.Buffer()
|
||||
srcBounds := pattern.Bounds()
|
||||
|
||||
point := image.Point { }
|
||||
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(realBounds.Min).Add(srcBounds.Min)
|
||||
|
||||
dstIndex := point.X + point.Y * dstStride
|
||||
srcIndex :=
|
||||
wrap(srcPoint.X, srcBounds.Min.X, srcBounds.Max.X) +
|
||||
wrap(srcPoint.Y, srcBounds.Min.Y, srcBounds.Max.Y) * srcStride
|
||||
dstData[dstIndex] = srcData[srcIndex]
|
||||
}}
|
||||
// offset is a vector that is added to points in destination space to
|
||||
// convert them to points in source space
|
||||
offset := srcBounds.Min.Sub(bounds.Min)
|
||||
|
||||
// calculate the starting position in source space
|
||||
srcPoint := dstBounds.Min.Add(offset)
|
||||
srcPoint.X = wrap(srcPoint.X, srcBounds.Min.X, srcBounds.Max.X)
|
||||
srcPoint.Y = wrap(srcPoint.Y, srcBounds.Min.Y, srcBounds.Max.Y)
|
||||
srcStartPoint := srcPoint
|
||||
|
||||
// for each row
|
||||
dstPoint := image.Point { }
|
||||
for dstPoint.Y = dstBounds.Min.Y; dstPoint.Y < dstBounds.Max.Y; dstPoint.Y ++ {
|
||||
srcPoint.X = srcStartPoint.X
|
||||
dstPoint.X = dstBounds.Min.X
|
||||
dstYComponent := dstPoint.Y * dstStride
|
||||
srcYComponent := srcPoint.Y * srcStride
|
||||
|
||||
// for each pixel in the row
|
||||
for {
|
||||
// draw pixel
|
||||
dstIndex := dstYComponent + dstPoint.X
|
||||
srcIndex := srcYComponent + srcPoint.X
|
||||
dstData[dstIndex] = srcData[srcIndex]
|
||||
|
||||
// increment X in source space. wrap to start if out of
|
||||
// bounds.
|
||||
srcPoint.X ++
|
||||
if srcPoint.X >= srcBounds.Max.X {
|
||||
srcPoint.X = srcBounds.Min.X
|
||||
}
|
||||
|
||||
// increment X in destination space. stop drawing this
|
||||
// row if out of bounds.
|
||||
dstPoint.X ++
|
||||
if dstPoint.X >= dstBounds.Max.X {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// increment row in source space. wrap to start if out of
|
||||
// bounds.
|
||||
srcPoint.Y ++
|
||||
if srcPoint.Y >= srcBounds.Max.Y {
|
||||
srcPoint.Y = srcBounds.Min.Y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func wrap (value, min, max int) int {
|
||||
|
||||
@@ -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 clipping rectangle with the pattern's color.
|
||||
func (pattern Uniform) Draw (destination canvas.Canvas, clip image.Rectangle) {
|
||||
shapes.FillColorRectangle(destination, color.RGBA(pattern), clip)
|
||||
// Draw fills the bounding rectangle with the pattern's color.
|
||||
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))
|
||||
}
|
||||
|
||||
+21
-18
@@ -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,9 @@ 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,
|
||||
) {
|
||||
@@ -20,15 +21,17 @@ func FillEllipse (
|
||||
srcData, srcStride := source.Buffer()
|
||||
|
||||
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
|
||||
bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds())
|
||||
realBounds := destination.Bounds()
|
||||
drawBounds :=
|
||||
source.Bounds().Sub(offset).
|
||||
Intersect(destination.Bounds()).
|
||||
Intersect(bounds)
|
||||
if bounds.Empty() { return }
|
||||
updatedRegion = bounds
|
||||
|
||||
point := image.Point { }
|
||||
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 ++ {
|
||||
if inEllipse(point, realBounds) {
|
||||
for point.Y = drawBounds.Min.Y; point.Y < drawBounds.Max.Y; point.Y ++ {
|
||||
for point.X = drawBounds.Min.X; point.X < drawBounds.Max.X; point.X ++ {
|
||||
if inEllipse(point, bounds) {
|
||||
offsetPoint := point.Add(offset)
|
||||
dstIndex := point.X + point.Y * dstStride
|
||||
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
|
||||
@@ -39,8 +42,9 @@ func FillEllipse (
|
||||
}
|
||||
|
||||
func StrokeEllipse (
|
||||
destination canvas.Canvas,
|
||||
source canvas.Canvas,
|
||||
destination artist.Canvas,
|
||||
source artist.Canvas,
|
||||
bounds image.Rectangle,
|
||||
weight int,
|
||||
) {
|
||||
if weight < 1 { return }
|
||||
@@ -48,10 +52,9 @@ func StrokeEllipse (
|
||||
dstData, dstStride := destination.Buffer()
|
||||
srcData, srcStride := source.Buffer()
|
||||
|
||||
bounds := destination.Bounds().Inset(weight - 1)
|
||||
drawBounds := destination.Bounds().Inset(weight - 1)
|
||||
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
|
||||
realBounds := destination.Bounds()
|
||||
if bounds.Empty() { return }
|
||||
if drawBounds.Empty() { return }
|
||||
|
||||
context := ellipsePlottingContext {
|
||||
plottingContext: plottingContext {
|
||||
@@ -61,11 +64,11 @@ func StrokeEllipse (
|
||||
srcStride: srcStride,
|
||||
weight: weight,
|
||||
offset: offset,
|
||||
bounds: realBounds,
|
||||
bounds: bounds,
|
||||
},
|
||||
radii: image.Pt(bounds.Dx() / 2, bounds.Dy() / 2),
|
||||
radii: image.Pt(drawBounds.Dx() / 2, drawBounds.Dy() / 2),
|
||||
}
|
||||
context.center = bounds.Min.Add(context.radii)
|
||||
context.center = drawBounds.Min.Add(context.radii)
|
||||
context.plotEllipse()
|
||||
}
|
||||
|
||||
@@ -167,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,
|
||||
) (
|
||||
@@ -193,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,
|
||||
@@ -70,7 +70,6 @@ func (context linePlottingContext) lineLow () {
|
||||
point := context.min
|
||||
|
||||
for ; point.X < context.max.X; point.X ++ {
|
||||
if !point.In(context.bounds) { break }
|
||||
context.plotColor(point)
|
||||
if D > 0 {
|
||||
D += 2 * (deltaY - deltaX)
|
||||
@@ -95,7 +94,6 @@ func (context linePlottingContext) lineHigh () {
|
||||
point := context.min
|
||||
|
||||
for ; point.Y < context.max.Y; point.Y ++ {
|
||||
if !point.In(context.bounds) { break }
|
||||
context.plotColor(point)
|
||||
if D > 0 {
|
||||
point.X += xi
|
||||
|
||||
+41
-27
@@ -2,28 +2,32 @@ 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,
|
||||
) {
|
||||
dstData, dstStride := destination.Buffer()
|
||||
srcData, srcStride := source.Buffer()
|
||||
|
||||
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
|
||||
bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds())
|
||||
if bounds.Empty() { return }
|
||||
updatedRegion = bounds
|
||||
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
|
||||
drawBounds :=
|
||||
source.Bounds().Sub(offset).
|
||||
Intersect(destination.Bounds()).
|
||||
Intersect(bounds)
|
||||
if drawBounds.Empty() { return }
|
||||
updatedRegion = drawBounds
|
||||
|
||||
point := image.Point { }
|
||||
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 ++ {
|
||||
for point.Y = drawBounds.Min.Y; point.Y < drawBounds.Max.Y; point.Y ++ {
|
||||
for point.X = drawBounds.Min.X; point.X < drawBounds.Max.X; point.X ++ {
|
||||
offsetPoint := point.Add(offset)
|
||||
dstIndex := point.X + point.Y * dstStride
|
||||
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
|
||||
@@ -34,39 +38,44 @@ func FillRectangle (
|
||||
}
|
||||
|
||||
func StrokeRectangle (
|
||||
destination canvas.Canvas,
|
||||
source canvas.Canvas,
|
||||
destination artist.Canvas,
|
||||
source artist.Canvas,
|
||||
bounds image.Rectangle,
|
||||
weight int,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
bounds := destination.Bounds()
|
||||
insetBounds := bounds.Inset(weight)
|
||||
if insetBounds.Empty() {
|
||||
FillRectangle(destination, source)
|
||||
return
|
||||
return FillRectangle(destination, source, bounds)
|
||||
}
|
||||
FillRectangleShatter(destination, source, insetBounds)
|
||||
return FillRectangleShatter(destination, source, bounds, insetBounds)
|
||||
}
|
||||
|
||||
// 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,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
tiles := shatter.Shatter(destination.Bounds(), rocks...)
|
||||
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
|
||||
tiles := shatter.Shatter(bounds, rocks...)
|
||||
for _, tile := range tiles {
|
||||
FillRectangle (
|
||||
canvas.Cut(destination, tile),
|
||||
canvas.Cut(source, tile.Add(offset)))
|
||||
artist.Cut(destination, tile),
|
||||
source, tile)
|
||||
updatedRegion = updatedRegion.Union(tile)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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,
|
||||
) (
|
||||
@@ -88,29 +97,34 @@ 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,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
tiles := shatter.Shatter(bounds, rocks...)
|
||||
for _, tile := range tiles {
|
||||
FillColorRectangle(destination, color, tile)
|
||||
updatedRegion = updatedRegion.Union(tile)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
insetBounds := bounds.Inset(weight)
|
||||
if insetBounds.Empty() {
|
||||
FillColorRectangle(destination, color, bounds)
|
||||
return
|
||||
return FillColorRectangle(destination, color, bounds)
|
||||
}
|
||||
FillColorRectangleShatter(destination, color, bounds, insetBounds)
|
||||
return FillColorRectangleShatter(destination, color, bounds, insetBounds)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
+23
-42
@@ -1,17 +1,13 @@
|
||||
package tomo
|
||||
|
||||
import "errors"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
import "image"
|
||||
|
||||
// Backend represents a connection to a display server, or something similar.
|
||||
// It is capable of managing an event loop, and creating windows.
|
||||
type Backend interface {
|
||||
// Run runs the backend's event loop. It must block until the backend
|
||||
// experiences a fatal error, or Stop() is called.
|
||||
Run () (err error)
|
||||
Run () error
|
||||
|
||||
// Stop stops the backend's event loop.
|
||||
Stop ()
|
||||
@@ -20,51 +16,36 @@ type Backend interface {
|
||||
// possible. This method must be safe to call from other threads.
|
||||
Do (callback func ())
|
||||
|
||||
// NewWindow creates a new window with the specified width and height,
|
||||
// and returns a struct representing it that fulfills the Window
|
||||
// interface.
|
||||
NewWindow (width, height int) (window elements.Window, err error)
|
||||
// NewEntity creates a new entity for the specified element.
|
||||
NewEntity (owner Element) Entity
|
||||
|
||||
// Copy puts data into the clipboard.
|
||||
Copy (data.Data)
|
||||
|
||||
// Paste returns the data currently in the clipboard.
|
||||
Paste (accept []data.Mime) (data.Data)
|
||||
// NewWindow creates a new window within the specified bounding
|
||||
// rectangle. The position on screen may be overridden by the backend or
|
||||
// operating system.
|
||||
NewWindow (bounds image.Rectangle) (MainWindow, error)
|
||||
|
||||
// SetTheme sets the theme of all open windows.
|
||||
SetTheme (theme.Theme)
|
||||
SetTheme (Theme)
|
||||
|
||||
// SetConfig sets the configuration of all open windows.
|
||||
SetConfig (config.Config)
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
// 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,339 +0,0 @@
|
||||
package x
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
|
||||
import "github.com/jezek/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
|
||||
type scrollSum struct {
|
||||
x, y int
|
||||
}
|
||||
|
||||
const scrollDistance = 16
|
||||
|
||||
func (sum *scrollSum) add (button xproto.Button, window *Window, state uint16) {
|
||||
shift :=
|
||||
(state & xproto.ModMaskShift) > 0 ||
|
||||
(state & window.backend.modifierMasks.shiftLock) > 0
|
||||
if shift {
|
||||
switch button {
|
||||
case 4:
|
||||
sum.x -= scrollDistance
|
||||
case 5:
|
||||
sum.x += scrollDistance
|
||||
case 6:
|
||||
sum.y -= scrollDistance
|
||||
case 7:
|
||||
sum.y += scrollDistance
|
||||
}
|
||||
} else {
|
||||
switch button {
|
||||
case 4:
|
||||
sum.y -= scrollDistance
|
||||
case 5:
|
||||
sum.y += scrollDistance
|
||||
case 6:
|
||||
sum.x -= scrollDistance
|
||||
case 7:
|
||||
sum.x += scrollDistance
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (window *Window) handleExpose (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ExposeEvent,
|
||||
) {
|
||||
_ = window.compressExpose(*event.ExposeEvent)
|
||||
window.redrawChildEntirely()
|
||||
}
|
||||
|
||||
func (window *Window) handleConfigureNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ConfigureNotifyEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
configureEvent := *event.ConfigureNotifyEvent
|
||||
|
||||
newWidth := int(configureEvent.Width)
|
||||
newHeight := int(configureEvent.Height)
|
||||
sizeChanged :=
|
||||
window.metrics.width != newWidth ||
|
||||
window.metrics.height != newHeight
|
||||
window.metrics.width = newWidth
|
||||
window.metrics.height = newHeight
|
||||
|
||||
if sizeChanged {
|
||||
configureEvent = window.compressConfigureNotify(configureEvent)
|
||||
window.metrics.width = int(configureEvent.Width)
|
||||
window.metrics.height = int(configureEvent.Height)
|
||||
window.reallocateCanvas()
|
||||
window.resizeChildToFit()
|
||||
|
||||
if !window.exposeEventFollows(configureEvent) {
|
||||
window.redrawChildEntirely()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (found bool) {
|
||||
nextEvents := xevent.Peek(window.backend.connection)
|
||||
if len(nextEvents) > 0 {
|
||||
untypedEvent := nextEvents[0]
|
||||
if untypedEvent.Err == nil {
|
||||
typedEvent, ok :=
|
||||
untypedEvent.Event.(xproto.ConfigureNotifyEvent)
|
||||
|
||||
if ok && typedEvent.Window == event.Window {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (window *Window) modifiersFromState (
|
||||
state uint16,
|
||||
) (
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
return input.Modifiers {
|
||||
Shift:
|
||||
(state & xproto.ModMaskShift) > 0 ||
|
||||
(state & window.backend.modifierMasks.shiftLock) > 0,
|
||||
Control: (state & xproto.ModMaskControl) > 0,
|
||||
Alt: (state & window.backend.modifierMasks.alt) > 0,
|
||||
Meta: (state & window.backend.modifierMasks.meta) > 0,
|
||||
Super: (state & window.backend.modifierMasks.super) > 0,
|
||||
Hyper: (state & window.backend.modifierMasks.hyper) > 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) handleKeyPress (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.KeyPressEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
keyEvent := *event.KeyPressEvent
|
||||
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
||||
modifiers := window.modifiersFromState(keyEvent.State)
|
||||
modifiers.NumberPad = numberPad
|
||||
|
||||
if key == input.KeyTab && modifiers.Alt {
|
||||
if child, ok := window.child.(elements.Focusable); ok {
|
||||
direction := input.KeynavDirectionForward
|
||||
if modifiers.Shift {
|
||||
direction = input.KeynavDirectionBackward
|
||||
}
|
||||
|
||||
if !child.HandleFocus(direction) {
|
||||
child.HandleUnfocus()
|
||||
}
|
||||
}
|
||||
} else if child, ok := window.child.(elements.KeyboardTarget); ok {
|
||||
child.HandleKeyDown(key, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) handleKeyRelease (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.KeyReleaseEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
keyEvent := *event.KeyReleaseEvent
|
||||
|
||||
// do not process this event if it was generated from a key repeat
|
||||
nextEvents := xevent.Peek(window.backend.connection)
|
||||
if len(nextEvents) > 0 {
|
||||
untypedEvent := nextEvents[0]
|
||||
if untypedEvent.Err == nil {
|
||||
typedEvent, ok :=
|
||||
untypedEvent.Event.(xproto.KeyPressEvent)
|
||||
|
||||
if ok && typedEvent.Detail == keyEvent.Detail &&
|
||||
typedEvent.Event == keyEvent.Event &&
|
||||
typedEvent.State == keyEvent.State {
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
||||
modifiers := window.modifiersFromState(keyEvent.State)
|
||||
modifiers.NumberPad = numberPad
|
||||
|
||||
if child, ok := window.child.(elements.KeyboardTarget); ok {
|
||||
child.HandleKeyUp(key, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) handleButtonPress (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ButtonPressEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
if child, ok := window.child.(elements.MouseTarget); ok {
|
||||
buttonEvent := *event.ButtonPressEvent
|
||||
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
|
||||
sum := scrollSum { }
|
||||
sum.add(buttonEvent.Detail, window, buttonEvent.State)
|
||||
window.compressScrollSum(buttonEvent, &sum)
|
||||
child.HandleMouseScroll (
|
||||
int(buttonEvent.EventX),
|
||||
int(buttonEvent.EventY),
|
||||
float64(sum.x), float64(sum.y))
|
||||
} else {
|
||||
child.HandleMouseDown (
|
||||
int(buttonEvent.EventX),
|
||||
int(buttonEvent.EventY),
|
||||
input.Button(buttonEvent.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (window *Window) handleButtonRelease (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ButtonReleaseEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
if child, ok := window.child.(elements.MouseTarget); ok {
|
||||
buttonEvent := *event.ButtonReleaseEvent
|
||||
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
|
||||
child.HandleMouseUp (
|
||||
int(buttonEvent.EventX),
|
||||
int(buttonEvent.EventY),
|
||||
input.Button(buttonEvent.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) handleMotionNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.MotionNotifyEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
if child, ok := window.child.(elements.MouseTarget); ok {
|
||||
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
|
||||
child.HandleMouseMove (
|
||||
int(motionEvent.EventX),
|
||||
int(motionEvent.EventY))
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) compressExpose (
|
||||
firstEvent xproto.ExposeEvent,
|
||||
) (
|
||||
lastEvent xproto.ExposeEvent,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.ExposeEvent)
|
||||
if !ok { continue }
|
||||
|
||||
// FIXME: union all areas into the last event
|
||||
if firstEvent.Window == typedEvent.Window {
|
||||
lastEvent = typedEvent
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) compressConfigureNotify (
|
||||
firstEvent xproto.ConfigureNotifyEvent,
|
||||
) (
|
||||
lastEvent xproto.ConfigureNotifyEvent,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.ConfigureNotifyEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Event == typedEvent.Event &&
|
||||
firstEvent.Window == typedEvent.Window {
|
||||
|
||||
lastEvent = typedEvent
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) compressScrollSum (
|
||||
firstEvent xproto.ButtonPressEvent,
|
||||
sum *scrollSum,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.ButtonPressEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Event == typedEvent.Event &&
|
||||
typedEvent.Detail >= 4 &&
|
||||
typedEvent.Detail <= 7 {
|
||||
|
||||
sum.add(typedEvent.Detail, window, typedEvent.State)
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) compressMotionNotify (
|
||||
firstEvent xproto.MotionNotifyEvent,
|
||||
) (
|
||||
lastEvent xproto.MotionNotifyEvent,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.MotionNotifyEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Event == typedEvent.Event {
|
||||
lastEvent = typedEvent
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
package x
|
||||
|
||||
import "image"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/ewmh"
|
||||
import "github.com/jezek/xgbutil/icccm"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
import "github.com/jezek/xgbutil/xwindow"
|
||||
import "github.com/jezek/xgbutil/xgraphics"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
|
||||
type Window struct {
|
||||
backend *Backend
|
||||
xWindow *xwindow.Window
|
||||
xCanvas *xgraphics.Image
|
||||
canvas canvas.BasicCanvas
|
||||
child elements.Element
|
||||
onClose func ()
|
||||
skipChildDrawCallback bool
|
||||
|
||||
theme theme.Theme
|
||||
config config.Config
|
||||
|
||||
metrics struct {
|
||||
width int
|
||||
height int
|
||||
}
|
||||
}
|
||||
|
||||
func (backend *Backend) NewWindow (
|
||||
width, height int,
|
||||
) (
|
||||
output elements.Window,
|
||||
err error,
|
||||
) {
|
||||
if backend == nil { panic("nil backend") }
|
||||
|
||||
window := &Window { backend: backend }
|
||||
|
||||
window.xWindow, err = xwindow.Generate(backend.connection)
|
||||
if err != nil { return }
|
||||
window.xWindow.Create (
|
||||
backend.connection.RootWin(),
|
||||
0, 0, width, height, 0)
|
||||
err = window.xWindow.Listen (
|
||||
xproto.EventMaskExposure,
|
||||
xproto.EventMaskStructureNotify,
|
||||
xproto.EventMaskPointerMotion,
|
||||
xproto.EventMaskKeyPress,
|
||||
xproto.EventMaskKeyRelease,
|
||||
xproto.EventMaskButtonPress,
|
||||
xproto.EventMaskButtonRelease)
|
||||
if err != nil { return }
|
||||
|
||||
window.xWindow.WMGracefulClose (func (xWindow *xwindow.Window) {
|
||||
window.Close()
|
||||
})
|
||||
|
||||
xevent.ExposeFun(window.handleExpose).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.ConfigureNotifyFun(window.handleConfigureNotify).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.KeyPressFun(window.handleKeyPress).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.KeyReleaseFun(window.handleKeyRelease).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.ButtonPressFun(window.handleButtonPress).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.ButtonReleaseFun(window.handleButtonRelease).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.MotionNotifyFun(window.handleMotionNotify).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
|
||||
window.SetTheme(backend.theme)
|
||||
window.SetConfig(backend.config)
|
||||
|
||||
window.metrics.width = width
|
||||
window.metrics.height = height
|
||||
window.childMinimumSizeChangeCallback(8, 8)
|
||||
|
||||
window.reallocateCanvas()
|
||||
|
||||
backend.windows[window.xWindow.Id] = window
|
||||
output = window
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) Adopt (child elements.Element) {
|
||||
// disown previous child
|
||||
if window.child != nil {
|
||||
window.child.OnDamage(nil)
|
||||
window.child.OnMinimumSizeChange(nil)
|
||||
}
|
||||
if previousChild, ok := window.child.(elements.Flexible); ok {
|
||||
previousChild.OnFlexibleHeightChange(nil)
|
||||
}
|
||||
if previousChild, ok := window.child.(elements.Focusable); ok {
|
||||
previousChild.OnFocusRequest(nil)
|
||||
previousChild.OnFocusMotionRequest(nil)
|
||||
if previousChild.Focused() {
|
||||
previousChild.HandleUnfocus()
|
||||
}
|
||||
}
|
||||
|
||||
// adopt new child
|
||||
window.child = child
|
||||
if newChild, ok := child.(elements.Themeable); ok {
|
||||
newChild.SetTheme(window.theme)
|
||||
}
|
||||
if newChild, ok := child.(elements.Configurable); ok {
|
||||
newChild.SetConfig(window.config)
|
||||
}
|
||||
if newChild, ok := child.(elements.Flexible); ok {
|
||||
newChild.OnFlexibleHeightChange(window.resizeChildToFit)
|
||||
}
|
||||
if newChild, ok := child.(elements.Focusable); ok {
|
||||
newChild.OnFocusRequest(window.childSelectionRequestCallback)
|
||||
}
|
||||
if child != nil {
|
||||
child.OnDamage(window.childDrawCallback)
|
||||
child.OnMinimumSizeChange (func () {
|
||||
window.childMinimumSizeChangeCallback (
|
||||
child.MinimumSize())
|
||||
})
|
||||
if !window.childMinimumSizeChangeCallback(child.MinimumSize()) {
|
||||
window.resizeChildToFit()
|
||||
window.redrawChildEntirely()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) Child () (child elements.Element) {
|
||||
child = window.child
|
||||
return
|
||||
}
|
||||
|
||||
func (window *Window) SetTitle (title string) {
|
||||
ewmh.WmNameSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
title)
|
||||
}
|
||||
|
||||
func (window *Window) SetIcon (sizes []image.Image) {
|
||||
wmIcons := []ewmh.WmIcon { }
|
||||
|
||||
for _, icon := range sizes {
|
||||
width := icon.Bounds().Max.X
|
||||
height := icon.Bounds().Max.Y
|
||||
wmIcon := ewmh.WmIcon {
|
||||
Width: uint(width),
|
||||
Height: uint(height),
|
||||
Data: make ([]uint, width * height),
|
||||
}
|
||||
|
||||
// manually convert image data beacuse of course we have to do
|
||||
// this
|
||||
index := 0
|
||||
for y := 0; y < height; y ++ {
|
||||
for x := 0; x < width; x ++ {
|
||||
r, g, b, a := icon.At(x, y).RGBA()
|
||||
r >>= 8
|
||||
g >>= 8
|
||||
b >>= 8
|
||||
a >>= 8
|
||||
wmIcon.Data[index] =
|
||||
(uint(a) << 24) |
|
||||
(uint(r) << 16) |
|
||||
(uint(g) << 8) |
|
||||
(uint(b) << 0)
|
||||
index ++
|
||||
}}
|
||||
|
||||
wmIcons = append(wmIcons, wmIcon)
|
||||
}
|
||||
|
||||
ewmh.WmIconSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
wmIcons)
|
||||
}
|
||||
|
||||
func (window *Window) Show () {
|
||||
if window.child == nil {
|
||||
window.xCanvas.For (func (x, y int) xgraphics.BGRA {
|
||||
return xgraphics.BGRA { }
|
||||
})
|
||||
|
||||
window.pushRegion(window.xCanvas.Bounds())
|
||||
}
|
||||
|
||||
window.xWindow.Map()
|
||||
}
|
||||
|
||||
func (window *Window) Hide () {
|
||||
window.xWindow.Unmap()
|
||||
}
|
||||
|
||||
func (window *Window) Close () {
|
||||
if window.onClose != nil { window.onClose() }
|
||||
delete(window.backend.windows, window.xWindow.Id)
|
||||
window.xWindow.Destroy()
|
||||
}
|
||||
|
||||
func (window *Window) OnClose (callback func ()) {
|
||||
window.onClose = callback
|
||||
}
|
||||
|
||||
func (window *Window) SetTheme (theme theme.Theme) {
|
||||
window.theme = theme
|
||||
if child, ok := window.child.(elements.Themeable); ok {
|
||||
child.SetTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) SetConfig (config config.Config) {
|
||||
window.config = config
|
||||
if child, ok := window.child.(elements.Configurable); ok {
|
||||
child.SetConfig(config)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) reallocateCanvas () {
|
||||
window.canvas.Reallocate(window.metrics.width, window.metrics.height)
|
||||
|
||||
previousWidth, previousHeight := 0, 0
|
||||
if window.xCanvas != nil {
|
||||
previousWidth = window.xCanvas.Bounds().Dx()
|
||||
previousHeight = window.xCanvas.Bounds().Dy()
|
||||
}
|
||||
|
||||
newWidth := window.metrics.width
|
||||
newHeight := window.metrics.height
|
||||
larger := newWidth > previousWidth || newHeight > previousHeight
|
||||
smaller := newWidth < previousWidth / 2 || newHeight < previousHeight / 2
|
||||
if larger || smaller {
|
||||
if window.xCanvas != nil {
|
||||
window.xCanvas.Destroy()
|
||||
}
|
||||
window.xCanvas = xgraphics.New (
|
||||
window.backend.connection,
|
||||
image.Rect (
|
||||
0, 0,
|
||||
(newWidth / 64) * 64 + 64,
|
||||
(newHeight / 64) * 64 + 64))
|
||||
window.xCanvas.CreatePixmap()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (window *Window) redrawChildEntirely () {
|
||||
window.pushRegion(window.paste(window.canvas))
|
||||
|
||||
}
|
||||
|
||||
func (window *Window) resizeChildToFit () {
|
||||
window.skipChildDrawCallback = true
|
||||
if child, ok := window.child.(elements.Flexible); ok {
|
||||
minimumHeight := child.FlexibleHeightFor(window.metrics.width)
|
||||
minimumWidth, _ := child.MinimumSize()
|
||||
|
||||
icccm.WmNormalHintsSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
&icccm.NormalHints {
|
||||
Flags: icccm.SizeHintPMinSize,
|
||||
MinWidth: uint(minimumWidth),
|
||||
MinHeight: uint(minimumHeight),
|
||||
})
|
||||
|
||||
if window.metrics.height >= minimumHeight &&
|
||||
window.metrics.width >= minimumWidth {
|
||||
window.child.DrawTo(window.canvas)
|
||||
}
|
||||
} else {
|
||||
window.child.DrawTo(window.canvas)
|
||||
}
|
||||
window.skipChildDrawCallback = false
|
||||
}
|
||||
|
||||
func (window *Window) childDrawCallback (region canvas.Canvas) {
|
||||
if window.skipChildDrawCallback { return }
|
||||
window.pushRegion(window.paste(region))
|
||||
}
|
||||
|
||||
func (window *Window) paste (canvas canvas.Canvas) (updatedRegion image.Rectangle) {
|
||||
data, stride := canvas.Buffer()
|
||||
bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds())
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
rgba := data[x + y * stride]
|
||||
index := x * 4 + y * window.xCanvas.Stride
|
||||
window.xCanvas.Pix[index + 0] = rgba.B
|
||||
window.xCanvas.Pix[index + 1] = rgba.G
|
||||
window.xCanvas.Pix[index + 2] = rgba.R
|
||||
window.xCanvas.Pix[index + 3] = rgba.A
|
||||
}}
|
||||
|
||||
return bounds
|
||||
}
|
||||
|
||||
func (window *Window) childMinimumSizeChangeCallback (width, height int) (resized bool) {
|
||||
icccm.WmNormalHintsSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
&icccm.NormalHints {
|
||||
Flags: icccm.SizeHintPMinSize,
|
||||
MinWidth: uint(width),
|
||||
MinHeight: uint(height),
|
||||
})
|
||||
newWidth := window.metrics.width
|
||||
newHeight := window.metrics.height
|
||||
if newWidth < width { newWidth = width }
|
||||
if newHeight < height { newHeight = height }
|
||||
if newWidth != window.metrics.width ||
|
||||
newHeight != window.metrics.height {
|
||||
window.xWindow.Resize(newWidth, newHeight)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (window *Window) childSelectionRequestCallback () (granted bool) {
|
||||
if _, ok := window.child.(elements.Focusable); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (window *Window) childSelectionMotionRequestCallback (
|
||||
direction input.KeynavDirection,
|
||||
) (
|
||||
granted bool,
|
||||
) {
|
||||
if child, ok := window.child.(elements.Focusable); ok {
|
||||
if !child.HandleFocus(direction) {
|
||||
child.HandleUnfocus()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (window *Window) pushRegion (region image.Rectangle) {
|
||||
if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") }
|
||||
image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image)
|
||||
if ok {
|
||||
image.XDraw()
|
||||
image.XExpPaint (
|
||||
window.xWindow.Id,
|
||||
image.Bounds().Min.X,
|
||||
image.Bounds().Min.Y)
|
||||
}
|
||||
}
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
package x
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
|
||||
import "github.com/jezek/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
|
||||
// Backend is an instance of an X backend.
|
||||
type Backend struct {
|
||||
connection *xgbutil.XUtil
|
||||
|
||||
doChannel chan(func ())
|
||||
|
||||
modifierMasks struct {
|
||||
capsLock uint16
|
||||
shiftLock uint16
|
||||
numLock uint16
|
||||
modeSwitch uint16
|
||||
|
||||
alt uint16
|
||||
meta uint16
|
||||
super uint16
|
||||
hyper uint16
|
||||
}
|
||||
|
||||
theme theme.Theme
|
||||
config config.Config
|
||||
|
||||
windows map[xproto.Window] *Window
|
||||
|
||||
open bool
|
||||
}
|
||||
|
||||
// NewBackend instantiates an X backend.
|
||||
func NewBackend () (output tomo.Backend, err error) {
|
||||
backend := &Backend {
|
||||
windows: map[xproto.Window] *Window { },
|
||||
doChannel: make(chan func (), 0),
|
||||
theme: theme.Default { },
|
||||
config: config.Default { },
|
||||
open: true,
|
||||
}
|
||||
|
||||
// connect to X
|
||||
backend.connection, err = xgbutil.NewConn()
|
||||
if err != nil { return }
|
||||
backend.initializeKeymapInformation()
|
||||
|
||||
output = backend
|
||||
return
|
||||
}
|
||||
|
||||
// Run runs the backend's event loop. This method will not exit until Stop() is
|
||||
// called, or the backend experiences a fatal error.
|
||||
func (backend *Backend) Run () (err error) {
|
||||
backend.assert()
|
||||
pingBefore,
|
||||
pingAfter,
|
||||
pingQuit := xevent.MainPing(backend.connection)
|
||||
for {
|
||||
select {
|
||||
case <- pingBefore:
|
||||
<- pingAfter
|
||||
case callback := <- backend.doChannel:
|
||||
callback()
|
||||
case <- pingQuit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully closes the connection and stops the event loop.
|
||||
func (backend *Backend) Stop () {
|
||||
backend.assert()
|
||||
if !backend.open { return }
|
||||
backend.open = false
|
||||
|
||||
toClose := []*Window { }
|
||||
for _, window := range backend.windows {
|
||||
toClose = append(toClose, window)
|
||||
}
|
||||
for _, window := range toClose {
|
||||
window.Close()
|
||||
}
|
||||
xevent.Quit(backend.connection)
|
||||
backend.connection.Conn().Close()
|
||||
}
|
||||
|
||||
// Do executes the specified callback within the main thread as soon as
|
||||
// possible. This function can be safely called from other threads.
|
||||
func (backend *Backend) Do (callback func ()) {
|
||||
backend.assert()
|
||||
backend.doChannel <- callback
|
||||
}
|
||||
|
||||
// Copy puts data into the clipboard. This method is not yet implemented and
|
||||
// will do nothing!
|
||||
func (backend *Backend) Copy (data data.Data) {
|
||||
backend.assert()
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Paste returns the data currently in the clipboard. This method may
|
||||
// return nil. This method is not yet implemented and will do nothing!
|
||||
func (backend *Backend) Paste (accept []data.Mime) (data data.Data) {
|
||||
backend.assert()
|
||||
// TODO
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// SetTheme sets the theme of all open windows.
|
||||
func (backend *Backend) SetTheme (theme theme.Theme) {
|
||||
backend.assert()
|
||||
backend.theme = theme
|
||||
for _, window := range backend.windows {
|
||||
window.SetTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfig sets the configuration of all open windows.
|
||||
func (backend *Backend) SetConfig (config config.Config) {
|
||||
backend.assert()
|
||||
backend.config = config
|
||||
for _, window := range backend.windows {
|
||||
window.SetConfig(config)
|
||||
}
|
||||
}
|
||||
|
||||
func (backend *Backend) assert () {
|
||||
if backend == nil { panic("nil backend") }
|
||||
}
|
||||
|
||||
func init () {
|
||||
tomo.RegisterBackend(NewBackend)
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,14 @@
|
||||
package tomo
|
||||
|
||||
import "time"
|
||||
|
||||
// Config can return global configuration parameters.
|
||||
type Config interface {
|
||||
// ScrollVelocity returns how many pixels should be scrolled every time
|
||||
// a scroll button is pressed.
|
||||
ScrollVelocity () int
|
||||
|
||||
// DoubleClickDelay returns the maximum delay between two clicks for
|
||||
// them to be registered as a double click.
|
||||
DoubleClickDelay () time.Duration
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package config
|
||||
|
||||
// Config can return global configuration parameters.
|
||||
type Config interface {
|
||||
// HandleWidth returns how large grab handles should typically be. This
|
||||
// is important for accessibility reasons.
|
||||
HandleWidth () int
|
||||
|
||||
// ScrollVelocity returns how many pixels should be scrolled every time
|
||||
// a scroll button is pressed.
|
||||
ScrollVelocity () int
|
||||
|
||||
// ThemePath returns the directory path to the theme.
|
||||
ThemePath () string
|
||||
}
|
||||
|
||||
// Default specifies default configuration values.
|
||||
type Default struct { }
|
||||
|
||||
|
||||
// HandleWidth returns the default handle width value.
|
||||
func (Default) HandleWidth () int {
|
||||
return 16
|
||||
}
|
||||
|
||||
// ScrollVelocity returns the default scroll velocity value.
|
||||
func (Default) ScrollVelocity () int {
|
||||
return 16
|
||||
}
|
||||
|
||||
// ThemePath returns the default theme path.
|
||||
func (Default) ThemePath () (string) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Wrapped wraps a configuration and uses Default if it is nil.
|
||||
type Wrapped struct {
|
||||
Config
|
||||
}
|
||||
|
||||
// HandleWidth returns how large grab handles should typically be. This
|
||||
// is important for accessibility reasons.
|
||||
func (wrapped Wrapped) HandleWidth () int {
|
||||
return wrapped.ensure().HandleWidth()
|
||||
}
|
||||
|
||||
// ScrollVelocity returns how many pixels should be scrolled every time
|
||||
// a scroll button is pressed.
|
||||
func (wrapped Wrapped) ScrollVelocity () int {
|
||||
return wrapped.ensure().ScrollVelocity()
|
||||
}
|
||||
|
||||
// ThemePath returns the directory path to the theme.
|
||||
func (wrapped Wrapped) ThemePath () string {
|
||||
return wrapped.ensure().ThemePath()
|
||||
}
|
||||
|
||||
func (wrapped Wrapped) ensure () (real Config) {
|
||||
real = wrapped.Config
|
||||
if real == nil { real = Default { } }
|
||||
return
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package config
|
||||
|
||||
import "io"
|
||||
|
||||
// Parse parses one or more configuration files and returns them as a Config.
|
||||
func Parse (sources ...io.Reader) (config Config) {
|
||||
// TODO
|
||||
return Default { }
|
||||
}
|
||||
+38
-1
@@ -1,10 +1,12 @@
|
||||
// Package data provides operations to deal with arbitrary data and MIME types.
|
||||
package data
|
||||
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
// Data represents arbitrary polymorphic data that can be used for data transfer
|
||||
// between applications.
|
||||
type Data map[Mime] io.ReadCloser
|
||||
type Data map[Mime] io.ReadSeekCloser
|
||||
|
||||
// Mime represents a MIME type.
|
||||
type Mime struct {
|
||||
@@ -15,6 +17,41 @@ type Mime struct {
|
||||
Type, Subtype string
|
||||
}
|
||||
|
||||
// M is shorthand for creating a MIME type.
|
||||
func M (ty, subtype string) Mime {
|
||||
return Mime { ty, subtype }
|
||||
}
|
||||
|
||||
// String returns the string representation of the MIME type.
|
||||
func (mime Mime) String () string {
|
||||
return mime.Type + "/" + mime.Subtype
|
||||
}
|
||||
|
||||
var MimePlain = Mime { "text", "plain" }
|
||||
|
||||
var MimeFile = Mime { "text", "uri-list" }
|
||||
|
||||
type byteReadCloser struct { *bytes.Reader }
|
||||
func (byteReadCloser) Close () error { return nil }
|
||||
|
||||
// Text returns plain text Data given a string.
|
||||
func Text (text string) Data {
|
||||
return Bytes(MimePlain, []byte(text))
|
||||
}
|
||||
|
||||
// Bytes constructs a Data given a buffer and a mime type.
|
||||
func Bytes (mime Mime, buffer []byte) Data {
|
||||
return Data {
|
||||
mime: byteReadCloser { bytes.NewReader(buffer) },
|
||||
}
|
||||
}
|
||||
|
||||
// Merge combines several Datas together. If multiple Datas provide a reader for
|
||||
// the same mime type, the ones further on in the list will take precedence.
|
||||
func Merge (individual ...Data) (combined Data) {
|
||||
for _, data := range individual {
|
||||
for mime, reader := range data {
|
||||
combined[mime] = reader
|
||||
}}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// Default specifies default configuration values.
|
||||
type Default struct { }
|
||||
|
||||
|
||||
// ScrollVelocity returns the default scroll velocity value.
|
||||
func (Default) ScrollVelocity () int {
|
||||
return 16
|
||||
}
|
||||
|
||||
// DoubleClickDelay returns the default double click delay.
|
||||
func (Default) DoubleClickDelay () time.Duration {
|
||||
return time.Second / 2
|
||||
}
|
||||
|
||||
// Wrapped wraps a configuration and uses Default if it is nil.
|
||||
type Wrapped struct {
|
||||
tomo.Config
|
||||
}
|
||||
|
||||
// ScrollVelocity returns how many pixels should be scrolled every time a scroll
|
||||
// button is pressed.
|
||||
func (wrapped Wrapped) ScrollVelocity () int {
|
||||
return wrapped.ensure().ScrollVelocity()
|
||||
}
|
||||
|
||||
// DoubleClickDelay returns the maximum delay between two clicks for them to be
|
||||
// registered as a double click.
|
||||
func (wrapped Wrapped) DoubleClickDelay () time.Duration {
|
||||
return wrapped.ensure().DoubleClickDelay()
|
||||
}
|
||||
|
||||
func (wrapped Wrapped) ensure () (real tomo.Config) {
|
||||
real = wrapped.Config
|
||||
if real == nil { real = Default { } }
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package config implements a default configuration.
|
||||
package config
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// import "io"
|
||||
|
||||
// Parse parses one or more configuration files and returns them as a Config.
|
||||
// func Parse (sources ...io.Reader) (config tomo.Config) {
|
||||
// // TODO
|
||||
// return Default { }
|
||||
// }
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 350 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,250 @@
|
||||
package theme
|
||||
|
||||
import "image"
|
||||
import "bytes"
|
||||
import _ "embed"
|
||||
import _ "image/png"
|
||||
import "image/color"
|
||||
import "golang.org/x/image/font"
|
||||
import "golang.org/x/image/font/basicfont"
|
||||
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 "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
|
||||
|
||||
//go:embed assets/default.png
|
||||
var defaultAtlasBytes []byte
|
||||
var defaultAtlas artist.Canvas
|
||||
var defaultTextures [7][7]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, 8, 8).Add(image.Pt(col, row).Mul(8))
|
||||
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)
|
||||
|
||||
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 (
|
||||
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 ++
|
||||
}}
|
||||
}
|
||||
|
||||
// Default is the default theme.
|
||||
type Default struct { }
|
||||
|
||||
// FontFace returns the default font face.
|
||||
func (Default) FontFace (style tomo.FontStyle, size tomo.FontSize, c tomo.Case) font.Face {
|
||||
return basicfont.Face7x13
|
||||
}
|
||||
|
||||
// Icon returns an icon from the default set corresponding to the given name.
|
||||
func (Default) 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MimeIcon returns an icon from the default set corresponding to the given mime.
|
||||
// type.
|
||||
func (Default) MimeIcon (data.Mime, tomo.IconSize, tomo.Case) artist.Icon {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pattern returns a pattern from the default theme corresponding to the given
|
||||
// pattern ID.
|
||||
func (Default) 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: 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 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: 0x656565FF,
|
||||
tomo.ColorBackground: 0xAAAAAAFF,
|
||||
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.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 {
|
||||
return image.Pt(6, 6)
|
||||
}
|
||||
|
||||
// Hints returns rendering optimization hints for a particular pattern.
|
||||
// These are optional, but following them may result in improved
|
||||
// performance.
|
||||
func (Default) Hints (pattern tomo.Pattern, c tomo.Case) (hints tomo.Hints) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sink returns the default sink vector for the given pattern.
|
||||
func (Default) Sink (pattern tomo.Pattern, c tomo.Case) image.Point {
|
||||
return image.Point { 1, 1 }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package theme implements a default theme.
|
||||
package theme
|
||||
@@ -1,47 +0,0 @@
|
||||
package defaultfont
|
||||
|
||||
import "golang.org/x/image/font/basicfont"
|
||||
|
||||
var FaceRegular = basicfont.Face7x13
|
||||
|
||||
// FIXME: make bold, italic, and bold italic masks by processing the Face7x13
|
||||
// mask.
|
||||
|
||||
var FaceBold = &basicfont.Face {
|
||||
Advance: 7,
|
||||
Width: 6,
|
||||
Height: 13,
|
||||
Ascent: 11,
|
||||
Descent: 2,
|
||||
Mask: FaceRegular.Mask, // TODO
|
||||
Ranges: []basicfont.Range {
|
||||
{ '\u0020', '\u007f', 0 },
|
||||
{ '\ufffd', '\ufffe', 95 },
|
||||
},
|
||||
}
|
||||
|
||||
var FaceItalic = &basicfont.Face {
|
||||
Advance: 7,
|
||||
Width: 6,
|
||||
Height: 13,
|
||||
Ascent: 11,
|
||||
Descent: 2,
|
||||
Mask: FaceRegular.Mask, // TODO
|
||||
Ranges: []basicfont.Range {
|
||||
{ '\u0020', '\u007f', 0 },
|
||||
{ '\ufffd', '\ufffe', 95 },
|
||||
},
|
||||
}
|
||||
|
||||
var FaceBoldItalic = &basicfont.Face {
|
||||
Advance: 7,
|
||||
Width: 6,
|
||||
Height: 13,
|
||||
Ascent: 11,
|
||||
Descent: 2,
|
||||
Mask: FaceRegular.Mask, // TODO
|
||||
Ranges: []basicfont.Range {
|
||||
{ '\u0020', '\u007f', 0 },
|
||||
{ '\ufffd', '\ufffe', 95 },
|
||||
},
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package dirs provides access to standard system and user directories.
|
||||
package dirs
|
||||
|
||||
import "os"
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package tomo
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
// 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 (artist.Canvas)
|
||||
|
||||
// Entity returns this element's entity.
|
||||
Entity () Entity
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "image"
|
||||
// import "runtime/debug"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// Button is a clickable button.
|
||||
type Button struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
drawer textdraw.Drawer
|
||||
|
||||
pressed bool
|
||||
text string
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onClick func ()
|
||||
}
|
||||
|
||||
// NewButton creates a new button with the specified label text.
|
||||
func NewButton (text string) (element *Button) {
|
||||
element = &Button { }
|
||||
element.theme.Case = theme.C("basic", "button")
|
||||
element.Core, element.core = core.NewCore(element.drawAll)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore (func () {
|
||||
element.drawAndPush(true)
|
||||
})
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseDown (x, y int, button input.Button) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.Focused() { element.Focus() }
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
element.drawAndPush(true)
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseUp (x, y int, button input.Button) {
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
within := image.Point { x, y }.
|
||||
In(element.Bounds())
|
||||
if element.Enabled() && within && element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
element.drawAndPush(true)
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseMove (x, y int) { }
|
||||
func (element *Button) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.drawAndPush(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.drawAndPush(true)
|
||||
if !element.Enabled() { return }
|
||||
if element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnClick sets the function to be called when the button is clicked.
|
||||
func (element *Button) OnClick (callback func ()) {
|
||||
element.onClick = callback
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this button can be clicked or not.
|
||||
func (element *Button) SetEnabled (enabled bool) {
|
||||
element.focusableControl.SetEnabled(enabled)
|
||||
}
|
||||
|
||||
// SetText sets the button's label text.
|
||||
func (element *Button) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.drawAndPush(false)
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *Button) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
element.drawer.SetFace (element.theme.FontFace (
|
||||
theme.FontStyleRegular,
|
||||
theme.FontSizeNormal))
|
||||
element.updateMinimumSize()
|
||||
element.drawAndPush(false)
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *Button) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.updateMinimumSize()
|
||||
element.drawAndPush(false)
|
||||
}
|
||||
|
||||
func (element *Button) updateMinimumSize () {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
padding := element.theme.Padding(theme.PatternButton)
|
||||
minimumSize := padding.Inverse().Apply(textBounds)
|
||||
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||
}
|
||||
|
||||
func (element *Button) drawAndPush (partial bool) {
|
||||
if element.core.HasImage () {
|
||||
if partial {
|
||||
element.core.DamageRegion (append (
|
||||
element.drawBackground(true),
|
||||
element.drawText(true))...)
|
||||
} else {
|
||||
element.drawAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) state () theme.State {
|
||||
return theme.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
Pressed: element.pressed,
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) drawBackground (partial bool) []image.Rectangle {
|
||||
state := element.state()
|
||||
bounds := element.Bounds()
|
||||
pattern := element.theme.Pattern(theme.PatternButton, state)
|
||||
static := element.theme.Hints(theme.PatternButton).StaticInset
|
||||
|
||||
if partial && static != (artist.Inset { }) {
|
||||
tiles := shatter.Shatter(bounds, static.Apply(bounds))
|
||||
artist.Draw(element.core, pattern, tiles...)
|
||||
return tiles
|
||||
} else {
|
||||
pattern.Draw(element.core, bounds)
|
||||
return []image.Rectangle { bounds }
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) drawText (partial bool) image.Rectangle {
|
||||
state := element.state()
|
||||
bounds := element.Bounds()
|
||||
foreground := element.theme.Color(theme.ColorForeground, state)
|
||||
sink := element.theme.Sink(theme.PatternButton)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset := image.Point {
|
||||
X: bounds.Min.X + (bounds.Dx() - textBounds.Dx()) / 2,
|
||||
Y: bounds.Min.Y + (bounds.Dy() - textBounds.Dy()) / 2,
|
||||
}
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
region := textBounds.Union(textBounds.Add(sink)).Add(offset)
|
||||
|
||||
if element.pressed {
|
||||
offset = offset.Add(sink)
|
||||
}
|
||||
|
||||
if partial {
|
||||
pattern := element.theme.Pattern(theme.PatternButton, state)
|
||||
pattern.Draw(element.core, region)
|
||||
}
|
||||
|
||||
element.drawer.Draw(element.core, foreground, offset)
|
||||
return region
|
||||
}
|
||||
|
||||
func (element *Button) drawAll () {
|
||||
element.drawBackground(false)
|
||||
element.drawText(false)
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// Checkbox is a toggle-able checkbox with a label.
|
||||
type Checkbox struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
drawer textdraw.Drawer
|
||||
|
||||
pressed bool
|
||||
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 }
|
||||
element.theme.Case = theme.C("basic", "checkbox")
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore(element.redo)
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
element.pressed = true
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) {
|
||||
if button != input.ButtonLeft || !element.pressed { return }
|
||||
|
||||
element.pressed = false
|
||||
within := image.Point { x, y }.
|
||||
In(element.Bounds())
|
||||
if within {
|
||||
element.checked = !element.checked
|
||||
}
|
||||
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if within && element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseMove (x, y int) { }
|
||||
func (element *Checkbox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.checked = !element.checked
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnToggle sets the function to be called when the checkbox is toggled.
|
||||
func (element *Checkbox) OnToggle (callback func ()) {
|
||||
element.onToggle = callback
|
||||
}
|
||||
|
||||
// Value reports whether or not the checkbox is currently checked.
|
||||
func (element *Checkbox) Value () (checked bool) {
|
||||
return element.checked
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this checkbox can be toggled or not.
|
||||
func (element *Checkbox) SetEnabled (enabled bool) {
|
||||
element.focusableControl.SetEnabled(enabled)
|
||||
}
|
||||
|
||||
// SetText sets the checkbox's label text.
|
||||
func (element *Checkbox) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *Checkbox) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
element.drawer.SetFace (element.theme.FontFace (
|
||||
theme.FontStyleRegular,
|
||||
theme.FontSizeNormal))
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *Checkbox) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *Checkbox) updateMinimumSize () {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
if element.text == "" {
|
||||
element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
|
||||
} else {
|
||||
margin := element.theme.Margin(theme.PatternBackground)
|
||||
element.core.SetMinimumSize (
|
||||
textBounds.Dy() + margin.X + textBounds.Dx(),
|
||||
textBounds.Dy())
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) redo () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) draw () {
|
||||
bounds := element.Bounds()
|
||||
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
||||
|
||||
state := theme.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
Pressed: element.pressed,
|
||||
On: element.checked,
|
||||
}
|
||||
|
||||
backgroundPattern := element.theme.Pattern (
|
||||
theme.PatternBackground, state)
|
||||
backgroundPattern.Draw(element.core, bounds)
|
||||
|
||||
pattern := element.theme.Pattern(theme.PatternButton, state)
|
||||
artist.DrawBounds(element.core, pattern, boxBounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
margin := element.theme.Margin(theme.PatternBackground)
|
||||
offset := bounds.Min.Add(image.Point {
|
||||
X: bounds.Dy() + margin.X,
|
||||
})
|
||||
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
foreground := element.theme.Color(theme.ColorForeground, state)
|
||||
element.drawer.Draw(element.core, foreground, offset)
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// Container is an element capable of containg other elements, and arranging
|
||||
// them in a layout.
|
||||
type Container struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
|
||||
layout layouts.Layout
|
||||
children []layouts.LayoutEntry
|
||||
drags [10]elements.MouseTarget
|
||||
warping bool
|
||||
focused bool
|
||||
focusable bool
|
||||
flexible bool
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onFocusRequest func () (granted bool)
|
||||
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
|
||||
onFlexibleHeightChange func ()
|
||||
}
|
||||
|
||||
// NewContainer creates a new container.
|
||||
func NewContainer (layout layouts.Layout) (element *Container) {
|
||||
element = &Container { }
|
||||
element.theme.Case = theme.C("basic", "container")
|
||||
element.Core, element.core = core.NewCore(element.redoAll)
|
||||
element.SetLayout(layout)
|
||||
return
|
||||
}
|
||||
|
||||
// SetLayout sets the layout of this container.
|
||||
func (element *Container) SetLayout (layout layouts.Layout) {
|
||||
element.layout = layout
|
||||
if element.core.HasImage() {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Adopt adds a new child element to the container. If expand is set to true,
|
||||
// the element will expand (instead of contract to its minimum size), in
|
||||
// whatever way is defined by the current layout.
|
||||
func (element *Container) Adopt (child elements.Element, expand bool) {
|
||||
// set event handlers
|
||||
if child0, ok := child.(elements.Themeable); ok {
|
||||
child0.SetTheme(element.theme.Theme)
|
||||
}
|
||||
if child0, ok := child.(elements.Configurable); ok {
|
||||
child0.SetConfig(element.config.Config)
|
||||
}
|
||||
child.OnDamage (func (region canvas.Canvas) {
|
||||
element.core.DamageRegion(region.Bounds())
|
||||
})
|
||||
child.OnMinimumSizeChange (func () {
|
||||
// TODO: this could probably stand to be more efficient. I mean
|
||||
// seriously?
|
||||
element.updateMinimumSize()
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
})
|
||||
if child0, ok := child.(elements.Flexible); ok {
|
||||
child0.OnFlexibleHeightChange(element.updateMinimumSize)
|
||||
}
|
||||
if child0, ok := child.(elements.Focusable); ok {
|
||||
child0.OnFocusRequest (func () (granted bool) {
|
||||
return element.childFocusRequestCallback(child0)
|
||||
})
|
||||
child0.OnFocusMotionRequest (
|
||||
func (direction input.KeynavDirection) (granted bool) {
|
||||
if element.onFocusMotionRequest == nil { return }
|
||||
return element.onFocusMotionRequest(direction)
|
||||
})
|
||||
}
|
||||
|
||||
// add child
|
||||
element.children = append (element.children, layouts.LayoutEntry {
|
||||
Element: child,
|
||||
Expand: expand,
|
||||
})
|
||||
|
||||
// refresh stale data
|
||||
element.updateMinimumSize()
|
||||
element.reflectChildProperties()
|
||||
if element.core.HasImage() && !element.warping {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Warp runs the specified callback, deferring all layout and rendering updates
|
||||
// until the callback has finished executing. This allows for aplications to
|
||||
// perform batch gui updates without flickering and stuff.
|
||||
func (element *Container) Warp (callback func ()) {
|
||||
if element.warping {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
element.warping = true
|
||||
callback()
|
||||
element.warping = false
|
||||
|
||||
// TODO: create some sort of task list so we don't do a full recalculate
|
||||
// and redraw every time, because although that is the most likely use
|
||||
// case, it is not the only one.
|
||||
if element.core.HasImage() {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Disown removes the given child from the container if it is contained within
|
||||
// it.
|
||||
func (element *Container) Disown (child elements.Element) {
|
||||
for index, entry := range element.children {
|
||||
if entry.Element == child {
|
||||
element.clearChildEventHandlers(entry.Element)
|
||||
element.children = append (
|
||||
element.children[:index],
|
||||
element.children[index + 1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.reflectChildProperties()
|
||||
if element.core.HasImage() && !element.warping {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) clearChildEventHandlers (child elements.Element) {
|
||||
child.DrawTo(nil)
|
||||
child.OnDamage(nil)
|
||||
child.OnMinimumSizeChange(nil)
|
||||
if child0, ok := child.(elements.Focusable); ok {
|
||||
child0.OnFocusRequest(nil)
|
||||
child0.OnFocusMotionRequest(nil)
|
||||
if child0.Focused() {
|
||||
child0.HandleUnfocus()
|
||||
}
|
||||
}
|
||||
if child0, ok := child.(elements.Flexible); ok {
|
||||
child0.OnFlexibleHeightChange(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// DisownAll removes all child elements from the container at once.
|
||||
func (element *Container) DisownAll () {
|
||||
element.children = nil
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.reflectChildProperties()
|
||||
if element.core.HasImage() && !element.warping {
|
||||
element.redoAll()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Children returns a slice containing this element's children.
|
||||
func (element *Container) Children () (children []elements.Element) {
|
||||
children = make([]elements.Element, len(element.children))
|
||||
for index, entry := range element.children {
|
||||
children[index] = entry.Element
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CountChildren returns the amount of children contained within this element.
|
||||
func (element *Container) CountChildren () (count int) {
|
||||
return len(element.children)
|
||||
}
|
||||
|
||||
// Child returns the child at the specified index. If the index is out of
|
||||
// bounds, this method will return nil.
|
||||
func (element *Container) Child (index int) (child elements.Element) {
|
||||
if index < 0 || index > len(element.children) { return }
|
||||
return element.children[index].Element
|
||||
}
|
||||
|
||||
// ChildAt returns the child that contains the specified x and y coordinates. If
|
||||
// there are no children at the coordinates, this method will return nil.
|
||||
func (element *Container) ChildAt (point image.Point) (child elements.Element) {
|
||||
for _, entry := range element.children {
|
||||
if point.In(entry.Bounds) {
|
||||
child = entry.Element
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Container) childPosition (child elements.Element) (position image.Point) {
|
||||
for _, entry := range element.children {
|
||||
if entry.Element == child {
|
||||
position = entry.Bounds.Min
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Container) redoAll () {
|
||||
if !element.core.HasImage() { return }
|
||||
// do a layout
|
||||
element.doLayout()
|
||||
|
||||
// draw a background
|
||||
rocks := make([]image.Rectangle, len(element.children))
|
||||
for index, entry := range element.children {
|
||||
rocks[index] = entry.Bounds
|
||||
}
|
||||
pattern := element.theme.Pattern (
|
||||
theme.PatternBackground,
|
||||
theme.State { })
|
||||
artist.DrawShatter (
|
||||
element.core, pattern, rocks...)
|
||||
|
||||
// cut our canvas up and give peices to child elements
|
||||
for _, entry := range element.children {
|
||||
entry.DrawTo(canvas.Cut(element.core, entry.Bounds))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *Container) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
for _, child := range element.children {
|
||||
if child0, ok := child.Element.(elements.Themeable); ok {
|
||||
child0.SetTheme(element.theme.Theme)
|
||||
}
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.redoAll()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *Container) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
for _, child := range element.children {
|
||||
if child0, ok := child.Element.(elements.Configurable); ok {
|
||||
child0.SetConfig(element.config)
|
||||
}
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.redoAll()
|
||||
}
|
||||
|
||||
func (element *Container) HandleMouseDown (x, y int, button input.Button) {
|
||||
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget)
|
||||
if !handlesMouse { return }
|
||||
element.drags[button] = child
|
||||
child.HandleMouseDown(x, y, button)
|
||||
}
|
||||
|
||||
func (element *Container) HandleMouseUp (x, y int, button input.Button) {
|
||||
child := element.drags[button]
|
||||
if child == nil { return }
|
||||
element.drags[button] = nil
|
||||
child.HandleMouseUp(x, y, button)
|
||||
}
|
||||
|
||||
func (element *Container) HandleMouseMove (x, y int) {
|
||||
for _, child := range element.drags {
|
||||
if child == nil { continue }
|
||||
child.HandleMouseMove(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) HandleMouseScroll (x, y int, deltaX, deltaY float64) {
|
||||
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget)
|
||||
if !handlesMouse { return }
|
||||
child.HandleMouseScroll(x, y, deltaX, deltaY)
|
||||
}
|
||||
|
||||
func (element *Container) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
element.forFocused (func (child elements.Focusable) bool {
|
||||
child0, handlesKeyboard := child.(elements.KeyboardTarget)
|
||||
if handlesKeyboard {
|
||||
child0.HandleKeyDown(key, modifiers)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (element *Container) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
element.forFocused (func (child elements.Focusable) bool {
|
||||
child0, handlesKeyboard := child.(elements.KeyboardTarget)
|
||||
if handlesKeyboard {
|
||||
child0.HandleKeyUp(key, modifiers)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (element *Container) FlexibleHeightFor (width int) (height int) {
|
||||
margin := element.theme.Margin(theme.PatternBackground)
|
||||
// TODO: have layouts take in x and y margins
|
||||
return element.layout.FlexibleHeightFor (
|
||||
element.children,
|
||||
margin.X, width)
|
||||
}
|
||||
|
||||
func (element *Container) OnFlexibleHeightChange (callback func ()) {
|
||||
element.onFlexibleHeightChange = callback
|
||||
}
|
||||
|
||||
func (element *Container) Focused () (focused bool) {
|
||||
return element.focused
|
||||
}
|
||||
|
||||
func (element *Container) Focus () {
|
||||
if element.onFocusRequest != nil {
|
||||
element.onFocusRequest()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) HandleFocus (direction input.KeynavDirection) (ok bool) {
|
||||
if !element.focusable { return false }
|
||||
direction = direction.Canon()
|
||||
|
||||
firstFocused := element.firstFocused()
|
||||
if firstFocused < 0 {
|
||||
// no element is currently focused, so we need to focus either
|
||||
// the first or last focusable element depending on the
|
||||
// direction.
|
||||
switch direction {
|
||||
case input.KeynavDirectionNeutral, input.KeynavDirectionForward:
|
||||
// if we recieve a neutral or forward direction, focus
|
||||
// the first focusable element.
|
||||
return element.focusFirstFocusableElement(direction)
|
||||
|
||||
case input.KeynavDirectionBackward:
|
||||
// if we recieve a backward direction, focus the last
|
||||
// focusable element.
|
||||
return element.focusLastFocusableElement(direction)
|
||||
}
|
||||
} else {
|
||||
// an element is currently focused, so we need to move the
|
||||
// focus in the specified direction
|
||||
firstFocusedChild :=
|
||||
element.children[firstFocused].Element.(elements.Focusable)
|
||||
|
||||
// before we move the focus, the currently focused child
|
||||
// may also be able to move its focus. if the child is able
|
||||
// to do that, we will let it and not move ours.
|
||||
if firstFocusedChild.HandleFocus(direction) {
|
||||
return true
|
||||
}
|
||||
|
||||
// find the previous/next focusable element relative to the
|
||||
// currently focused element, if it exists.
|
||||
for index := firstFocused + int(direction);
|
||||
index < len(element.children) && index >= 0;
|
||||
index += int(direction) {
|
||||
|
||||
child, focusable :=
|
||||
element.children[index].
|
||||
Element.(elements.Focusable)
|
||||
if focusable && child.HandleFocus(direction) {
|
||||
// we have found one, so we now actually move
|
||||
// the focus.
|
||||
firstFocusedChild.HandleUnfocus()
|
||||
element.focused = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (element *Container) focusFirstFocusableElement (
|
||||
direction input.KeynavDirection,
|
||||
) (
|
||||
ok bool,
|
||||
) {
|
||||
element.forFocusable (func (child elements.Focusable) bool {
|
||||
if child.HandleFocus(direction) {
|
||||
element.focused = true
|
||||
ok = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Container) focusLastFocusableElement (
|
||||
direction input.KeynavDirection,
|
||||
) (
|
||||
ok bool,
|
||||
) {
|
||||
element.forFocusableBackward (func (child elements.Focusable) bool {
|
||||
if child.HandleFocus(direction) {
|
||||
element.focused = true
|
||||
ok = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Container) HandleUnfocus () {
|
||||
element.focused = false
|
||||
element.forFocused (func (child elements.Focusable) bool {
|
||||
child.HandleUnfocus()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (element *Container) OnFocusRequest (callback func () (granted bool)) {
|
||||
element.onFocusRequest = callback
|
||||
}
|
||||
|
||||
func (element *Container) OnFocusMotionRequest (
|
||||
callback func (direction input.KeynavDirection) (granted bool),
|
||||
) {
|
||||
element.onFocusMotionRequest = callback
|
||||
}
|
||||
|
||||
func (element *Container) forFocused (callback func (child elements.Focusable) bool) {
|
||||
for _, entry := range element.children {
|
||||
child, focusable := entry.Element.(elements.Focusable)
|
||||
if focusable && child.Focused() {
|
||||
if !callback(child) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) forFocusable (callback func (child elements.Focusable) bool) {
|
||||
for _, entry := range element.children {
|
||||
child, focusable := entry.Element.(elements.Focusable)
|
||||
if focusable {
|
||||
if !callback(child) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) forFlexible (callback func (child elements.Flexible) bool) {
|
||||
for _, entry := range element.children {
|
||||
child, flexible := entry.Element.(elements.Flexible)
|
||||
if flexible {
|
||||
if !callback(child) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) forFocusableBackward (callback func (child elements.Focusable) bool) {
|
||||
for index := len(element.children) - 1; index >= 0; index -- {
|
||||
child, focusable := element.children[index].Element.(elements.Focusable)
|
||||
if focusable {
|
||||
if !callback(child) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) firstFocused () (index int) {
|
||||
for currentIndex, entry := range element.children {
|
||||
child, focusable := entry.Element.(elements.Focusable)
|
||||
if focusable && child.Focused() {
|
||||
return currentIndex
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (element *Container) reflectChildProperties () {
|
||||
element.focusable = false
|
||||
element.forFocusable (func (elements.Focusable) bool {
|
||||
element.focusable = true
|
||||
return false
|
||||
})
|
||||
element.flexible = false
|
||||
element.forFlexible (func (elements.Flexible) bool {
|
||||
element.flexible = true
|
||||
return false
|
||||
})
|
||||
if !element.focusable {
|
||||
element.focused = false
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) childFocusRequestCallback (
|
||||
child elements.Focusable,
|
||||
) (
|
||||
granted bool,
|
||||
) {
|
||||
if element.onFocusRequest != nil && element.onFocusRequest() {
|
||||
element.focused = true
|
||||
element.forFocused (func (child elements.Focusable) bool {
|
||||
child.HandleUnfocus()
|
||||
return true
|
||||
})
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Container) updateMinimumSize () {
|
||||
margin := element.theme.Margin(theme.PatternBackground)
|
||||
// TODO: have layouts take in x and y margins
|
||||
width, height := element.layout.MinimumSize(element.children, margin.X)
|
||||
if element.flexible {
|
||||
height = element.layout.FlexibleHeightFor (
|
||||
element.children,
|
||||
margin.X, width)
|
||||
}
|
||||
element.core.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *Container) doLayout () {
|
||||
margin := element.theme.Margin(theme.PatternBackground)
|
||||
// TODO: have layouts take in x and y margins
|
||||
element.layout.Arrange (
|
||||
element.children, margin.X, element.Bounds())
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package basicElements provides standard elements that are commonly used in
|
||||
// GUI applications.
|
||||
package basicElements
|
||||
@@ -1,25 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
|
||||
|
||||
type Image struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
buffer canvas.Canvas
|
||||
}
|
||||
|
||||
func NewImage (image image.Image) (element *Image) {
|
||||
element = &Image { buffer: canvas.FromImage(image) }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
bounds := image.Bounds()
|
||||
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Image) draw () {
|
||||
(patterns.Texture { Canvas: element.buffer }).
|
||||
Draw(element.core, element.Bounds())
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// Label is a simple text box.
|
||||
type Label struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
|
||||
wrap bool
|
||||
text string
|
||||
drawer textdraw.Drawer
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onFlexibleHeightChange func ()
|
||||
}
|
||||
|
||||
// NewLabel creates a new label. If wrap is set to true, the text inside will be
|
||||
// wrapped.
|
||||
func NewLabel (text string, wrap bool) (element *Label) {
|
||||
element = &Label { }
|
||||
element.theme.Case = theme.C("basic", "label")
|
||||
element.Core, element.core = core.NewCore(element.handleResize)
|
||||
element.SetWrap(wrap)
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Label) redo () {
|
||||
face := element.theme.FontFace (
|
||||
theme.FontStyleRegular,
|
||||
theme.FontSizeNormal)
|
||||
element.drawer.SetFace(face)
|
||||
element.updateMinimumSize()
|
||||
bounds := element.Bounds()
|
||||
if element.wrap {
|
||||
element.drawer.SetMaxWidth(bounds.Dx())
|
||||
element.drawer.SetMaxHeight(bounds.Dy())
|
||||
}
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
|
||||
func (element *Label) handleResize () {
|
||||
bounds := element.Bounds()
|
||||
if element.wrap {
|
||||
element.drawer.SetMaxWidth(bounds.Dx())
|
||||
element.drawer.SetMaxHeight(bounds.Dy())
|
||||
}
|
||||
element.draw()
|
||||
return
|
||||
}
|
||||
|
||||
// FlexibleHeightFor returns the reccomended height for this element based on
|
||||
// the given width in order to allow the text to wrap properly.
|
||||
func (element *Label) FlexibleHeightFor (width int) (height int) {
|
||||
if element.wrap {
|
||||
return element.drawer.ReccomendedHeightFor(width)
|
||||
} else {
|
||||
_, height = element.MinimumSize()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// OnFlexibleHeightChange sets a function to be called when the parameters
|
||||
// affecting this element's flexible height are changed.
|
||||
func (element *Label) OnFlexibleHeightChange (callback func ()) {
|
||||
element.onFlexibleHeightChange = callback
|
||||
}
|
||||
|
||||
// SetText sets the label's text.
|
||||
func (element *Label) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// SetWrap sets wether or not the label's text wraps. If the text is set to
|
||||
// wrap, the element will have a minimum size of a single character and
|
||||
// automatically wrap its text. If the text is set to not wrap, the element will
|
||||
// have a minimum size that fits its text.
|
||||
func (element *Label) SetWrap (wrap bool) {
|
||||
if wrap == element.wrap { return }
|
||||
if !wrap {
|
||||
element.drawer.SetMaxWidth(0)
|
||||
element.drawer.SetMaxHeight(0)
|
||||
}
|
||||
element.wrap = wrap
|
||||
element.updateMinimumSize()
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *Label) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
element.drawer.SetFace (element.theme.FontFace (
|
||||
theme.FontStyleRegular,
|
||||
theme.FontSizeNormal))
|
||||
element.updateMinimumSize()
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *Label) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.updateMinimumSize()
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Label) updateMinimumSize () {
|
||||
if element.wrap {
|
||||
em := element.drawer.Em().Round()
|
||||
if em < 1 {
|
||||
em = element.theme.Padding(theme.PatternBackground)[0]
|
||||
}
|
||||
element.core.SetMinimumSize (
|
||||
em, element.drawer.LineHeight().Round())
|
||||
if element.onFlexibleHeightChange != nil {
|
||||
element.onFlexibleHeightChange()
|
||||
}
|
||||
} else {
|
||||
bounds := element.drawer.LayoutBounds()
|
||||
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Label) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
pattern := element.theme.Pattern (
|
||||
theme.PatternBackground,
|
||||
theme.State { })
|
||||
pattern.Draw(element.core, bounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
|
||||
foreground := element.theme.Color (
|
||||
theme.ColorForeground,
|
||||
theme.State { })
|
||||
element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min))
|
||||
}
|
||||
@@ -1,465 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "fmt"
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// List is an element that contains several objects that a user can select.
|
||||
type List struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
|
||||
pressed bool
|
||||
|
||||
contentHeight int
|
||||
forcedMinimumWidth int
|
||||
forcedMinimumHeight int
|
||||
|
||||
selectedEntry int
|
||||
scroll int
|
||||
entries []ListEntry
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onScrollBoundsChange func ()
|
||||
onNoEntrySelected func ()
|
||||
}
|
||||
|
||||
// NewList creates a new list element with the specified entries.
|
||||
func NewList (entries ...ListEntry) (element *List) {
|
||||
element = &List { selectedEntry: -1 }
|
||||
element.theme.Case = theme.C("basic", "list")
|
||||
element.Core, element.core = core.NewCore(element.handleResize)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore (func () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
})
|
||||
|
||||
element.entries = make([]ListEntry, len(entries))
|
||||
for index, entry := range entries {
|
||||
element.entries[index] = entry
|
||||
}
|
||||
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
func (element *List) handleResize () {
|
||||
for index, entry := range element.entries {
|
||||
element.entries[index] = element.resizeEntryToFit(entry)
|
||||
}
|
||||
|
||||
if element.scroll > element.maxScrollHeight() {
|
||||
element.scroll = element.maxScrollHeight()
|
||||
}
|
||||
element.draw()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *List) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
for index, entry := range element.entries {
|
||||
entry.SetTheme(element.theme.Theme)
|
||||
element.entries[index] = entry
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *List) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
for index, entry := range element.entries {
|
||||
entry.SetConfig(element.config)
|
||||
element.entries[index] = entry
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *List) redo () {
|
||||
for index, entry := range element.entries {
|
||||
element.entries[index] = element.resizeEntryToFit(entry)
|
||||
}
|
||||
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Collapse forces a minimum width and height upon the list. If a zero value is
|
||||
// given for a dimension, its minimum will be determined by the list's content.
|
||||
// If the list's height goes beyond the forced size, it will need to be accessed
|
||||
// via scrolling. If an entry's width goes beyond the forced size, its text will
|
||||
// be truncated so that it fits.
|
||||
func (element *List) Collapse (width, height int) {
|
||||
if
|
||||
element.forcedMinimumWidth == width &&
|
||||
element.forcedMinimumHeight == height {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
element.forcedMinimumWidth = width
|
||||
element.forcedMinimumHeight = height
|
||||
element.updateMinimumSize()
|
||||
|
||||
for index, entry := range element.entries {
|
||||
element.entries[index] = element.resizeEntryToFit(entry)
|
||||
}
|
||||
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseDown (x, y int, button input.Button) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.Focused() { element.Focus() }
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
if element.selectUnderMouse(x, y) && element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseUp (x, y int, button input.Button) {
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseMove (x, y int) {
|
||||
if element.pressed {
|
||||
if element.selectUnderMouse(x, y) && element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
|
||||
altered := false
|
||||
switch key {
|
||||
case input.KeyLeft, input.KeyUp:
|
||||
altered = element.changeSelectionBy(-1)
|
||||
|
||||
case input.KeyRight, input.KeyDown:
|
||||
altered = element.changeSelectionBy(1)
|
||||
|
||||
case input.KeyEscape:
|
||||
altered = element.selectEntry(-1)
|
||||
}
|
||||
|
||||
if altered && element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *List) ScrollContentBounds () (bounds image.Rectangle) {
|
||||
return image.Rect (
|
||||
0, 0,
|
||||
1, element.contentHeight)
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's viewport
|
||||
// relative to ScrollBounds.
|
||||
func (element *List) ScrollViewportBounds () (bounds image.Rectangle) {
|
||||
return image.Rect (
|
||||
0, element.scroll,
|
||||
0, element.scroll + element.scrollViewportHeight())
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *List) ScrollTo (position image.Point) {
|
||||
element.scroll = position.Y
|
||||
if element.scroll < 0 {
|
||||
element.scroll = 0
|
||||
} else if element.scroll > element.maxScrollHeight() {
|
||||
element.scroll = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *List) ScrollAxes () (horizontal, vertical bool) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (element *List) scrollViewportHeight () (height int) {
|
||||
padding := element.theme.Padding(theme.PatternSunken)
|
||||
return element.Bounds().Dy() - padding[0] - padding[2]
|
||||
}
|
||||
|
||||
func (element *List) maxScrollHeight () (height int) {
|
||||
height =
|
||||
element.contentHeight -
|
||||
element.scrollViewportHeight()
|
||||
if height < 0 { height = 0 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *List) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
// OnNoEntrySelected sets a function to be called when the user chooses to
|
||||
// deselect the current selected entry by clicking on empty space within the
|
||||
// list or by pressing the escape key.
|
||||
func (element *List) OnNoEntrySelected (callback func ()) {
|
||||
element.onNoEntrySelected = callback
|
||||
}
|
||||
|
||||
// CountEntries returns the amount of entries in the list.
|
||||
func (element *List) CountEntries () (count int) {
|
||||
return len(element.entries)
|
||||
}
|
||||
|
||||
// Append adds an entry to the end of the list.
|
||||
func (element *List) Append (entry ListEntry) {
|
||||
// append
|
||||
entry = element.resizeEntryToFit(entry)
|
||||
entry.SetTheme(element.theme.Theme)
|
||||
entry.SetConfig(element.config)
|
||||
element.entries = append(element.entries, entry)
|
||||
|
||||
// recalculate, redraw, notify
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// EntryAt returns the entry at the specified index. If the index is out of
|
||||
// bounds, it panics.
|
||||
func (element *List) EntryAt (index int) (entry ListEntry) {
|
||||
if index < 0 || index >= len(element.entries) {
|
||||
panic(fmt.Sprint("basic.List.EntryAt index out of range: ", index))
|
||||
}
|
||||
return element.entries[index]
|
||||
}
|
||||
|
||||
// Insert inserts an entry into the list at the speified index. If the index is
|
||||
// out of bounds, it is constrained either to zero or len(entries).
|
||||
func (element *List) Insert (index int, entry ListEntry) {
|
||||
if index < 0 { index = 0 }
|
||||
if index > len(element.entries) { index = len(element.entries) }
|
||||
|
||||
// insert
|
||||
element.entries = append (
|
||||
element.entries[:index + 1],
|
||||
element.entries[index:]...)
|
||||
entry = element.resizeEntryToFit(entry)
|
||||
element.entries[index] = entry
|
||||
|
||||
// recalculate, redraw, notify
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes the entry at the specified index. If the index is out of
|
||||
// bounds, it panics.
|
||||
func (element *List) Remove (index int) {
|
||||
if index < 0 || index >= len(element.entries) {
|
||||
panic(fmt.Sprint("basic.List.Remove index out of range: ", index))
|
||||
}
|
||||
|
||||
// delete
|
||||
element.entries = append (
|
||||
element.entries[:index],
|
||||
element.entries[index + 1:]...)
|
||||
|
||||
// recalculate, redraw, notify
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Replace replaces the entry at the specified index with another. If the index
|
||||
// is out of bounds, it panics.
|
||||
func (element *List) Replace (index int, entry ListEntry) {
|
||||
if index < 0 || index >= len(element.entries) {
|
||||
panic(fmt.Sprint("basic.List.Replace index out of range: ", index))
|
||||
}
|
||||
|
||||
// replace
|
||||
entry = element.resizeEntryToFit(entry)
|
||||
element.entries[index] = entry
|
||||
|
||||
// redraw
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Select selects a specific item in the list. If the index is out of bounds,
|
||||
// no items will be selecected.
|
||||
func (element *List) Select (index int) {
|
||||
if element.selectEntry(index) {
|
||||
element.redo()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) selectUnderMouse (x, y int) (updated bool) {
|
||||
padding := element.theme.Padding(theme.PatternSunken)
|
||||
bounds := padding.Apply(element.Bounds())
|
||||
mousePoint := image.Pt(x, y)
|
||||
dot := image.Pt (
|
||||
bounds.Min.X,
|
||||
bounds.Min.Y - element.scroll)
|
||||
|
||||
newlySelectedEntryIndex := -1
|
||||
for index, entry := range element.entries {
|
||||
entryPosition := dot
|
||||
dot.Y += entry.Bounds().Dy()
|
||||
if entryPosition.Y > bounds.Max.Y { break }
|
||||
if mousePoint.In(entry.Bounds().Add(entryPosition)) {
|
||||
newlySelectedEntryIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return element.selectEntry(newlySelectedEntryIndex)
|
||||
}
|
||||
|
||||
func (element *List) selectEntry (index int) (updated bool) {
|
||||
if element.selectedEntry == index { return false }
|
||||
element.selectedEntry = index
|
||||
if element.selectedEntry < 0 {
|
||||
if element.onNoEntrySelected != nil {
|
||||
element.onNoEntrySelected()
|
||||
}
|
||||
} else {
|
||||
element.entries[element.selectedEntry].RunSelect()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (element *List) changeSelectionBy (delta int) (updated bool) {
|
||||
newIndex := element.selectedEntry + delta
|
||||
if newIndex < 0 { newIndex = len(element.entries) - 1 }
|
||||
if newIndex >= len(element.entries) { newIndex = 0 }
|
||||
return element.selectEntry(newIndex)
|
||||
}
|
||||
|
||||
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
|
||||
bounds := element.Bounds()
|
||||
padding := element.theme.Padding(theme.PatternSunken)
|
||||
entry.Resize(padding.Apply(bounds).Dx())
|
||||
return entry
|
||||
}
|
||||
|
||||
func (element *List) updateMinimumSize () {
|
||||
element.contentHeight = 0
|
||||
for _, entry := range element.entries {
|
||||
element.contentHeight += entry.Bounds().Dy()
|
||||
}
|
||||
|
||||
minimumWidth := element.forcedMinimumWidth
|
||||
minimumHeight := element.forcedMinimumHeight
|
||||
|
||||
if minimumWidth == 0 {
|
||||
for _, entry := range element.entries {
|
||||
entryWidth := entry.MinimumWidth()
|
||||
if entryWidth > minimumWidth {
|
||||
minimumWidth = entryWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if minimumHeight == 0 {
|
||||
minimumHeight = element.contentHeight
|
||||
}
|
||||
|
||||
padding := element.theme.Padding(theme.PatternSunken)
|
||||
minimumHeight += padding[0] + padding[2]
|
||||
|
||||
element.core.SetMinimumSize(minimumWidth, minimumHeight)
|
||||
}
|
||||
|
||||
func (element *List) draw () {
|
||||
bounds := element.Bounds()
|
||||
padding := element.theme.Padding(theme.PatternSunken)
|
||||
innerBounds := padding.Apply(bounds)
|
||||
state := theme.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
}
|
||||
|
||||
dot := image.Point {
|
||||
innerBounds.Min.X,
|
||||
innerBounds.Min.Y - element.scroll,
|
||||
}
|
||||
innerCanvas := canvas.Cut(element.core, innerBounds)
|
||||
for index, entry := range element.entries {
|
||||
entryPosition := dot
|
||||
dot.Y += entry.Bounds().Dy()
|
||||
if dot.Y < innerBounds.Min.Y { continue }
|
||||
if entryPosition.Y > innerBounds.Max.Y { break }
|
||||
entry.Draw (
|
||||
innerCanvas, entryPosition,
|
||||
element.Focused(), element.selectedEntry == index)
|
||||
}
|
||||
|
||||
covered := image.Rect (
|
||||
0, 0,
|
||||
innerBounds.Dx(), element.contentHeight,
|
||||
).Add(innerBounds.Min).Intersect(innerBounds)
|
||||
pattern := element.theme.Pattern(theme.PatternSunken, state)
|
||||
artist.DrawShatter (
|
||||
element.core, pattern, covered)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
||||
|
||||
// ListEntry is an item that can be added to a list.
|
||||
type ListEntry struct {
|
||||
drawer textdraw.Drawer
|
||||
bounds image.Rectangle
|
||||
text string
|
||||
width int
|
||||
minimumWidth int
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onSelect func ()
|
||||
}
|
||||
|
||||
func NewListEntry (text string, onSelect func ()) (entry ListEntry) {
|
||||
entry = ListEntry {
|
||||
text: text,
|
||||
onSelect: onSelect,
|
||||
}
|
||||
entry.theme.Case = theme.C("basic", "listEntry")
|
||||
entry.drawer.SetText([]rune(text))
|
||||
entry.updateBounds()
|
||||
return
|
||||
}
|
||||
|
||||
func (entry *ListEntry) SetTheme (new theme.Theme) {
|
||||
if new == entry.theme.Theme { return }
|
||||
entry.theme.Theme = new
|
||||
entry.drawer.SetFace (entry.theme.FontFace (
|
||||
theme.FontStyleRegular,
|
||||
theme.FontSizeNormal))
|
||||
entry.updateBounds()
|
||||
}
|
||||
|
||||
func (entry *ListEntry) SetConfig (new config.Config) {
|
||||
if new == entry.config.Config { return }
|
||||
entry.config.Config = new
|
||||
}
|
||||
|
||||
func (entry *ListEntry) updateBounds () {
|
||||
padding := entry.theme.Padding(theme.PatternRaised)
|
||||
entry.bounds = padding.Inverse().Apply(entry.drawer.LayoutBounds())
|
||||
entry.bounds = entry.bounds.Sub(entry.bounds.Min)
|
||||
entry.minimumWidth = entry.bounds.Dx()
|
||||
entry.bounds.Max.X = entry.width
|
||||
}
|
||||
|
||||
func (entry *ListEntry) Draw (
|
||||
destination canvas.Canvas,
|
||||
offset image.Point,
|
||||
focused bool,
|
||||
on bool,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
state := theme.State {
|
||||
Focused: focused,
|
||||
On: on,
|
||||
}
|
||||
|
||||
pattern := entry.theme.Pattern(theme.PatternRaised, state)
|
||||
padding := entry.theme.Padding(theme.PatternRaised)
|
||||
bounds := entry.Bounds().Add(offset)
|
||||
artist.DrawBounds(destination, pattern, bounds)
|
||||
|
||||
foreground := entry.theme.Color (theme.ColorForeground, state)
|
||||
return entry.drawer.Draw (
|
||||
destination,
|
||||
foreground,
|
||||
offset.Add(image.Pt(padding[artist.SideLeft], padding[artist.SideTop])).
|
||||
Sub(entry.drawer.LayoutBounds().Min))
|
||||
}
|
||||
|
||||
func (entry *ListEntry) RunSelect () {
|
||||
if entry.onSelect != nil {
|
||||
entry.onSelect()
|
||||
}
|
||||
}
|
||||
|
||||
func (entry *ListEntry) Bounds () (bounds image.Rectangle) {
|
||||
return entry.bounds
|
||||
}
|
||||
|
||||
func (entry *ListEntry) Resize (width int) {
|
||||
entry.width = width
|
||||
entry.updateBounds()
|
||||
}
|
||||
|
||||
func (entry *ListEntry) MinimumWidth () (width int) {
|
||||
return entry.minimumWidth
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// ProgressBar displays a visual indication of how far along a task is.
|
||||
type ProgressBar struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
progress float64
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
}
|
||||
|
||||
// NewProgressBar creates a new progress bar displaying the given progress
|
||||
// level.
|
||||
func NewProgressBar (progress float64) (element *ProgressBar) {
|
||||
element = &ProgressBar { progress: progress }
|
||||
element.theme.Case = theme.C("basic", "progressBar")
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
return
|
||||
}
|
||||
|
||||
// SetProgress sets the progress level of the bar.
|
||||
func (element *ProgressBar) SetProgress (progress float64) {
|
||||
if progress == element.progress { return }
|
||||
element.progress = progress
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *ProgressBar) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *ProgressBar) SetConfig (new config.Config) {
|
||||
if new == nil || new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element (ProgressBar)) updateMinimumSize() {
|
||||
padding := element.theme.Padding(theme.PatternSunken)
|
||||
innerPadding := element.theme.Padding(theme.PatternMercury)
|
||||
element.core.SetMinimumSize (
|
||||
padding.Horizontal() + innerPadding.Horizontal(),
|
||||
padding.Vertical() + innerPadding.Vertical())
|
||||
}
|
||||
|
||||
func (element *ProgressBar) redo () {
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ProgressBar) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
pattern := element.theme.Pattern(theme.PatternSunken, theme.State { })
|
||||
padding := element.theme.Padding(theme.PatternSunken)
|
||||
pattern.Draw(element.core, 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(theme.PatternMercury, theme.State { })
|
||||
artist.DrawBounds(element.core, mercury, meterBounds)
|
||||
}
|
||||
@@ -1,528 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// ScrollContainer is a container that is capable of holding a scrollable
|
||||
// element.
|
||||
type ScrollContainer struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
focused bool
|
||||
|
||||
child elements.Scrollable
|
||||
childWidth, childHeight int
|
||||
|
||||
horizontal struct {
|
||||
theme theme.Wrapped
|
||||
exists bool
|
||||
enabled bool
|
||||
dragging bool
|
||||
dragOffset int
|
||||
gutter image.Rectangle
|
||||
track image.Rectangle
|
||||
bar image.Rectangle
|
||||
}
|
||||
|
||||
vertical struct {
|
||||
theme theme.Wrapped
|
||||
exists bool
|
||||
enabled bool
|
||||
dragging bool
|
||||
dragOffset int
|
||||
gutter image.Rectangle
|
||||
track image.Rectangle
|
||||
bar image.Rectangle
|
||||
}
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onFocusRequest func () (granted bool)
|
||||
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
|
||||
}
|
||||
|
||||
// NewScrollContainer creates a new scroll container with the specified scroll
|
||||
// bars.
|
||||
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
|
||||
element = &ScrollContainer { }
|
||||
element.theme.Case = theme.C("basic", "scrollContainer")
|
||||
element.horizontal.theme.Case = theme.C("basic", "scrollBarHorizontal")
|
||||
element.vertical.theme.Case = theme.C("basic", "scrollBarVertical")
|
||||
|
||||
element.Core, element.core = core.NewCore(element.handleResize)
|
||||
element.horizontal.exists = horizontal
|
||||
element.vertical.exists = vertical
|
||||
return
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) handleResize () {
|
||||
element.recalculate()
|
||||
element.resizeChildToFit()
|
||||
element.draw()
|
||||
}
|
||||
|
||||
// Adopt adds a scrollable element to the scroll container. The container can
|
||||
// only contain one scrollable element at a time, and when a new one is adopted
|
||||
// it replaces the last one.
|
||||
func (element *ScrollContainer) Adopt (child elements.Scrollable) {
|
||||
// disown previous child if it exists
|
||||
if element.child != nil {
|
||||
element.clearChildEventHandlers(child)
|
||||
}
|
||||
|
||||
// adopt new child
|
||||
element.child = child
|
||||
if child != nil {
|
||||
if child0, ok := child.(elements.Themeable); ok {
|
||||
child0.SetTheme(element.theme.Theme)
|
||||
}
|
||||
if child0, ok := child.(elements.Configurable); ok {
|
||||
child0.SetConfig(element.config.Config)
|
||||
}
|
||||
child.OnDamage(element.childDamageCallback)
|
||||
child.OnMinimumSizeChange(element.updateMinimumSize)
|
||||
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
|
||||
if newChild, ok := child.(elements.Focusable); ok {
|
||||
newChild.OnFocusRequest (
|
||||
element.childFocusRequestCallback)
|
||||
newChild.OnFocusMotionRequest (
|
||||
element.childFocusMotionRequestCallback)
|
||||
}
|
||||
|
||||
element.updateMinimumSize()
|
||||
|
||||
element.horizontal.enabled,
|
||||
element.vertical.enabled = element.child.ScrollAxes()
|
||||
|
||||
if element.core.HasImage() {
|
||||
element.resizeChildToFit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *ScrollContainer) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
if child, ok := element.child.(elements.Themeable); ok {
|
||||
child.SetTheme(element.theme.Theme)
|
||||
}
|
||||
if element.core.HasImage() {
|
||||
element.recalculate()
|
||||
element.resizeChildToFit()
|
||||
element.draw()
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *ScrollContainer) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
if child, ok := element.child.(elements.Configurable); ok {
|
||||
child.SetConfig(element.config.Config)
|
||||
}
|
||||
if element.core.HasImage() {
|
||||
element.recalculate()
|
||||
element.resizeChildToFit()
|
||||
element.draw()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if child, ok := element.child.(elements.KeyboardTarget); ok {
|
||||
child.HandleKeyDown(key, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
if child, ok := element.child.(elements.KeyboardTarget); ok {
|
||||
child.HandleKeyUp(key, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) {
|
||||
velocity := element.config.ScrollVelocity()
|
||||
point := image.Pt(x, y)
|
||||
if point.In(element.horizontal.bar) {
|
||||
element.horizontal.dragging = true
|
||||
element.horizontal.dragOffset =
|
||||
x - element.horizontal.bar.Min.X +
|
||||
element.Bounds().Min.X
|
||||
element.dragHorizontalBar(point)
|
||||
|
||||
} else if point.In(element.horizontal.gutter) {
|
||||
switch button {
|
||||
case input.ButtonLeft:
|
||||
element.horizontal.dragging = true
|
||||
element.horizontal.dragOffset =
|
||||
element.horizontal.bar.Dx() / 2 +
|
||||
element.Bounds().Min.X
|
||||
element.dragHorizontalBar(point)
|
||||
case input.ButtonMiddle:
|
||||
viewport := element.child.ScrollViewportBounds().Dx()
|
||||
if x > element.horizontal.bar.Min.X {
|
||||
element.scrollChildBy(viewport, 0)
|
||||
} else {
|
||||
element.scrollChildBy(-viewport, 0)
|
||||
}
|
||||
case input.ButtonRight:
|
||||
if x > element.horizontal.bar.Min.X {
|
||||
element.scrollChildBy(velocity, 0)
|
||||
} else {
|
||||
element.scrollChildBy(-velocity, 0)
|
||||
}
|
||||
}
|
||||
|
||||
} else if point.In(element.vertical.bar) {
|
||||
element.vertical.dragging = true
|
||||
element.vertical.dragOffset =
|
||||
y - element.vertical.bar.Min.Y +
|
||||
element.Bounds().Min.Y
|
||||
element.dragVerticalBar(point)
|
||||
|
||||
} else if point.In(element.vertical.gutter) {
|
||||
switch button {
|
||||
case input.ButtonLeft:
|
||||
element.vertical.dragging = true
|
||||
element.vertical.dragOffset =
|
||||
element.vertical.bar.Dy() / 2 +
|
||||
element.Bounds().Min.Y
|
||||
element.dragVerticalBar(point)
|
||||
case input.ButtonMiddle:
|
||||
viewport := element.child.ScrollViewportBounds().Dy()
|
||||
if y > element.vertical.bar.Min.Y {
|
||||
element.scrollChildBy(0, viewport)
|
||||
} else {
|
||||
element.scrollChildBy(0, -viewport)
|
||||
}
|
||||
case input.ButtonRight:
|
||||
if y > element.vertical.bar.Min.Y {
|
||||
element.scrollChildBy(0, velocity)
|
||||
} else {
|
||||
element.scrollChildBy(0, -velocity)
|
||||
}
|
||||
}
|
||||
|
||||
} else if child, ok := element.child.(elements.MouseTarget); ok {
|
||||
child.HandleMouseDown(x, y, button)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleMouseUp (x, y int, button input.Button) {
|
||||
if element.horizontal.dragging {
|
||||
element.horizontal.dragging = false
|
||||
element.drawHorizontalBar()
|
||||
element.core.DamageRegion(element.horizontal.bar)
|
||||
|
||||
} else if element.vertical.dragging {
|
||||
element.vertical.dragging = false
|
||||
element.drawVerticalBar()
|
||||
element.core.DamageRegion(element.vertical.bar)
|
||||
|
||||
} else if child, ok := element.child.(elements.MouseTarget); ok {
|
||||
child.HandleMouseUp(x, y, button)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleMouseMove (x, y int) {
|
||||
if element.horizontal.dragging {
|
||||
element.dragHorizontalBar(image.Pt(x, y))
|
||||
|
||||
} else if element.vertical.dragging {
|
||||
element.dragVerticalBar(image.Pt(x, y))
|
||||
|
||||
} else if child, ok := element.child.(elements.MouseTarget); ok {
|
||||
child.HandleMouseMove(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleMouseScroll (
|
||||
x, y int,
|
||||
deltaX, deltaY float64,
|
||||
) {
|
||||
element.scrollChildBy(int(deltaX), int(deltaY))
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) scrollChildBy (x, y int) {
|
||||
if element.child == nil { return }
|
||||
scrollPoint :=
|
||||
element.child.ScrollViewportBounds().Min.
|
||||
Add(image.Pt(x, y))
|
||||
element.child.ScrollTo(scrollPoint)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) Focused () (focused bool) {
|
||||
return element.focused
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) Focus () {
|
||||
if element.onFocusRequest != nil {
|
||||
if element.onFocusRequest() {
|
||||
element.focused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleFocus (
|
||||
direction input.KeynavDirection,
|
||||
) (
|
||||
accepted bool,
|
||||
) {
|
||||
if child, ok := element.child.(elements.Focusable); ok {
|
||||
element.focused = child.HandleFocus(direction)
|
||||
return element.focused
|
||||
} else {
|
||||
element.focused = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) HandleUnfocus () {
|
||||
if child, ok := element.child.(elements.Focusable); ok {
|
||||
child.HandleUnfocus()
|
||||
}
|
||||
element.focused = false
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) OnFocusRequest (callback func () (granted bool)) {
|
||||
element.onFocusRequest = callback
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) OnFocusMotionRequest (
|
||||
callback func (direction input.KeynavDirection) (granted bool),
|
||||
) {
|
||||
element.onFocusMotionRequest = callback
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) childDamageCallback (region canvas.Canvas) {
|
||||
element.core.DamageRegion(region.Bounds())
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) childFocusRequestCallback () (granted bool) {
|
||||
if element.onFocusRequest != nil {
|
||||
element.focused = element.onFocusRequest()
|
||||
return element.focused
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) childFocusMotionRequestCallback (
|
||||
direction input.KeynavDirection,
|
||||
) (
|
||||
granted bool,
|
||||
) {
|
||||
if element.onFocusMotionRequest == nil { return }
|
||||
return element.onFocusMotionRequest(direction)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollable) {
|
||||
child.DrawTo(nil)
|
||||
child.OnDamage(nil)
|
||||
child.OnMinimumSizeChange(nil)
|
||||
child.OnScrollBoundsChange(nil)
|
||||
if child0, ok := child.(elements.Focusable); ok {
|
||||
child0.OnFocusRequest(nil)
|
||||
child0.OnFocusMotionRequest(nil)
|
||||
if child0.Focused() {
|
||||
child0.HandleUnfocus()
|
||||
}
|
||||
}
|
||||
if child0, ok := child.(elements.Flexible); ok {
|
||||
child0.OnFlexibleHeightChange(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) resizeChildToFit () {
|
||||
childBounds := image.Rect (
|
||||
0, 0,
|
||||
element.childWidth,
|
||||
element.childHeight).Add(element.Bounds().Min)
|
||||
element.child.DrawTo(canvas.Cut(element.core, childBounds))
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) recalculate () {
|
||||
horizontal := &element.horizontal
|
||||
vertical := &element.vertical
|
||||
|
||||
gutterInsetHorizontal := horizontal.theme.Padding(theme.PatternGutter)
|
||||
gutterInsetVertical := vertical.theme.Padding(theme.PatternGutter)
|
||||
|
||||
bounds := element.Bounds()
|
||||
thicknessHorizontal :=
|
||||
element.config.HandleWidth() +
|
||||
gutterInsetHorizontal[3] +
|
||||
gutterInsetHorizontal[1]
|
||||
thicknessVertical :=
|
||||
element.config.HandleWidth() +
|
||||
gutterInsetVertical[3] +
|
||||
gutterInsetVertical[1]
|
||||
|
||||
// calculate child size
|
||||
element.childWidth = bounds.Dx()
|
||||
element.childHeight = bounds.Dy()
|
||||
|
||||
// reset bounds
|
||||
horizontal.gutter = image.Rectangle { }
|
||||
vertical.gutter = image.Rectangle { }
|
||||
horizontal.bar = image.Rectangle { }
|
||||
vertical.bar = image.Rectangle { }
|
||||
|
||||
// if enabled, give substance to the gutters
|
||||
if horizontal.exists {
|
||||
horizontal.gutter.Min.X = bounds.Min.X
|
||||
horizontal.gutter.Min.Y = bounds.Max.Y - thicknessHorizontal
|
||||
horizontal.gutter.Max.X = bounds.Max.X
|
||||
horizontal.gutter.Max.Y = bounds.Max.Y
|
||||
if vertical.exists {
|
||||
horizontal.gutter.Max.X -= thicknessVertical
|
||||
}
|
||||
element.childHeight -= thicknessHorizontal
|
||||
horizontal.track = gutterInsetHorizontal.Apply(horizontal.gutter)
|
||||
}
|
||||
if vertical.exists {
|
||||
vertical.gutter.Min.X = bounds.Max.X - thicknessVertical
|
||||
vertical.gutter.Max.X = bounds.Max.X
|
||||
vertical.gutter.Min.Y = bounds.Min.Y
|
||||
vertical.gutter.Max.Y = bounds.Max.Y
|
||||
if horizontal.exists {
|
||||
vertical.gutter.Max.Y -= thicknessHorizontal
|
||||
}
|
||||
element.childWidth -= thicknessVertical
|
||||
vertical.track = gutterInsetVertical.Apply(vertical.gutter)
|
||||
}
|
||||
|
||||
// if enabled, calculate the positions of the bars
|
||||
contentBounds := element.child.ScrollContentBounds()
|
||||
viewportBounds := element.child.ScrollViewportBounds()
|
||||
if horizontal.exists && horizontal.enabled {
|
||||
horizontal.bar.Min.Y = horizontal.track.Min.Y
|
||||
horizontal.bar.Max.Y = horizontal.track.Max.Y
|
||||
|
||||
scale := float64(horizontal.track.Dx()) /
|
||||
float64(contentBounds.Dx())
|
||||
horizontal.bar.Min.X = int(float64(viewportBounds.Min.X) * scale)
|
||||
horizontal.bar.Max.X = int(float64(viewportBounds.Max.X) * scale)
|
||||
|
||||
horizontal.bar.Min.X += horizontal.track.Min.X
|
||||
horizontal.bar.Max.X += horizontal.track.Min.X
|
||||
}
|
||||
if vertical.exists && vertical.enabled {
|
||||
vertical.bar.Min.X = vertical.track.Min.X
|
||||
vertical.bar.Max.X = vertical.track.Max.X
|
||||
|
||||
scale := float64(vertical.track.Dy()) /
|
||||
float64(contentBounds.Dy())
|
||||
vertical.bar.Min.Y = int(float64(viewportBounds.Min.Y) * scale)
|
||||
vertical.bar.Max.Y = int(float64(viewportBounds.Max.Y) * scale)
|
||||
|
||||
vertical.bar.Min.Y += vertical.track.Min.Y
|
||||
vertical.bar.Max.Y += vertical.track.Min.Y
|
||||
}
|
||||
|
||||
// if the scroll bars are out of bounds, don't display them.
|
||||
if horizontal.bar.Dx() >= horizontal.track.Dx() {
|
||||
horizontal.bar = image.Rectangle { }
|
||||
}
|
||||
if vertical.bar.Dy() >= vertical.track.Dy() {
|
||||
vertical.bar = image.Rectangle { }
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) draw () {
|
||||
deadPattern := element.theme.Pattern (
|
||||
theme.PatternDead, theme.State { })
|
||||
artist.DrawBounds (
|
||||
element.core, deadPattern,
|
||||
image.Rect (
|
||||
element.vertical.gutter.Min.X,
|
||||
element.horizontal.gutter.Min.Y,
|
||||
element.vertical.gutter.Max.X,
|
||||
element.horizontal.gutter.Max.Y))
|
||||
element.drawHorizontalBar()
|
||||
element.drawVerticalBar()
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) drawHorizontalBar () {
|
||||
state := theme.State {
|
||||
Disabled: !element.horizontal.enabled,
|
||||
Pressed: element.horizontal.dragging,
|
||||
}
|
||||
gutterPattern := element.horizontal.theme.Pattern(theme.PatternGutter, state)
|
||||
artist.DrawBounds(element.core, gutterPattern, element.horizontal.gutter)
|
||||
|
||||
handlePattern := element.horizontal.theme.Pattern(theme.PatternHandle, state)
|
||||
artist.DrawBounds(element.core, handlePattern, element.horizontal.bar)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) drawVerticalBar () {
|
||||
state := theme.State {
|
||||
Disabled: !element.vertical.enabled,
|
||||
Pressed: element.vertical.dragging,
|
||||
}
|
||||
gutterPattern := element.vertical.theme.Pattern(theme.PatternGutter, state)
|
||||
artist.DrawBounds(element.core, gutterPattern, element.vertical.gutter)
|
||||
|
||||
handlePattern := element.vertical.theme.Pattern(theme.PatternHandle, state)
|
||||
artist.DrawBounds(element.core, handlePattern, element.vertical.bar)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) {
|
||||
scrollX :=
|
||||
float64(element.child.ScrollContentBounds().Dx()) /
|
||||
float64(element.horizontal.track.Dx()) *
|
||||
float64(mousePosition.X - element.horizontal.dragOffset)
|
||||
scrollY := element.child.ScrollViewportBounds().Min.Y
|
||||
element.child.ScrollTo(image.Pt(int(scrollX), scrollY))
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) {
|
||||
scrollY :=
|
||||
float64(element.child.ScrollContentBounds().Dy()) /
|
||||
float64(element.vertical.track.Dy()) *
|
||||
float64(mousePosition.Y - element.vertical.dragOffset)
|
||||
scrollX := element.child.ScrollViewportBounds().Min.X
|
||||
element.child.ScrollTo(image.Pt(scrollX, int(scrollY)))
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) updateMinimumSize () {
|
||||
gutterInsetHorizontal := element.horizontal.theme.Padding(theme.PatternGutter)
|
||||
gutterInsetVertical := element.vertical.theme.Padding(theme.PatternGutter)
|
||||
|
||||
thicknessHorizontal :=
|
||||
element.config.HandleWidth() +
|
||||
gutterInsetHorizontal[3] +
|
||||
gutterInsetHorizontal[1]
|
||||
thicknessVertical :=
|
||||
element.config.HandleWidth() +
|
||||
gutterInsetVertical[3] +
|
||||
gutterInsetVertical[1]
|
||||
|
||||
width := thicknessHorizontal
|
||||
height := thicknessVertical
|
||||
if element.child != nil {
|
||||
childWidth, childHeight := element.child.MinimumSize()
|
||||
width += childWidth
|
||||
height += childHeight
|
||||
}
|
||||
element.core.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *ScrollContainer) childScrollBoundsChangeCallback () {
|
||||
element.horizontal.enabled,
|
||||
element.vertical.enabled = element.child.ScrollAxes()
|
||||
if element.core.HasImage() {
|
||||
element.recalculate()
|
||||
element.drawHorizontalBar()
|
||||
element.drawVerticalBar()
|
||||
element.core.DamageRegion(element.horizontal.gutter)
|
||||
element.core.DamageRegion(element.vertical.gutter)
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// Slider is a slider control with a floating point value between zero and one.
|
||||
type Slider struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
|
||||
value float64
|
||||
vertical bool
|
||||
dragging bool
|
||||
dragOffset int
|
||||
track image.Rectangle
|
||||
bar image.Rectangle
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onSlide func ()
|
||||
onRelease func ()
|
||||
}
|
||||
|
||||
// NewSlider creates a new slider with the specified value. If vertical is set
|
||||
// to true,
|
||||
func NewSlider (value float64, vertical bool) (element *Slider) {
|
||||
element = &Slider {
|
||||
value: value,
|
||||
vertical: vertical,
|
||||
}
|
||||
if vertical {
|
||||
element.theme.Case = theme.C("basic", "sliderVertical")
|
||||
} else {
|
||||
element.theme.Case = theme.C("basic", "sliderHorizontal")
|
||||
}
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore(element.redo)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Slider) HandleMouseDown (x, y int, button input.Button) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
if button == input.ButtonLeft {
|
||||
element.dragging = true
|
||||
element.value = element.valueFor(x, y)
|
||||
if element.onSlide != nil {
|
||||
element.onSlide()
|
||||
}
|
||||
element.redo()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Slider) HandleMouseUp (x, y int, button input.Button) {
|
||||
if button != input.ButtonLeft || !element.dragging { return }
|
||||
element.dragging = false
|
||||
if element.onRelease != nil {
|
||||
element.onRelease()
|
||||
}
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *Slider) HandleMouseMove (x, y int) {
|
||||
if element.dragging {
|
||||
element.dragging = true
|
||||
element.value = element.valueFor(x, y)
|
||||
if element.onSlide != nil {
|
||||
element.onSlide()
|
||||
}
|
||||
element.redo()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Slider) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *Slider) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
// TODO: handle left and right arrows
|
||||
}
|
||||
|
||||
func (element *Slider) HandleKeyUp (key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
// Value returns the slider's value.
|
||||
func (element *Slider) Value () (value float64) {
|
||||
return element.value
|
||||
}
|
||||
|
||||
// SetEnabled sets whether or not the slider can be interacted with.
|
||||
func (element *Slider) SetEnabled (enabled bool) {
|
||||
element.focusableControl.SetEnabled(enabled)
|
||||
}
|
||||
|
||||
// SetValue sets the slider's value.
|
||||
func (element *Slider) SetValue (value float64) {
|
||||
if value < 0 { value = 0 }
|
||||
if value > 1 { value = 1 }
|
||||
|
||||
if element.value == value { return }
|
||||
|
||||
element.value = value
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// OnSlide sets a function to be called every time the slider handle changes
|
||||
// position while being dragged.
|
||||
func (element *Slider) OnSlide (callback func ()) {
|
||||
element.onSlide = callback
|
||||
}
|
||||
|
||||
// OnRelease sets a function to be called when the handle stops being dragged.
|
||||
func (element *Slider) OnRelease (callback func ()) {
|
||||
element.onRelease = callback
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *Slider) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *Slider) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *Slider) valueFor (x, y int) (value float64) {
|
||||
if element.vertical {
|
||||
value =
|
||||
float64(y - element.track.Min.Y - element.bar.Dy() / 2) /
|
||||
float64(element.track.Dy() - element.bar.Dy())
|
||||
value = 1 - value
|
||||
} else {
|
||||
value =
|
||||
float64(x - element.track.Min.X - element.bar.Dx() / 2) /
|
||||
float64(element.track.Dx() - element.bar.Dx())
|
||||
}
|
||||
|
||||
if value < 0 { value = 0 }
|
||||
if value > 1 { value = 1 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Slider) updateMinimumSize () {
|
||||
if element.vertical {
|
||||
element.core.SetMinimumSize (
|
||||
element.config.HandleWidth(),
|
||||
element.config.HandleWidth() * 2)
|
||||
} else {
|
||||
element.core.SetMinimumSize (
|
||||
element.config.HandleWidth() * 2,
|
||||
element.config.HandleWidth())
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Slider) redo () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Slider) draw () {
|
||||
bounds := element.Bounds()
|
||||
element.track = element.theme.Padding(theme.PatternGutter).Apply(bounds)
|
||||
if element.vertical {
|
||||
barSize := element.track.Dx()
|
||||
element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min)
|
||||
barOffset :=
|
||||
float64(element.track.Dy() - barSize) *
|
||||
(1 - element.value)
|
||||
element.bar = element.bar.Add(image.Pt(0, int(barOffset)))
|
||||
} else {
|
||||
barSize := element.track.Dy()
|
||||
element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min)
|
||||
barOffset :=
|
||||
float64(element.track.Dx() - barSize) *
|
||||
element.value
|
||||
element.bar = element.bar.Add(image.Pt(int(barOffset), 0))
|
||||
}
|
||||
|
||||
state := theme.State {
|
||||
Focused: element.Focused(),
|
||||
Disabled: !element.Enabled(),
|
||||
Pressed: element.dragging,
|
||||
}
|
||||
artist.DrawBounds (
|
||||
element.core,
|
||||
element.theme.Pattern(theme.PatternGutter, state),
|
||||
bounds)
|
||||
artist.DrawBounds (
|
||||
element.core,
|
||||
element.theme.Pattern(theme.PatternHandle, state),
|
||||
element.bar)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
// Spacer can be used to put space between two elements..
|
||||
type Spacer struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
line bool
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
}
|
||||
|
||||
// NewSpacer creates a new spacer. If line is set to true, the spacer will be
|
||||
// filled with a line color, and if compressed to its minimum width or height,
|
||||
// will appear as a line.
|
||||
func NewSpacer (line bool) (element *Spacer) {
|
||||
element = &Spacer { line: line }
|
||||
element.theme.Case = theme.C("basic", "spacer")
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
/// SetLine sets whether or not the spacer will appear as a colored line.
|
||||
func (element *Spacer) SetLine (line bool) {
|
||||
if element.line == line { return }
|
||||
element.line = line
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *Spacer) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *Spacer) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *Spacer) updateMinimumSize () {
|
||||
if element.line {
|
||||
padding := element.theme.Padding(theme.PatternLine)
|
||||
element.core.SetMinimumSize (
|
||||
padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
} else {
|
||||
element.core.SetMinimumSize(1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Spacer) redo () {
|
||||
if !element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Spacer) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
if element.line {
|
||||
pattern := element.theme.Pattern (
|
||||
theme.PatternLine,
|
||||
theme.State { })
|
||||
pattern.Draw(element.core, bounds)
|
||||
} else {
|
||||
pattern := element.theme.Pattern (
|
||||
theme.PatternBackground,
|
||||
theme.State { })
|
||||
pattern.Draw(element.core, bounds)
|
||||
}
|
||||
}
|
||||
@@ -1,418 +0,0 @@
|
||||
package basicElements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/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/elements/core"
|
||||
|
||||
// TextBox is a single-line text input.
|
||||
type TextBox struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
|
||||
dragging bool
|
||||
dot textmanip.Dot
|
||||
scroll int
|
||||
placeholder string
|
||||
text []rune
|
||||
|
||||
placeholderDrawer textdraw.Drawer
|
||||
valueDrawer textdraw.Drawer
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
|
||||
onChange func ()
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
// NewTextBox creates a new text box with the specified placeholder text, and
|
||||
// a value. When the value is empty, the placeholder will be displayed in gray
|
||||
// text.
|
||||
func NewTextBox (placeholder, value string) (element *TextBox) {
|
||||
element = &TextBox { }
|
||||
element.theme.Case = theme.C("basic", "textBox")
|
||||
element.Core, element.core = core.NewCore(element.handleResize)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore (func () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
})
|
||||
element.placeholder = placeholder
|
||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||
element.updateMinimumSize()
|
||||
element.SetValue(value)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *TextBox) handleResize () {
|
||||
element.scrollToCursor()
|
||||
element.draw()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseDown (x, y int, button input.Button) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.Focused() { element.Focus() }
|
||||
|
||||
if button == input.ButtonLeft {
|
||||
runeIndex := element.atPosition(image.Pt(x, y))
|
||||
element.dragging = true
|
||||
if runeIndex > -1 {
|
||||
element.dot = textmanip.EmptyDot(runeIndex)
|
||||
element.redo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseMove (x, y int) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.Focused() { element.Focus() }
|
||||
|
||||
if element.dragging {
|
||||
runeIndex := element.atPosition(image.Pt(x, y))
|
||||
if runeIndex > -1 {
|
||||
element.dot.End = runeIndex
|
||||
element.redo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) atPosition (position image.Point) int {
|
||||
padding := element.theme.Padding(theme.PatternInput)
|
||||
offset := element.Bounds().Min.Add (image.Pt (
|
||||
padding[artist.SideLeft] - element.scroll,
|
||||
padding[artist.SideTop]))
|
||||
textBoundsMin := element.valueDrawer.LayoutBounds().Min
|
||||
return element.valueDrawer.AtPosition (
|
||||
fixedutil.Pt(position.Sub(offset).Add(textBoundsMin)))
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseUp (x, y int, button input.Button) {
|
||||
if button == input.ButtonLeft {
|
||||
element.dragging = false
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) {
|
||||
if element.onKeyDown != nil && element.onKeyDown(key, modifiers) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: text selection with shift
|
||||
|
||||
scrollMemory := element.scroll
|
||||
altered := true
|
||||
textChanged := false
|
||||
switch {
|
||||
case key == input.KeyBackspace:
|
||||
if len(element.text) < 1 { break }
|
||||
element.text, element.dot = textmanip.Backspace (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
textChanged = true
|
||||
|
||||
case key == input.KeyDelete:
|
||||
if len(element.text) < 1 { break }
|
||||
element.text, element.dot = textmanip.Delete (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
textChanged = true
|
||||
|
||||
case key == input.KeyLeft:
|
||||
if modifiers.Shift {
|
||||
element.dot = textmanip.SelectLeft (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
} else {
|
||||
element.dot = textmanip.MoveLeft (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
}
|
||||
|
||||
case key == input.KeyRight:
|
||||
if modifiers.Shift {
|
||||
element.dot = textmanip.SelectRight (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
} else {
|
||||
element.dot = textmanip.MoveRight (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
}
|
||||
|
||||
case key.Printable():
|
||||
element.text, element.dot = textmanip.Type (
|
||||
element.text,
|
||||
element.dot,
|
||||
rune(key))
|
||||
textChanged = true
|
||||
|
||||
default:
|
||||
altered = false
|
||||
}
|
||||
|
||||
if textChanged {
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
}
|
||||
|
||||
if altered {
|
||||
element.scrollToCursor()
|
||||
}
|
||||
|
||||
if (textChanged || scrollMemory != element.scroll) &&
|
||||
element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
|
||||
if altered {
|
||||
element.redo()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
func (element *TextBox) SetPlaceholder (placeholder string) {
|
||||
if element.placeholder == placeholder { return }
|
||||
|
||||
element.placeholder = placeholder
|
||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *TextBox) SetValue (text string) {
|
||||
// if element.text == text { return }
|
||||
|
||||
element.text = []rune(text)
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
if element.dot.End > element.valueDrawer.Length() {
|
||||
element.dot = textmanip.EmptyDot(element.valueDrawer.Length())
|
||||
}
|
||||
element.scrollToCursor()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *TextBox) Value () (value string) {
|
||||
return string(element.text)
|
||||
}
|
||||
|
||||
func (element *TextBox) Filled () (filled bool) {
|
||||
return len(element.text) > 0
|
||||
}
|
||||
|
||||
func (element *TextBox) OnKeyDown (
|
||||
callback func (key input.Key, modifiers input.Modifiers) (handled bool),
|
||||
) {
|
||||
element.onKeyDown = callback
|
||||
}
|
||||
|
||||
func (element *TextBox) OnChange (callback func ()) {
|
||||
element.onChange = callback
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) {
|
||||
bounds = element.valueDrawer.LayoutBounds()
|
||||
return bounds.Sub(bounds.Min)
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's viewport
|
||||
// relative to ScrollBounds.
|
||||
func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
|
||||
return image.Rect (
|
||||
element.scroll,
|
||||
0,
|
||||
element.scroll + element.scrollViewportWidth(),
|
||||
0)
|
||||
}
|
||||
|
||||
func (element *TextBox) scrollViewportWidth () (width int) {
|
||||
padding := element.theme.Padding(theme.PatternInput)
|
||||
return padding.Apply(element.Bounds()).Dx()
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *TextBox) ScrollTo (position image.Point) {
|
||||
// constrain to minimum
|
||||
element.scroll = position.X
|
||||
if element.scroll < 0 { element.scroll = 0 }
|
||||
|
||||
// constrain to maximum
|
||||
contentBounds := element.ScrollContentBounds()
|
||||
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
|
||||
if element.scroll > maxPosition { element.scroll = maxPosition }
|
||||
|
||||
element.redo()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
|
||||
return true, false
|
||||
}
|
||||
|
||||
func (element *TextBox) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
func (element *TextBox) runOnChange () {
|
||||
if element.onChange != nil {
|
||||
element.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) scrollToCursor () {
|
||||
if !element.core.HasImage() { return }
|
||||
|
||||
padding := element.theme.Padding(theme.PatternInput)
|
||||
bounds := padding.Apply(element.Bounds())
|
||||
bounds = bounds.Sub(bounds.Min)
|
||||
bounds.Max.X -= element.valueDrawer.Em().Round()
|
||||
cursorPosition := fixedutil.RoundPt (
|
||||
element.valueDrawer.PositionAt(element.dot.End))
|
||||
cursorPosition.X -= element.scroll
|
||||
maxX := bounds.Max.X
|
||||
minX := maxX
|
||||
if cursorPosition.X > maxX {
|
||||
element.scroll += cursorPosition.X - maxX
|
||||
} else if cursorPosition.X < minX {
|
||||
element.scroll -= minX - cursorPosition.X
|
||||
if element.scroll < 0 { element.scroll = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *TextBox) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
face := element.theme.FontFace (
|
||||
theme.FontStyleRegular,
|
||||
theme.FontSizeNormal)
|
||||
element.placeholderDrawer.SetFace(face)
|
||||
element.valueDrawer.SetFace(face)
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *TextBox) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *TextBox) updateMinimumSize () {
|
||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||
padding := element.theme.Padding(theme.PatternInput)
|
||||
element.core.SetMinimumSize (
|
||||
padding.Horizontal() + textBounds.Dx(),
|
||||
padding.Vertical() +
|
||||
element.placeholderDrawer.LineHeight().Round())
|
||||
}
|
||||
|
||||
func (element *TextBox) redo () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
state := theme.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
}
|
||||
pattern := element.theme.Pattern(theme.PatternInput, state)
|
||||
padding := element.theme.Padding(theme.PatternInput)
|
||||
innerCanvas := canvas.Cut(element.core, padding.Apply(bounds))
|
||||
pattern.Draw(element.core, bounds)
|
||||
|
||||
offset := bounds.Min.Add (image.Point {
|
||||
X: padding[artist.SideLeft] - element.scroll,
|
||||
Y: padding[artist.SideTop],
|
||||
})
|
||||
|
||||
if element.Focused() && !element.dot.Empty() {
|
||||
// draw selection bounds
|
||||
accent := element.theme.Color(theme.ColorAccent, state)
|
||||
canon := element.dot.Canon()
|
||||
foff := fixedutil.Pt(offset)
|
||||
start := element.valueDrawer.PositionAt(canon.Start).Add(foff)
|
||||
end := element.valueDrawer.PositionAt(canon.End).Add(foff)
|
||||
end.Y += element.valueDrawer.LineHeight()
|
||||
shapes.FillColorRectangle (
|
||||
innerCanvas,
|
||||
accent,
|
||||
image.Rectangle {
|
||||
fixedutil.RoundPt(start),
|
||||
fixedutil.RoundPt(end),
|
||||
})
|
||||
}
|
||||
|
||||
if len(element.text) == 0 {
|
||||
// draw placeholder
|
||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||
foreground := element.theme.Color (
|
||||
theme.ColorForeground,
|
||||
theme.State { Disabled: true })
|
||||
element.placeholderDrawer.Draw (
|
||||
innerCanvas,
|
||||
foreground,
|
||||
offset.Sub(textBounds.Min))
|
||||
} else {
|
||||
// draw input value
|
||||
textBounds := element.valueDrawer.LayoutBounds()
|
||||
foreground := element.theme.Color(theme.ColorForeground, state)
|
||||
element.valueDrawer.Draw (
|
||||
innerCanvas,
|
||||
foreground,
|
||||
offset.Sub(textBounds.Min))
|
||||
}
|
||||
|
||||
if element.Focused() && element.dot.Empty() {
|
||||
// draw cursor
|
||||
foreground := element.theme.Color(theme.ColorForeground, state)
|
||||
cursorPosition := fixedutil.RoundPt (
|
||||
element.valueDrawer.PositionAt(element.dot.End))
|
||||
shapes.ColorLine (
|
||||
innerCanvas,
|
||||
foreground, 1,
|
||||
cursorPosition.Add(offset),
|
||||
image.Pt (
|
||||
cursorPosition.X,
|
||||
cursorPosition.Y + element.valueDrawer.
|
||||
LineHeight().Round()).Add(offset))
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||
|
||||
var boxCase = tomo.C("tomo", "box")
|
||||
|
||||
// Space is a list of spacing configurations that can be passed to some
|
||||
// containers.
|
||||
type Space int; const (
|
||||
SpaceNone Space = 0
|
||||
SpacePadding Space = 1
|
||||
SpaceMargin Space = 2
|
||||
SpaceBoth Space = SpacePadding | SpaceMargin
|
||||
)
|
||||
|
||||
// Includes returns whether a spacing value has been or'd with another spacing
|
||||
// value.
|
||||
func (space Space) Includes (sub Space) bool {
|
||||
return (space & sub) > 0
|
||||
}
|
||||
|
||||
// Box is a container that lays out its children horizontally or vertically.
|
||||
// Child elements can be set to contract to their minimum size, or expand to
|
||||
// fill remaining space. Boxes can be nested and used together to create more
|
||||
// complex layouts.
|
||||
type Box struct {
|
||||
container
|
||||
padding bool
|
||||
margin bool
|
||||
vertical bool
|
||||
}
|
||||
|
||||
// NewHBox creates a new horizontal box.
|
||||
func NewHBox (space Space, children ...tomo.Element) (element *Box) {
|
||||
element = &Box {
|
||||
padding: space.Includes(SpacePadding),
|
||||
margin: space.Includes(SpaceMargin),
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init()
|
||||
element.Adopt(children...)
|
||||
return
|
||||
}
|
||||
|
||||
// NewHBox creates a new vertical box.
|
||||
func NewVBox (space Space, children ...tomo.Element) (element *Box) {
|
||||
element = &Box {
|
||||
padding: space.Includes(SpacePadding),
|
||||
margin: space.Includes(SpaceMargin),
|
||||
vertical: true,
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init()
|
||||
element.Adopt(children...)
|
||||
return
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination 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()
|
||||
}
|
||||
|
||||
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
|
||||
for _, tile := range tiles {
|
||||
element.entity.DrawBackground(artist.Cut(destination, tile))
|
||||
}
|
||||
}
|
||||
|
||||
// Layout causes this element to perform a layout operation.
|
||||
func (element *Box) Layout () {
|
||||
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) }
|
||||
|
||||
var marginSize float64; if element.vertical {
|
||||
marginSize = float64(margin.Y)
|
||||
} else {
|
||||
marginSize = float64(margin.X)
|
||||
}
|
||||
|
||||
freeSpace, nExpanding := element.freeSpace()
|
||||
expandingElementSize := freeSpace / nExpanding
|
||||
|
||||
// set the size and position of each element
|
||||
x := float64(bounds.Min.X)
|
||||
y := float64(bounds.Min.Y)
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
entry := element.scratch[element.entity.Child(index)]
|
||||
|
||||
var size float64; if entry.expand {
|
||||
size = expandingElementSize
|
||||
} else {
|
||||
size = entry.minSize
|
||||
}
|
||||
|
||||
var childBounds image.Rectangle; if element.vertical {
|
||||
childBounds = tomo.Bounds(int(x), int(y), bounds.Dx(), int(size))
|
||||
} else {
|
||||
childBounds = tomo.Bounds(int(x), int(y), int(size), bounds.Dy())
|
||||
}
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
|
||||
if element.vertical {
|
||||
y += size
|
||||
if element.margin { y += marginSize }
|
||||
} else {
|
||||
x += size
|
||||
if element.margin { x += marginSize }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AdoptExpand adds one or more elements to the box. These elements will be
|
||||
// expanded to fill in empty space.
|
||||
func (element *Box) AdoptExpand (children ...tomo.Element) {
|
||||
element.adopt(true, children...)
|
||||
}
|
||||
|
||||
// DrawBackground draws this element's background pattern to the specified
|
||||
// destination canvas.
|
||||
func (element *Box) DrawBackground (destination artist.Canvas) {
|
||||
element.entity.DrawBackground(destination)
|
||||
}
|
||||
|
||||
func (element *Box) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Box) freeSpace () (space float64, nExpanding float64) {
|
||||
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
|
||||
} else {
|
||||
marginSize = margin.X
|
||||
}
|
||||
|
||||
if element.vertical {
|
||||
space = float64(element.entity.Bounds().Dy())
|
||||
} else {
|
||||
space = float64(element.entity.Bounds().Dx())
|
||||
}
|
||||
|
||||
for _, entry := range element.scratch {
|
||||
if entry.expand {
|
||||
nExpanding ++;
|
||||
} else {
|
||||
space -= float64(entry.minSize)
|
||||
}
|
||||
}
|
||||
|
||||
if element.padding {
|
||||
space -= float64(padding.Vertical())
|
||||
}
|
||||
if element.margin {
|
||||
space -= float64(marginSize * (len(element.scratch) - 1))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Box) updateMinimumSize () {
|
||||
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
|
||||
} else {
|
||||
marginSize = margin.X
|
||||
}
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
childWidth, childHeight := element.entity.ChildMinimumSize(index)
|
||||
var childBreadth, childSize int; if element.vertical {
|
||||
childBreadth, childSize = childWidth, childHeight
|
||||
} else {
|
||||
childBreadth, childSize = childHeight, childWidth
|
||||
}
|
||||
|
||||
key := element.entity.Child(index)
|
||||
entry := element.scratch[key]
|
||||
entry.minSize = float64(childSize)
|
||||
element.scratch[key] = entry
|
||||
|
||||
if childBreadth > breadth {
|
||||
breadth = childBreadth
|
||||
}
|
||||
size += childSize
|
||||
if element.margin && index > 0 {
|
||||
size += marginSize
|
||||
}
|
||||
}
|
||||
|
||||
var width, height int; if element.vertical {
|
||||
width, height = breadth, size
|
||||
} else {
|
||||
width, height = size, breadth
|
||||
}
|
||||
|
||||
if element.padding {
|
||||
width += padding.Horizontal()
|
||||
height += padding.Vertical()
|
||||
}
|
||||
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package elements
|
||||
|
||||
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/textdraw"
|
||||
|
||||
var buttonCase = tomo.C("tomo", "button")
|
||||
|
||||
// Button is a clickable button.
|
||||
type Button struct {
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
text string
|
||||
|
||||
showText bool
|
||||
hasIcon bool
|
||||
iconId tomo.Icon
|
||||
|
||||
onClick func ()
|
||||
}
|
||||
|
||||
// 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.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
buttonCase))
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Button) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Button) Draw (destination artist.Canvas) {
|
||||
state := element.state()
|
||||
bounds := element.entity.Bounds()
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, buttonCase)
|
||||
|
||||
pattern.Draw(destination, bounds)
|
||||
|
||||
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,
|
||||
bounds.Dy() / 2).Add(bounds.Min)
|
||||
|
||||
if element.showText {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset.X -= textBounds.Dx() / 2
|
||||
offset.Y -= textBounds.Dy() / 2
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
}
|
||||
|
||||
if element.hasIcon {
|
||||
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase)
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
addedWidth := iconBounds.Dx()
|
||||
iconOffset := offset
|
||||
|
||||
if element.showText {
|
||||
addedWidth += margin.X
|
||||
}
|
||||
|
||||
iconOffset.X -= addedWidth / 2
|
||||
iconOffset.Y =
|
||||
bounds.Min.Y +
|
||||
(bounds.Dy() -
|
||||
iconBounds.Dy()) / 2
|
||||
if element.pressed {
|
||||
iconOffset = iconOffset.Add(sink)
|
||||
}
|
||||
offset.X += addedWidth / 2
|
||||
|
||||
icon.Draw(destination, foreground, iconOffset)
|
||||
}
|
||||
}
|
||||
|
||||
if element.showText {
|
||||
if element.pressed {
|
||||
offset = offset.Add(sink)
|
||||
}
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
}
|
||||
|
||||
// OnClick sets the function to be called when the button is clicked.
|
||||
func (element *Button) OnClick (callback func ()) {
|
||||
element.onClick = callback
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *Button) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this button is enabled or not.
|
||||
func (element *Button) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this button can be clicked or not.
|
||||
func (element *Button) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetText sets the button's label text.
|
||||
func (element *Button) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetIcon sets the icon of the button. Passing theme.IconNone removes the
|
||||
// current icon if it exists.
|
||||
func (element *Button) SetIcon (id tomo.Icon) {
|
||||
if id == tomo.IconNone {
|
||||
element.hasIcon = false
|
||||
} else {
|
||||
if element.hasIcon && element.iconId == id { return }
|
||||
element.hasIcon = true
|
||||
element.iconId = id
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// ShowText sets whether or not the button's text will be displayed.
|
||||
func (element *Button) ShowText (showText bool) {
|
||||
if element.showText == showText { return }
|
||||
element.showText = showText
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
buttonCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
within := position.In(element.entity.Bounds())
|
||||
if element.Enabled() && within && element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.entity.Invalidate()
|
||||
if !element.Enabled() { return }
|
||||
if element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Button) updateMinimumSize () {
|
||||
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.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase)
|
||||
if icon != nil {
|
||||
bounds := icon.Bounds()
|
||||
if element.showText {
|
||||
minimumSize.Max.X += bounds.Dx()
|
||||
minimumSize.Max.X += margin.X
|
||||
} else {
|
||||
minimumSize.Max.X = bounds.Dx()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
minimumSize = padding.Inverse().Apply(minimumSize)
|
||||
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||
}
|
||||
|
||||
func (element *Button) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package elements
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
|
||||
|
||||
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 tomo.Entity
|
||||
child tomo.Element
|
||||
enabled bool
|
||||
|
||||
onSelectionChange func ()
|
||||
}
|
||||
|
||||
// NewCell creates a new cell element. If padding is true, the cell will have
|
||||
// padding on all sides. Child can be nil and added later with the Adopt()
|
||||
// method.
|
||||
func NewCell (child tomo.Element) (element *Cell) {
|
||||
element = &Cell { enabled: true }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.Adopt(child)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Cell) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Cell) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase)
|
||||
if element.child == nil {
|
||||
pattern.Draw(destination, bounds)
|
||||
} else {
|
||||
artutil.DrawShatter (
|
||||
destination, pattern, bounds,
|
||||
element.child.Entity().Bounds())
|
||||
}
|
||||
}
|
||||
|
||||
// Draw causes the element to perform a layout operation.
|
||||
func (element *Cell) Layout () {
|
||||
if element.child == nil { return }
|
||||
|
||||
bounds := element.entity.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 artist.Canvas) {
|
||||
element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase).
|
||||
Draw(destination, element.entity.Bounds())
|
||||
}
|
||||
|
||||
// Adopt sets this element's child. If nil is passed, any child is removed.
|
||||
func (element *Cell) Adopt (child tomo.Element) {
|
||||
if element.child != nil {
|
||||
element.entity.Disown(element.entity.IndexOf(element.child))
|
||||
}
|
||||
if child != nil {
|
||||
element.entity.Adopt(child)
|
||||
}
|
||||
element.child = child
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.invalidateChild()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Child returns this element's child. If there is no child, this method will
|
||||
// return nil.
|
||||
func (element *Cell) Child () tomo.Element {
|
||||
return element.child
|
||||
}
|
||||
|
||||
// Enabled returns whether this cell is enabled or not.
|
||||
func (element *Cell) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this cell can be selected or not.
|
||||
func (element *Cell) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
element.invalidateChild()
|
||||
}
|
||||
|
||||
// OnSelectionChange sets a function to be called when this element is selected
|
||||
// or unselected.
|
||||
func (element *Cell) OnSelectionChange (callback func ()) {
|
||||
element.onSelectionChange = callback
|
||||
}
|
||||
|
||||
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()
|
||||
if element.onSelectionChange != nil {
|
||||
element.onSelectionChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Cell) HandleChildMinimumSizeChange (tomo.Element) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Cell) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.enabled,
|
||||
On: element.entity.Selected(),
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Cell) updateMinimumSize () {
|
||||
width, height := 0, 0
|
||||
|
||||
if element.child != nil {
|
||||
childWidth, childHeight := element.entity.ChildMinimumSize(0)
|
||||
width += childWidth
|
||||
height += childHeight
|
||||
}
|
||||
padding := element.entity.Theme().Padding(tomo.PatternTableCell, cellCase)
|
||||
width += padding.Horizontal()
|
||||
height += padding.Vertical()
|
||||
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *Cell) invalidateChild () {
|
||||
if element.child != nil {
|
||||
element.child.Entity().Invalidate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package elements
|
||||
|
||||
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/textdraw"
|
||||
|
||||
var checkboxCase = tomo.C("tomo", "checkbox")
|
||||
|
||||
// Checkbox is a toggle-able checkbox with a label.
|
||||
type Checkbox struct {
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
checked bool
|
||||
text string
|
||||
|
||||
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.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
checkboxCase))
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Checkbox) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Checkbox) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
||||
|
||||
state := tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
On: element.checked,
|
||||
}
|
||||
|
||||
element.entity.DrawBackground(destination)
|
||||
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, checkboxCase)
|
||||
pattern.Draw(destination, boxBounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase)
|
||||
offset := bounds.Min.Add(image.Point {
|
||||
X: bounds.Dy() + margin.X,
|
||||
})
|
||||
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, checkboxCase)
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
|
||||
// OnToggle sets the function to be called when the checkbox is toggled.
|
||||
func (element *Checkbox) OnToggle (callback func ()) {
|
||||
element.onToggle = callback
|
||||
}
|
||||
|
||||
// Value reports whether or not the checkbox is currently checked.
|
||||
func (element *Checkbox) Value () (checked bool) {
|
||||
return element.checked
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *Checkbox) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this checkbox is enabled or not.
|
||||
func (element *Checkbox) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this checkbox can be toggled or not.
|
||||
func (element *Checkbox) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetText sets the checkbox's label text.
|
||||
func (element *Checkbox) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
checkboxCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft || !element.pressed { return }
|
||||
|
||||
element.pressed = false
|
||||
within := position.In(element.entity.Bounds())
|
||||
if within {
|
||||
element.checked = !element.checked
|
||||
}
|
||||
|
||||
element.entity.Invalidate()
|
||||
if within && element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.checked = !element.checked
|
||||
element.entity.Invalidate()
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Checkbox) updateMinimumSize () {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
if element.text == "" {
|
||||
element.entity.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
|
||||
} else {
|
||||
margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase)
|
||||
element.entity.SetMinimumSize (
|
||||
textBounds.Dy() + margin.X + textBounds.Dx(),
|
||||
textBounds.Dy())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package elements
|
||||
|
||||
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/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
|
||||
|
||||
func (option Option) Title () string {
|
||||
if option == "" {
|
||||
return "(None)"
|
||||
} else {
|
||||
return string(option)
|
||||
}
|
||||
}
|
||||
|
||||
// ComboBox is an input that can be one of several predetermined values.
|
||||
type ComboBox struct {
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
options []Option
|
||||
selected Option
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
|
||||
onChange func ()
|
||||
}
|
||||
|
||||
// NewComboBox creates a new ComboBox with the specifed options.
|
||||
func NewComboBox (options ...Option) (element *ComboBox) {
|
||||
if len(options) == 0 { options = []Option { "" } }
|
||||
element = &ComboBox { enabled: true, options: options }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
comboBoxCase))
|
||||
element.Select(options[0])
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *ComboBox) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *ComboBox) Draw (destination artist.Canvas) {
|
||||
state := element.state()
|
||||
bounds := element.entity.Bounds()
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, comboBoxCase)
|
||||
|
||||
pattern.Draw(destination, bounds)
|
||||
|
||||
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)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset.Y -= textBounds.Dy() / 2
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
icon := element.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase)
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
addedWidth := iconBounds.Dx() + margin.X
|
||||
iconOffset := bounds.Min
|
||||
|
||||
iconOffset.X += padding[3]
|
||||
iconOffset.Y =
|
||||
bounds.Min.Y +
|
||||
(bounds.Dy() -
|
||||
iconBounds.Dy()) / 2
|
||||
if element.pressed {
|
||||
iconOffset = iconOffset.Add(sink)
|
||||
}
|
||||
offset.X += addedWidth + padding[3]
|
||||
|
||||
icon.Draw(destination, foreground, iconOffset)
|
||||
}
|
||||
|
||||
if element.pressed {
|
||||
offset = offset.Add(sink)
|
||||
}
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
|
||||
// OnChange sets the function to be called when this element's value is changed.
|
||||
func (element *ComboBox) OnChange (callback func ()) {
|
||||
element.onChange = callback
|
||||
}
|
||||
|
||||
// Value returns this element's value.
|
||||
func (element *ComboBox) Value () Option {
|
||||
return element.selected
|
||||
}
|
||||
|
||||
// Select sets this element's value.
|
||||
func (element *ComboBox) Select (option Option) {
|
||||
element.selected = option
|
||||
element.drawer.SetText([]rune(option.Title()))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
if element.onChange != nil {
|
||||
element.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Filled returns whether this element has a value other than (None).
|
||||
func (element *ComboBox) Filled () bool {
|
||||
return element.selected != ""
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *ComboBox) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this element is enabled or not.
|
||||
func (element *ComboBox) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this element is enabled or not.
|
||||
func (element *ComboBox) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
comboBoxCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
if button != input.ButtonLeft { return }
|
||||
element.dropDown()
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *ComboBox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
|
||||
selectionDelta := 0
|
||||
switch key {
|
||||
case input.KeyEnter:
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
case input.KeyUp, input.KeyLeft:
|
||||
selectionDelta = -1
|
||||
case input.KeyDown, input.KeyRight:
|
||||
selectionDelta = 1
|
||||
}
|
||||
|
||||
if selectionDelta != 0 {
|
||||
selected := 0
|
||||
for index, option := range element.options {
|
||||
if option == element.selected {
|
||||
selected = index
|
||||
}
|
||||
}
|
||||
selected += selectionDelta
|
||||
if selected < 0 {
|
||||
selected = len(element.options) - 1
|
||||
} else if selected >= len(element.options) {
|
||||
selected = 0
|
||||
}
|
||||
|
||||
element.Select(element.options[selected])
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ComboBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.entity.Invalidate()
|
||||
if !element.Enabled() { return }
|
||||
element.dropDown()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ComboBox) dropDown () {
|
||||
window := element.entity.Window()
|
||||
menu, err := window.NewMenu(element.entity.Bounds())
|
||||
if err != nil { return }
|
||||
|
||||
cellToOption := make(map[ability.Selectable] Option)
|
||||
|
||||
list := NewList()
|
||||
for _, option := range element.options {
|
||||
option := option
|
||||
cell := NewCell(NewLabel(option.Title()))
|
||||
cellToOption[cell] = option
|
||||
list.Adopt(cell)
|
||||
|
||||
if option == element.selected {
|
||||
list.Select(cell)
|
||||
}
|
||||
}
|
||||
list.OnClick(func () {
|
||||
selected := list.Selected()
|
||||
if selected == nil { return }
|
||||
element.Select(cellToOption[selected])
|
||||
menu.Close()
|
||||
})
|
||||
|
||||
menu.Adopt(list)
|
||||
list.Focus()
|
||||
menu.Show()
|
||||
}
|
||||
|
||||
func (element *ComboBox) updateMinimumSize () {
|
||||
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.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase)
|
||||
if icon != nil {
|
||||
bounds := icon.Bounds()
|
||||
minimumSize.Max.X += bounds.Dx()
|
||||
minimumSize.Max.X += margin.X
|
||||
}
|
||||
|
||||
minimumSize = padding.Inverse().Apply(minimumSize)
|
||||
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||
}
|
||||
|
||||
func (element *ComboBox) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package elements
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
type scratchEntry struct {
|
||||
expand bool
|
||||
minSize float64
|
||||
minBreadth float64
|
||||
}
|
||||
|
||||
type container struct {
|
||||
entity tomo.Entity
|
||||
scratch map[tomo.Element] scratchEntry
|
||||
minimumSize func ()
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (container *container) Entity () tomo.Entity {
|
||||
return container.entity
|
||||
}
|
||||
|
||||
// Adopt adds one or more elements to the container.
|
||||
func (container *container) Adopt (children ...tomo.Element) {
|
||||
container.adopt(false, children...)
|
||||
}
|
||||
|
||||
func (container *container) init () {
|
||||
container.scratch = make(map[tomo.Element] scratchEntry)
|
||||
}
|
||||
|
||||
func (container *container) adopt (expand bool, children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
container.entity.Adopt(child)
|
||||
container.scratch[child] = scratchEntry { expand: expand }
|
||||
}
|
||||
container.minimumSize()
|
||||
container.entity.Invalidate()
|
||||
container.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Disown removes one or more elements from the container.
|
||||
func (container *container) Disown (children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
index := container.entity.IndexOf(child)
|
||||
if index < 0 { continue }
|
||||
container.entity.Disown(index)
|
||||
delete(container.scratch, child)
|
||||
}
|
||||
container.minimumSize()
|
||||
container.entity.Invalidate()
|
||||
container.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// DisownAll removes all elements from the container.
|
||||
func (container *container) DisownAll () {
|
||||
func () {
|
||||
for index := 0; index < container.entity.CountChildren(); index ++ {
|
||||
index := index
|
||||
defer container.entity.Disown(index)
|
||||
}
|
||||
} ()
|
||||
container.scratch = make(map[tomo.Element] scratchEntry)
|
||||
container.minimumSize()
|
||||
container.entity.Invalidate()
|
||||
container.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Child returns the child at the specified index.
|
||||
func (container *container) Child (index int) tomo.Element {
|
||||
if index < 0 || index >= container.entity.CountChildren() { return nil }
|
||||
return container.entity.Child(index)
|
||||
}
|
||||
|
||||
// CountChildren returns the amount of children in this container.
|
||||
func (container *container) CountChildren () int {
|
||||
return container.entity.CountChildren()
|
||||
}
|
||||
|
||||
func (container *container) HandleChildMinimumSizeChange (child tomo.Element) {
|
||||
container.minimumSize()
|
||||
container.entity.Invalidate()
|
||||
container.entity.InvalidateLayout()
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package core
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
|
||||
// Core is a struct that implements some core functionality common to most
|
||||
// widgets. It is meant to be embedded directly into a struct.
|
||||
type Core struct {
|
||||
canvas canvas.Canvas
|
||||
|
||||
metrics struct {
|
||||
minimumWidth int
|
||||
minimumHeight int
|
||||
}
|
||||
|
||||
drawSizeChange func ()
|
||||
onMinimumSizeChange func ()
|
||||
onDamage func (region canvas.Canvas)
|
||||
}
|
||||
|
||||
// NewCore creates a new element core and its corresponding control.
|
||||
func NewCore (
|
||||
drawSizeChange func (),
|
||||
) (
|
||||
core *Core,
|
||||
control CoreControl,
|
||||
) {
|
||||
core = &Core {
|
||||
drawSizeChange: drawSizeChange,
|
||||
}
|
||||
control = CoreControl { core: core }
|
||||
return
|
||||
}
|
||||
|
||||
// Bounds fulfills the tomo.Element interface. This should not need to be
|
||||
// overridden.
|
||||
func (core *Core) Bounds () (bounds image.Rectangle) {
|
||||
if core.canvas == nil { return }
|
||||
return core.canvas.Bounds()
|
||||
}
|
||||
|
||||
// MinimumSize fulfils the tomo.Element interface. This should not need to be
|
||||
// overridden.
|
||||
func (core *Core) MinimumSize () (width, height int) {
|
||||
return core.metrics.minimumWidth, core.metrics.minimumHeight
|
||||
}
|
||||
|
||||
// DrawTo fulfills the tomo.Element interface. This should not need to be
|
||||
// overridden.
|
||||
func (core *Core) DrawTo (canvas canvas.Canvas) {
|
||||
core.canvas = canvas
|
||||
if core.drawSizeChange != nil {
|
||||
core.drawSizeChange()
|
||||
}
|
||||
}
|
||||
|
||||
// OnDamage fulfils the tomo.Element interface. This should not need to be
|
||||
// overridden.
|
||||
func (core *Core) OnDamage (callback func (region canvas.Canvas)) {
|
||||
core.onDamage = callback
|
||||
}
|
||||
|
||||
// OnMinimumSizeChange fulfils the tomo.Element interface. This should not need
|
||||
// to be overridden.
|
||||
func (core *Core) OnMinimumSizeChange (callback func ()) {
|
||||
core.onMinimumSizeChange = callback
|
||||
}
|
||||
|
||||
// CoreControl is a struct that can exert control over a Core struct. It can be
|
||||
// used as a canvas. It must not be directly embedded into an element, but
|
||||
// instead kept as a private member. When a Core struct is created, a
|
||||
// corresponding CoreControl struct is linked to it and returned alongside it.
|
||||
type CoreControl struct {
|
||||
core *Core
|
||||
}
|
||||
|
||||
// ColorModel fulfills the draw.Image interface.
|
||||
func (control CoreControl) ColorModel () (model color.Model) {
|
||||
return color.RGBAModel
|
||||
}
|
||||
|
||||
// At fulfills the draw.Image interface.
|
||||
func (control CoreControl) At (x, y int) (pixel color.Color) {
|
||||
if control.core.canvas == nil { return }
|
||||
return control.core.canvas.At(x, y)
|
||||
}
|
||||
|
||||
// Bounds fulfills the draw.Image interface.
|
||||
func (control CoreControl) Bounds () (bounds image.Rectangle) {
|
||||
if control.core.canvas == nil { return }
|
||||
return control.core.canvas.Bounds()
|
||||
}
|
||||
|
||||
// Set fulfills the draw.Image interface.
|
||||
func (control CoreControl) Set (x, y int, c color.Color) () {
|
||||
if control.core.canvas == nil { return }
|
||||
control.core.canvas.Set(x, y, c)
|
||||
}
|
||||
|
||||
// Buffer fulfills the canvas.Canvas interface.
|
||||
func (control CoreControl) Buffer () (data []color.RGBA, stride int) {
|
||||
if control.core.canvas == nil { return }
|
||||
return control.core.canvas.Buffer()
|
||||
}
|
||||
|
||||
// HasImage returns true if the core has an allocated image buffer, and false if
|
||||
// it doesn't.
|
||||
func (control CoreControl) HasImage () (has bool) {
|
||||
return control.core.canvas != nil && !control.core.canvas.Bounds().Empty()
|
||||
}
|
||||
|
||||
// DamageRegion pushes the selected region of pixels to the parent element. This
|
||||
// does not need to be called when responding to a resize event.
|
||||
func (control CoreControl) DamageRegion (regions ...image.Rectangle) {
|
||||
if control.core.canvas == nil { return }
|
||||
if control.core.onDamage != nil {
|
||||
for _, region := range regions {
|
||||
control.core.onDamage (
|
||||
canvas.Cut(control.core.canvas, region))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DamageAll pushes all pixels to the parent element. This does not need to be
|
||||
// called when redrawing in response to a change in size.
|
||||
func (control CoreControl) DamageAll () {
|
||||
control.DamageRegion(control.core.Bounds())
|
||||
}
|
||||
|
||||
// SetMinimumSize sets the minimum size of this element, notifying the parent
|
||||
// element in the process.
|
||||
func (control CoreControl) SetMinimumSize (width, height int) {
|
||||
core := control.core
|
||||
if width == core.metrics.minimumWidth &&
|
||||
height == core.metrics.minimumHeight {
|
||||
return
|
||||
}
|
||||
|
||||
core.metrics.minimumWidth = width
|
||||
core.metrics.minimumHeight = height
|
||||
if control.core.onMinimumSizeChange != nil {
|
||||
control.core.onMinimumSizeChange()
|
||||
}
|
||||
}
|
||||
|
||||
// ConstrainSize contstrains the specified width and height to the minimum width
|
||||
// and height, and returns wether or not anything ended up being constrained.
|
||||
func (control CoreControl) ConstrainSize (
|
||||
inWidth, inHeight int,
|
||||
) (
|
||||
outWidth, outHeight int,
|
||||
constrained bool,
|
||||
) {
|
||||
core := control.core
|
||||
outWidth = inWidth
|
||||
outHeight = inHeight
|
||||
if outWidth < core.metrics.minimumWidth {
|
||||
outWidth = core.metrics.minimumWidth
|
||||
constrained = true
|
||||
}
|
||||
if outHeight < core.metrics.minimumHeight {
|
||||
outHeight = core.metrics.minimumHeight
|
||||
constrained = true
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Package core provides tools that allow elements to easily fulfill common
|
||||
// interfaces without having to duplicate a ton of code. Each "core" is a type
|
||||
// that can be embedded into an element directly, working to fulfill a
|
||||
// particular interface. Each one comes with a corresponding core control, which
|
||||
// provides an interface for elements to exert control over the core. Core
|
||||
// controls should be kept private.
|
||||
package core
|
||||
@@ -1,118 +0,0 @@
|
||||
package core
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
|
||||
// FocusableCore is a struct that can be embedded into objects to make them
|
||||
// focusable, giving them the default keynav behavior.
|
||||
type FocusableCore struct {
|
||||
focused bool
|
||||
enabled bool
|
||||
drawFocusChange func ()
|
||||
onFocusRequest func () (granted bool)
|
||||
onFocusMotionRequest func(input.KeynavDirection) (granted bool)
|
||||
}
|
||||
|
||||
// NewFocusableCore creates a new focusability core and its corresponding
|
||||
// control. If your element needs to visually update itself when it's focus
|
||||
// state changes (which it should), a callback to draw and push the update can
|
||||
// be specified.
|
||||
func NewFocusableCore (
|
||||
drawFocusChange func (),
|
||||
) (
|
||||
core *FocusableCore,
|
||||
control FocusableCoreControl,
|
||||
) {
|
||||
core = &FocusableCore {
|
||||
drawFocusChange: drawFocusChange,
|
||||
enabled: true,
|
||||
}
|
||||
control = FocusableCoreControl { core: core }
|
||||
return
|
||||
}
|
||||
|
||||
// Focused returns whether or not this element is currently focused.
|
||||
func (core *FocusableCore) Focused () (focused bool) {
|
||||
return core.focused
|
||||
}
|
||||
|
||||
// Focus focuses this element, if its parent element grants the request.
|
||||
func (core *FocusableCore) Focus () {
|
||||
if !core.enabled || core.focused { return }
|
||||
if core.onFocusRequest != nil {
|
||||
if core.onFocusRequest() {
|
||||
core.focused = true
|
||||
if core.drawFocusChange != nil {
|
||||
core.drawFocusChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleFocus causes this element to mark itself as focused, if it can
|
||||
// currently be. Otherwise, it will return false and do nothing.
|
||||
func (core *FocusableCore) HandleFocus (
|
||||
direction input.KeynavDirection,
|
||||
) (
|
||||
accepted bool,
|
||||
) {
|
||||
direction = direction.Canon()
|
||||
if !core.enabled { return false }
|
||||
if core.focused && direction != input.KeynavDirectionNeutral {
|
||||
return false
|
||||
}
|
||||
|
||||
if core.focused == false {
|
||||
core.focused = true
|
||||
if core.drawFocusChange != nil { core.drawFocusChange() }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HandleUnfocus causes this element to mark itself as unfocused.
|
||||
func (core *FocusableCore) HandleUnfocus () {
|
||||
core.focused = false
|
||||
if core.drawFocusChange != nil { core.drawFocusChange() }
|
||||
}
|
||||
|
||||
// OnFocusRequest sets a function to be called when this element
|
||||
// wants its parent element to focus it. Parent elements should return
|
||||
// true if the request was granted, and false if it was not.
|
||||
func (core *FocusableCore) OnFocusRequest (callback func () (granted bool)) {
|
||||
core.onFocusRequest = callback
|
||||
}
|
||||
|
||||
// OnFocusMotionRequest sets a function to be called when this
|
||||
// element wants its parent element to focus the element behind or in
|
||||
// front of it, depending on the specified direction. Parent elements
|
||||
// should return true if the request was granted, and false if it was
|
||||
// not.
|
||||
func (core *FocusableCore) OnFocusMotionRequest (
|
||||
callback func (direction input.KeynavDirection) (granted bool),
|
||||
) {
|
||||
core.onFocusMotionRequest = callback
|
||||
}
|
||||
|
||||
// Enabled returns whether or not the element is enabled.
|
||||
func (core *FocusableCore) Enabled () (enabled bool) {
|
||||
return core.enabled
|
||||
}
|
||||
|
||||
// FocusableCoreControl is a struct that can be used to exert control over a
|
||||
// focusability core. It must not be directly embedded into an element, but
|
||||
// instead kept as a private member. When a FocusableCore struct is created, a
|
||||
// corresponding FocusableCoreControl struct is linked to it and returned
|
||||
// alongside it.
|
||||
type FocusableCoreControl struct {
|
||||
core *FocusableCore
|
||||
}
|
||||
|
||||
// SetEnabled sets whether the focusability core is enabled. If the state
|
||||
// changes, this will call drawFocusChange.
|
||||
func (control FocusableCoreControl) SetEnabled (enabled bool) {
|
||||
if control.core.enabled == enabled { return }
|
||||
control.core.enabled = enabled
|
||||
if !enabled { control.core.focused = false }
|
||||
if control.core.drawFocusChange != nil {
|
||||
control.core.drawFocusChange()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "path/filepath"
|
||||
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/ability"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||
|
||||
// TODO: base on flow implementation of list. also be able to switch to a table
|
||||
// variant for a more information dense view.
|
||||
|
||||
var directoryCase = tomo.C("tomo", "list")
|
||||
|
||||
type historyEntry struct {
|
||||
location string
|
||||
filesystem ReadDirStatFS
|
||||
}
|
||||
|
||||
// Directory displays a list of files within a particular directory and
|
||||
// file system.
|
||||
type Directory struct {
|
||||
container
|
||||
entity tomo.Entity
|
||||
|
||||
scroll image.Point
|
||||
contentBounds image.Rectangle
|
||||
|
||||
history []historyEntry
|
||||
historyIndex int
|
||||
|
||||
onChoose func (file string)
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
// NewDirectory creates a new directory view. If within is nil, it will use
|
||||
// the OS file system.
|
||||
func NewDirectory (
|
||||
location string,
|
||||
within ReadDirStatFS,
|
||||
) (
|
||||
element *Directory,
|
||||
err error,
|
||||
) {
|
||||
element = &Directory { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.container.entity = element.entity
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init()
|
||||
err = element.SetLocation(location, within)
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
|
||||
for _, tile := range tiles {
|
||||
element.DrawBackground(artist.Cut(destination, tile))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Directory) Layout () {
|
||||
if element.scroll.Y > element.maxScrollHeight() {
|
||||
element.scroll.Y = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
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 { }
|
||||
|
||||
dot := bounds.Min.Sub(element.scroll)
|
||||
xStart := dot.X
|
||||
rowHeight := 0
|
||||
|
||||
nextLine := func () {
|
||||
dot.X = xStart
|
||||
dot.Y += margin.Y
|
||||
dot.Y += rowHeight
|
||||
rowHeight = 0
|
||||
}
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
width := int(entry.minBreadth)
|
||||
height := int(entry.minSize)
|
||||
if width + dot.X > bounds.Max.X {
|
||||
nextLine()
|
||||
}
|
||||
if typedChild, ok := child.(ability.Flexible); ok {
|
||||
height = typedChild.FlexibleHeightFor(width)
|
||||
}
|
||||
if rowHeight < height {
|
||||
rowHeight = height
|
||||
}
|
||||
|
||||
childBounds := tomo.Bounds (
|
||||
dot.X, dot.Y,
|
||||
width, height)
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||
|
||||
dot.X += width + margin.X
|
||||
}
|
||||
|
||||
element.contentBounds =
|
||||
element.contentBounds.Sub(element.contentBounds.Min)
|
||||
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Directory) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
element.selectNone()
|
||||
}
|
||||
|
||||
func (element *Directory) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *Directory) HandleChildMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element,
|
||||
) {
|
||||
element.selectNone()
|
||||
if child, ok := child.(ability.Selectable); ok {
|
||||
index := element.entity.IndexOf(child)
|
||||
element.entity.SelectChild(index, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Directory) HandleChildMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element,
|
||||
) { }
|
||||
|
||||
func (element *Directory) HandleChildFlexibleHeightChange (child ability.Flexible) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *Directory) ScrollContentBounds () image.Rectangle {
|
||||
return element.contentBounds
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
func (element *Directory) ScrollViewportBounds () image.Rectangle {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
|
||||
return bounds
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *Directory) ScrollTo (position image.Point) {
|
||||
if position.Y < 0 {
|
||||
position.Y = 0
|
||||
}
|
||||
maxScrollHeight := element.maxScrollHeight()
|
||||
if position.Y > maxScrollHeight {
|
||||
position.Y = maxScrollHeight
|
||||
}
|
||||
element.scroll = position
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||
// bounds, content bounds, or scroll axes change.
|
||||
func (element *Directory) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *Directory) ScrollAxes () (horizontal, vertical bool) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (element *Directory) DrawBackground (destination artist.Canvas) {
|
||||
element.entity.Theme().Pattern(tomo.PatternPinboard, tomo.State { }, directoryCase).
|
||||
Draw(destination, element.entity.Bounds())
|
||||
}
|
||||
|
||||
func (element *Directory) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Location returns the directory's location and filesystem.
|
||||
func (element *Directory) Location () (string, ReadDirStatFS) {
|
||||
if len(element.history) < 1 { return "", nil }
|
||||
current := element.history[element.historyIndex]
|
||||
return current.location, current.filesystem
|
||||
}
|
||||
|
||||
// SetLocation sets the directory's location and filesystem. If within is nil,
|
||||
// it will use the OS file system.
|
||||
func (element *Directory) SetLocation (
|
||||
location string,
|
||||
within ReadDirStatFS,
|
||||
) error {
|
||||
if within == nil {
|
||||
within = defaultFS { }
|
||||
}
|
||||
element.scroll = image.Point { }
|
||||
|
||||
if element.history != nil {
|
||||
element.historyIndex ++
|
||||
}
|
||||
element.history = append (
|
||||
element.history[:element.historyIndex],
|
||||
historyEntry { location, within })
|
||||
return element.Update()
|
||||
}
|
||||
|
||||
// Backward goes back a directory in history
|
||||
func (element *Directory) Backward () (bool, error) {
|
||||
if element.historyIndex > 1 {
|
||||
element.historyIndex --
|
||||
return true, element.Update()
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Forward goes forward a directory in history
|
||||
func (element *Directory) Forward () (bool, error) {
|
||||
if element.historyIndex < len(element.history) - 1 {
|
||||
element.historyIndex ++
|
||||
return true, element.Update()
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Update refreshes the directory's contents.
|
||||
func (element *Directory) Update () error {
|
||||
location, filesystem := element.Location()
|
||||
entries, err := filesystem.ReadDir(location)
|
||||
|
||||
children := make([]tomo.Element, len(entries))
|
||||
for index, entry := range entries {
|
||||
filePath := filepath.Join(location, entry.Name())
|
||||
file, _ := NewFile(filePath, filesystem)
|
||||
file.OnChoose (func () {
|
||||
if element.onChoose != nil {
|
||||
element.onChoose(filePath)
|
||||
}
|
||||
})
|
||||
|
||||
children[index] = file
|
||||
}
|
||||
|
||||
element.DisownAll()
|
||||
element.Adopt(children...)
|
||||
return err
|
||||
}
|
||||
|
||||
// OnChoose sets a function to be called when the user double-clicks a file or
|
||||
// sub-directory within the directory view.
|
||||
func (element *Directory) OnChoose (callback func (file string)) {
|
||||
element.onChoose = callback
|
||||
}
|
||||
|
||||
func (element *Directory) selectNone () {
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
element.entity.SelectChild(index, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Directory) maxScrollHeight () (height int) {
|
||||
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 }
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func (element *Directory) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
|
||||
minimumWidth := 0
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
width, height := element.entity.ChildMinimumSize(index)
|
||||
if width > minimumWidth {
|
||||
minimumWidth = width
|
||||
}
|
||||
|
||||
key := element.entity.Child(index)
|
||||
entry := element.scratch[key]
|
||||
entry.minSize = float64(height)
|
||||
entry.minBreadth = float64(width)
|
||||
element.scratch[key] = entry
|
||||
}
|
||||
element.entity.SetMinimumSize (
|
||||
minimumWidth + padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
}
|
||||
+2
-5
@@ -1,6 +1,3 @@
|
||||
// Package elements provides several standard interfaces that elements can
|
||||
// fulfill in order to inform other elements of their capabilities and what
|
||||
// events they are able to process. Sub-packages of this package provide
|
||||
// pre-made standard elements, as well as tools that can be used to easily
|
||||
// create more.
|
||||
// Package elements provides standard elements that are commonly used in GUI
|
||||
// applications.
|
||||
package elements
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/ability"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||
|
||||
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 tomo.Entity
|
||||
|
||||
scroll image.Point
|
||||
contentBounds image.Rectangle
|
||||
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
// NewDocument creates a new document container.
|
||||
func NewDocument (children ...tomo.Element) (element *Document) {
|
||||
element = &Document { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.container.entity = element.entity
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init()
|
||||
element.Adopt(children...)
|
||||
return
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination 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()
|
||||
}
|
||||
|
||||
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
|
||||
for _, tile := range tiles {
|
||||
element.entity.DrawBackground(artist.Cut(destination, tile))
|
||||
}
|
||||
}
|
||||
|
||||
// Layout causes this element to perform a layout operation.
|
||||
func (element *Document) Layout () {
|
||||
if element.scroll.Y > element.maxScrollHeight() {
|
||||
element.scroll.Y = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
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 { }
|
||||
|
||||
dot := bounds.Min.Sub(element.scroll)
|
||||
xStart := dot.X
|
||||
rowHeight := 0
|
||||
|
||||
nextLine := func () {
|
||||
dot.X = xStart
|
||||
dot.Y += margin.Y
|
||||
dot.Y += rowHeight
|
||||
rowHeight = 0
|
||||
}
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
if dot.X > xStart && entry.expand {
|
||||
nextLine()
|
||||
}
|
||||
|
||||
width := int(entry.minBreadth)
|
||||
height := int(entry.minSize)
|
||||
if width + dot.X > bounds.Max.X && !entry.expand {
|
||||
nextLine()
|
||||
}
|
||||
if width < bounds.Dx() && entry.expand {
|
||||
width = bounds.Dx()
|
||||
}
|
||||
if typedChild, ok := child.(ability.Flexible); ok {
|
||||
height = typedChild.FlexibleHeightFor(width)
|
||||
}
|
||||
if rowHeight < height {
|
||||
rowHeight = height
|
||||
}
|
||||
|
||||
childBounds := tomo.Bounds (
|
||||
dot.X, dot.Y,
|
||||
width, height)
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||
|
||||
if entry.expand {
|
||||
nextLine()
|
||||
} else {
|
||||
dot.X += width + margin.X
|
||||
}
|
||||
}
|
||||
|
||||
element.contentBounds =
|
||||
element.contentBounds.Sub(element.contentBounds.Min)
|
||||
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Adopt adds one or more elements to the container, placing each on its own
|
||||
// line.
|
||||
func (element *Document) Adopt (children ...tomo.Element) {
|
||||
element.adopt(true, children...)
|
||||
}
|
||||
|
||||
// AdoptInline adds one or more elements to the container, packing multiple
|
||||
// elements onto the same line(s).
|
||||
func (element *Document) AdoptInline (children ...tomo.Element) {
|
||||
element.adopt(false, children...)
|
||||
}
|
||||
|
||||
func (element *Document) HandleChildFlexibleHeightChange (child ability.Flexible) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// DrawBackground draws this element's background pattern to the specified
|
||||
// destination canvas.
|
||||
func (element *Document) DrawBackground (destination artist.Canvas) {
|
||||
element.entity.DrawBackground(destination)
|
||||
}
|
||||
|
||||
func (element *Document) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *Document) ScrollContentBounds () image.Rectangle {
|
||||
return element.contentBounds
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
func (element *Document) ScrollViewportBounds () image.Rectangle {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
|
||||
return bounds
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *Document) ScrollTo (position image.Point) {
|
||||
if position.Y < 0 {
|
||||
position.Y = 0
|
||||
}
|
||||
maxScrollHeight := element.maxScrollHeight()
|
||||
if position.Y > maxScrollHeight {
|
||||
position.Y = maxScrollHeight
|
||||
}
|
||||
element.scroll = position
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||
// bounds, content bounds, or scroll axes change.
|
||||
func (element *Document) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *Document) ScrollAxes () (horizontal, vertical bool) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (element *Document) maxScrollHeight () (height int) {
|
||||
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 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Document) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
|
||||
minimumWidth := 0
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
width, height := element.entity.ChildMinimumSize(index)
|
||||
if width > minimumWidth {
|
||||
minimumWidth = width
|
||||
}
|
||||
|
||||
key := element.entity.Child(index)
|
||||
entry := element.scratch[key]
|
||||
entry.minSize = float64(height)
|
||||
entry.minBreadth = float64(width)
|
||||
element.scratch[key] = entry
|
||||
}
|
||||
element.entity.SetMinimumSize (
|
||||
minimumWidth + padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
|
||||
// Element represents a basic on-screen object.
|
||||
type Element interface {
|
||||
// Bounds reports the element's bounding box. This must reflect the
|
||||
// bounding box of the last canvas given to the element by DrawTo.
|
||||
Bounds () (bounds image.Rectangle)
|
||||
|
||||
// DrawTo sets this element's canvas. This should only be called by the
|
||||
// parent element. This is typically a region of the parent element's
|
||||
// canvas.
|
||||
DrawTo (canvas canvas.Canvas)
|
||||
|
||||
// OnDamage sets a function to be called when an area of the element is
|
||||
// drawn on and should be pushed to the screen.
|
||||
OnDamage (callback func (region canvas.Canvas))
|
||||
|
||||
// MinimumSize specifies the minimum amount of pixels this element's
|
||||
// width and height may be set to. If the element is given a resize
|
||||
// event with dimensions smaller than this, it will use its minimum
|
||||
// instead of the offending dimension(s).
|
||||
MinimumSize () (width, height int)
|
||||
|
||||
// OnMinimumSizeChange sets a function to be called when the element's
|
||||
// minimum size is changed.
|
||||
OnMinimumSizeChange (callback func ())
|
||||
}
|
||||
|
||||
// Focusable represents an element that has keyboard navigation support. This
|
||||
// includes inputs, buttons, sliders, etc. as well as any elements that have
|
||||
// children (so keyboard navigation events can be propagated downward).
|
||||
type Focusable interface {
|
||||
Element
|
||||
|
||||
// Focused returns whether or not this element or any of its children
|
||||
// are currently focused.
|
||||
Focused () (selected bool)
|
||||
|
||||
// Focus focuses this element, if its parent element grants the
|
||||
// request.
|
||||
Focus ()
|
||||
|
||||
// HandleFocus causes this element to mark itself as focused. If the
|
||||
// element does not have children, it is disabled, or there are no more
|
||||
// selectable children in the given direction, it should return false
|
||||
// and do nothing. Otherwise, it should select itself and any children
|
||||
// (if applicable) and return true.
|
||||
HandleFocus (direction input.KeynavDirection) (accepted bool)
|
||||
|
||||
// HandleDeselection causes this element to mark itself and all of its
|
||||
// children as unfocused.
|
||||
HandleUnfocus ()
|
||||
|
||||
// OnFocusRequest sets a function to be called when this element wants
|
||||
// its parent element to focus it. Parent elements should return true if
|
||||
// the request was granted, and false if it was not. If the parent
|
||||
// element returns true, the element must act as if a HandleFocus call
|
||||
// was made with KeynavDirectionNeutral.
|
||||
OnFocusRequest (func () (granted bool))
|
||||
|
||||
// OnFocusMotionRequest sets a function to be called when this
|
||||
// element wants its parent element to focus the element behind or in
|
||||
// front of it, depending on the specified direction. Parent elements
|
||||
// should return true if the request was granted, and false if it was
|
||||
// not.
|
||||
OnFocusMotionRequest (func (direction input.KeynavDirection) (granted bool))
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Each of these handler methods is passed the position of the mouse
|
||||
// cursor at the time of the event as x, y.
|
||||
|
||||
// HandleMouseDown is called when a mouse button is pressed down on this
|
||||
// element.
|
||||
HandleMouseDown (x, y int, button input.Button)
|
||||
|
||||
// HandleMouseUp is called when a mouse button is released that was
|
||||
// originally pressed down on this element.
|
||||
HandleMouseUp (x, y int, button input.Button)
|
||||
|
||||
// HandleMouseMove 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.
|
||||
HandleMouseMove (x, y int)
|
||||
|
||||
// HandleScroll is called when the mouse is scrolled. The X and Y
|
||||
// direction of the scroll event are passed as deltaX and deltaY.
|
||||
HandleMouseScroll (x, y int, deltaX, deltaY float64)
|
||||
}
|
||||
|
||||
// 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 flexible.
|
||||
FlexibleHeightFor (width int) (height int)
|
||||
|
||||
// OnFlexibleHeightChange sets a function to be called when the
|
||||
// parameters affecting this element's flexible height are changed.
|
||||
OnFlexibleHeightChange (callback func ())
|
||||
}
|
||||
|
||||
// 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 () (bounds image.Rectangle)
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
ScrollViewportBounds () (bounds 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)
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's
|
||||
// ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed.
|
||||
OnScrollBoundsChange (callback func ())
|
||||
}
|
||||
|
||||
// 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.Theme)
|
||||
}
|
||||
|
||||
// Configurable represents an element that can modify its behavior to fit within
|
||||
// a set of configuration parameters.
|
||||
type Configurable interface {
|
||||
Element
|
||||
|
||||
// SetConfig sets the element's configuration to something fulfilling
|
||||
// the config.Config interface.
|
||||
SetConfig (config.Config)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package elements
|
||||
|
||||
import "time"
|
||||
import "io/fs"
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
var fileCase = tomo.C("files", "file")
|
||||
|
||||
// File displays an interactive visual representation of a file within any
|
||||
// file system.
|
||||
type File struct {
|
||||
entity tomo.Entity
|
||||
|
||||
lastClick time.Time
|
||||
pressed bool
|
||||
enabled bool
|
||||
iconID tomo.Icon
|
||||
filesystem fs.StatFS
|
||||
location string
|
||||
|
||||
onChoose func ()
|
||||
}
|
||||
|
||||
// NewFile creates a new file element. If within is nil, it will use the OS file
|
||||
// system
|
||||
func NewFile (
|
||||
location string,
|
||||
within fs.StatFS,
|
||||
) (
|
||||
element *File,
|
||||
err error,
|
||||
) {
|
||||
element = &File { enabled: true }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
err = element.SetLocation(location, within)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *File) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *File) Draw (destination artist.Canvas) {
|
||||
// background
|
||||
state := element.state()
|
||||
bounds := element.entity.Bounds()
|
||||
sink := element.entity.Theme().Sink(tomo.PatternButton, fileCase)
|
||||
element.entity.Theme().
|
||||
Pattern(tomo.PatternButton, state, fileCase).
|
||||
Draw(destination, bounds)
|
||||
|
||||
// icon
|
||||
icon := element.icon()
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
offset := image.Pt (
|
||||
(bounds.Dx() - iconBounds.Dx()) / 2,
|
||||
(bounds.Dy() - iconBounds.Dy()) / 2)
|
||||
if element.pressed {
|
||||
offset = offset.Add(sink)
|
||||
}
|
||||
icon.Draw (
|
||||
destination,
|
||||
element.entity.Theme().Color(tomo.ColorForeground, state, fileCase),
|
||||
bounds.Min.Add(offset))
|
||||
}
|
||||
}
|
||||
// Location returns the file's location and filesystem.
|
||||
func (element *File) Location () (string, fs.StatFS) {
|
||||
return element.location, element.filesystem
|
||||
}
|
||||
|
||||
// SetLocation sets the file's location and filesystem. If within is nil, it
|
||||
// will use the OS file system.
|
||||
func (element *File) SetLocation (
|
||||
location string,
|
||||
within fs.StatFS,
|
||||
) error {
|
||||
if within == nil {
|
||||
within = defaultFS { }
|
||||
}
|
||||
element.location = location
|
||||
element.filesystem = within
|
||||
return element.Update()
|
||||
}
|
||||
|
||||
// Update refreshes the element to match the file it represents.
|
||||
func (element *File) Update () error {
|
||||
info, err := element.filesystem.Stat(element.location)
|
||||
|
||||
if err != nil {
|
||||
element.iconID = tomo.IconError
|
||||
} else if info.IsDir() {
|
||||
element.iconID = tomo.IconDirectory
|
||||
} else {
|
||||
// TODO: choose icon based on file mime type
|
||||
element.iconID = tomo.IconFile
|
||||
}
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
return err
|
||||
}
|
||||
|
||||
func (element *File) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *File) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
if !element.Enabled() { return }
|
||||
element.entity.Invalidate()
|
||||
if element.onChoose != nil {
|
||||
element.onChoose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *File) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) HandleSelectionChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) OnChoose (callback func ()) {
|
||||
element.onChoose = callback
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *File) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this file is enabled or not.
|
||||
func (element *File) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this file is enabled or not.
|
||||
func (element *File) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.entity.Focused() { element.Focus() }
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
within := position.In(element.entity.Bounds())
|
||||
if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() {
|
||||
if element.Enabled() && within && element.onChoose != nil {
|
||||
element.onChoose()
|
||||
}
|
||||
} else {
|
||||
element.lastClick = time.Now()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *File) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
On: element.entity.Selected(),
|
||||
}
|
||||
}
|
||||
|
||||
func (element *File) icon () artist.Icon {
|
||||
return element.entity.Theme().Icon(element.iconID, tomo.IconSizeLarge, fileCase)
|
||||
}
|
||||
|
||||
func (element *File) updateMinimumSize () {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternButton, fileCase)
|
||||
icon := element.icon()
|
||||
if icon == nil {
|
||||
element.entity.SetMinimumSize (
|
||||
padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
} else {
|
||||
bounds := padding.Inverse().Apply(icon.Bounds())
|
||||
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package elements
|
||||
|
||||
import "os"
|
||||
import "io/fs"
|
||||
|
||||
// ReadDirStatFS is a combination of fs.ReadDirFS and fs.StatFS. It is the
|
||||
// minimum filesystem needed to satisfy a directory view.
|
||||
type ReadDirStatFS interface {
|
||||
fs.ReadDirFS
|
||||
fs.StatFS
|
||||
}
|
||||
|
||||
type defaultFS struct { }
|
||||
|
||||
func (defaultFS) Open (name string) (fs.File, error) {
|
||||
return os.Open(name)
|
||||
}
|
||||
|
||||
func (defaultFS) ReadDir (name string) ([]fs.DirEntry, error) {
|
||||
return os.ReadDir(name)
|
||||
}
|
||||
|
||||
func (defaultFS) Stat (name string) (fs.FileInfo, error) {
|
||||
return os.Stat(name)
|
||||
}
|
||||
+37
-59
@@ -4,73 +4,48 @@ import "time"
|
||||
import "math"
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
var clockCase = tomo.C("tomo", "clock")
|
||||
|
||||
// AnalogClock can display the time of day in an analog format.
|
||||
type AnalogClock struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
time time.Time
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
entity tomo.Entity
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// NewAnalogClock creates a new analog clock that displays the specified time.
|
||||
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
|
||||
element = &AnalogClock { }
|
||||
element.theme.Case = theme.C("fun", "clock")
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.core.SetMinimumSize(64, 64)
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.entity.SetMinimumSize(64, 64)
|
||||
return
|
||||
}
|
||||
|
||||
// SetTime changes the time that the clock displays.
|
||||
func (element *AnalogClock) SetTime (newTime time.Time) {
|
||||
if newTime == element.time { return }
|
||||
element.time = newTime
|
||||
element.redo()
|
||||
// Entity returns this element's entity.
|
||||
func (element *AnalogClock) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *AnalogClock) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
element.redo()
|
||||
}
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *AnalogClock) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *AnalogClock) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *AnalogClock) redo () {
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *AnalogClock) draw () {
|
||||
bounds := element.Bounds()
|
||||
|
||||
state := theme.State { }
|
||||
pattern := element.theme.Pattern(theme.PatternSunken, state)
|
||||
padding := element.theme.Padding(theme.PatternSunken)
|
||||
pattern.Draw(element.core, bounds)
|
||||
state := tomo.State { }
|
||||
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(theme.ColorForeground, state)
|
||||
accent := element.theme.Color(theme.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 (
|
||||
destination,
|
||||
foreground,
|
||||
0.8, 0.9, float64(hour) / 6 * math.Pi)
|
||||
}
|
||||
@@ -79,34 +54,37 @@ func (element *AnalogClock) draw () {
|
||||
minute := float64(element.time.Minute()) + second / 60
|
||||
hour := float64(element.time.Hour()) + minute / 60
|
||||
|
||||
element.radialLine(foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
|
||||
element.radialLine(foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
|
||||
element.radialLine(accent, 0, 0.7, (second - 15) / 30 * math.Pi)
|
||||
element.radialLine(destination, foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
|
||||
element.radialLine(destination, foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
|
||||
element.radialLine(destination, accent, 0, 0.7, (second - 15) / 30 * math.Pi)
|
||||
}
|
||||
|
||||
// FlexibleHeightFor constrains the clock's minimum size to a 1:1 aspect ratio.
|
||||
func (element *AnalogClock) FlexibleHeightFor (width int) (height int) {
|
||||
return width
|
||||
// SetTime changes the time that the clock displays.
|
||||
func (element *AnalogClock) SetTime (newTime time.Time) {
|
||||
if newTime == element.time { return }
|
||||
element.time = newTime
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// OnFlexibleHeightChange sets a function to be called when the parameters
|
||||
// affecting the clock's flexible height change.
|
||||
func (element *AnalogClock) OnFlexibleHeightChange (func ()) { }
|
||||
func (element *AnalogClock) HandleThemeChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *AnalogClock) radialLine (
|
||||
destination artist.Canvas,
|
||||
source color.RGBA,
|
||||
inner float64,
|
||||
outer float64,
|
||||
radian float64,
|
||||
) {
|
||||
bounds := element.Bounds()
|
||||
bounds := element.entity.Bounds()
|
||||
width := float64(bounds.Dx()) / 2
|
||||
height := float64(bounds.Dy()) / 2
|
||||
min := element.Bounds().Min.Add(image.Pt (
|
||||
min := bounds.Min.Add(image.Pt (
|
||||
int(math.Cos(radian) * inner * width + width),
|
||||
int(math.Sin(radian) * inner * height + height)))
|
||||
max := element.Bounds().Min.Add(image.Pt (
|
||||
max := bounds.Min.Add(image.Pt (
|
||||
int(math.Cos(radian) * outer * width + width),
|
||||
int(math.Sin(radian) * outer * height + height)))
|
||||
shapes.ColorLine(element.core, source, 1, min, max)
|
||||
shapes.ColorLine(destination, source, 1, min, max)
|
||||
}
|
||||
|
||||
+87
-92
@@ -1,13 +1,16 @@
|
||||
package fun
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
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 {
|
||||
@@ -17,19 +20,14 @@ type pianoKey struct {
|
||||
|
||||
// Piano is an element that can be used to input midi notes.
|
||||
type Piano struct {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
low, high music.Octave
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
entity tomo.Entity
|
||||
|
||||
low, high music.Octave
|
||||
flatKeys []pianoKey
|
||||
sharpKeys []pianoKey
|
||||
contentBounds image.Rectangle
|
||||
|
||||
enabled bool
|
||||
pressed *pianoKey
|
||||
keynavPressed map[music.Note] bool
|
||||
|
||||
@@ -40,11 +38,7 @@ type Piano struct {
|
||||
// NewPiano returns a new piano element with a lowest and highest octave,
|
||||
// inclusive. If low is greater than high, they will be swapped.
|
||||
func NewPiano (low, high music.Octave) (element *Piano) {
|
||||
if low > high {
|
||||
temp := low
|
||||
low = high
|
||||
high = temp
|
||||
}
|
||||
if low > high { low, high = high, low }
|
||||
|
||||
element = &Piano {
|
||||
low: low,
|
||||
@@ -52,17 +46,68 @@ func NewPiano (low, high music.Octave) (element *Piano) {
|
||||
keynavPressed: make(map[music.Note] bool),
|
||||
}
|
||||
|
||||
element.theme.Case = theme.C("fun", "piano")
|
||||
element.Core, element.core = core.NewCore (func () {
|
||||
element.recalculate()
|
||||
element.draw()
|
||||
})
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore(element.redo)
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Piano) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Piano) Draw (destination artist.Canvas) {
|
||||
element.recalculate()
|
||||
|
||||
state := tomo.State {
|
||||
Focused: element.entity.Focused(),
|
||||
Disabled: !element.Enabled(),
|
||||
}
|
||||
|
||||
for _, key := range element.flatKeys {
|
||||
_, keynavPressed := element.keynavPressed[key.Note]
|
||||
element.drawFlat (
|
||||
destination,
|
||||
key.Rectangle,
|
||||
element.pressed != nil &&
|
||||
(*element.pressed).Note == key.Note || keynavPressed,
|
||||
state)
|
||||
}
|
||||
for _, key := range element.sharpKeys {
|
||||
_, keynavPressed := element.keynavPressed[key.Note]
|
||||
element.drawSharp (
|
||||
destination,
|
||||
key.Rectangle,
|
||||
element.pressed != nil &&
|
||||
(*element.pressed).Note == key.Note || keynavPressed,
|
||||
state)
|
||||
}
|
||||
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternPinboard, state, pianoCase)
|
||||
artutil.DrawShatter (
|
||||
destination, pattern, element.entity.Bounds(),
|
||||
element.contentBounds)
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *Piano) Focus () {
|
||||
element.entity.Focus()
|
||||
}
|
||||
|
||||
// Enabled returns whether this piano can be played or not.
|
||||
func (element *Piano) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this piano can be played or not.
|
||||
func (element *Piano) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
|
||||
// OnPress sets a function to be called when a key is pressed.
|
||||
func (element *Piano) OnPress (callback func (note music.Note)) {
|
||||
element.onPress = callback
|
||||
@@ -85,16 +130,14 @@ func (element *Piano) HandleMouseUp (x, y int, button input.Button) {
|
||||
element.onRelease((*element.pressed).Note)
|
||||
}
|
||||
element.pressed = nil
|
||||
element.redo()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Piano) HandleMouseMove (x, y int) {
|
||||
func (element *Piano) HandleMotion (x, y int) {
|
||||
if element.pressed == nil { return }
|
||||
element.pressUnderMouseCursor(image.Pt(x, y))
|
||||
}
|
||||
|
||||
func (element *Piano) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *Piano) pressUnderMouseCursor (point image.Point) {
|
||||
// find out which note is being pressed
|
||||
newKey := (*pianoKey)(nil)
|
||||
@@ -123,7 +166,7 @@ func (element *Piano) pressUnderMouseCursor (point image.Point) {
|
||||
if element.onPress != nil {
|
||||
element.onPress((*element.pressed).Note)
|
||||
}
|
||||
element.redo()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +226,7 @@ func (element *Piano) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if element.onPress != nil {
|
||||
element.onPress(note)
|
||||
}
|
||||
element.redo()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,30 +239,17 @@ func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
if element.onRelease != nil {
|
||||
element.onRelease(note)
|
||||
}
|
||||
element.redo()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *Piano) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
func (element *Piano) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.recalculate()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *Piano) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.updateMinimumSize()
|
||||
element.recalculate()
|
||||
element.redo()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Piano) updateMinimumSize () {
|
||||
padding := element.theme.Padding(theme.PatternPinboard)
|
||||
element.core.SetMinimumSize (
|
||||
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
|
||||
element.entity.SetMinimumSize (
|
||||
pianoKeyWidth * 7 * element.countOctaves() +
|
||||
padding.Horizontal(),
|
||||
64 + padding.Vertical())
|
||||
@@ -237,19 +267,12 @@ func (element *Piano) countSharps () int {
|
||||
return element.countOctaves() * 5
|
||||
}
|
||||
|
||||
func (element *Piano) redo () {
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Piano) recalculate () {
|
||||
element.flatKeys = make([]pianoKey, element.countFlats())
|
||||
element.sharpKeys = make([]pianoKey, element.countSharps())
|
||||
|
||||
padding := element.theme.Padding(theme.PatternPinboard)
|
||||
bounds := padding.Apply(element.Bounds())
|
||||
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
|
||||
bounds := padding.Apply(element.entity.Bounds())
|
||||
|
||||
dot := bounds.Min
|
||||
note := element.low.Note(0)
|
||||
@@ -280,52 +303,24 @@ func (element *Piano) recalculate () {
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Piano) draw () {
|
||||
state := theme.State {
|
||||
Focused: element.Focused(),
|
||||
Disabled: !element.Enabled(),
|
||||
}
|
||||
|
||||
for _, key := range element.flatKeys {
|
||||
_, keynavPressed := element.keynavPressed[key.Note]
|
||||
element.drawFlat (
|
||||
key.Rectangle,
|
||||
element.pressed != nil &&
|
||||
(*element.pressed).Note == key.Note || keynavPressed,
|
||||
state)
|
||||
}
|
||||
for _, key := range element.sharpKeys {
|
||||
_, keynavPressed := element.keynavPressed[key.Note]
|
||||
element.drawSharp (
|
||||
key.Rectangle,
|
||||
element.pressed != nil &&
|
||||
(*element.pressed).Note == key.Note || keynavPressed,
|
||||
state)
|
||||
}
|
||||
|
||||
pattern := element.theme.Pattern(theme.PatternPinboard, state)
|
||||
artist.DrawShatter (
|
||||
element.core, pattern, element.contentBounds)
|
||||
}
|
||||
|
||||
func (element *Piano) drawFlat (
|
||||
destination artist.Canvas,
|
||||
bounds image.Rectangle,
|
||||
pressed bool,
|
||||
state theme.State,
|
||||
state tomo.State,
|
||||
) {
|
||||
state.Pressed = pressed
|
||||
pattern := element.theme.Theme.Pattern (
|
||||
theme.PatternButton, state, theme.C("fun", "flatKey"))
|
||||
artist.DrawBounds(element.core, pattern, bounds)
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, flatCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
}
|
||||
|
||||
func (element *Piano) drawSharp (
|
||||
destination artist.Canvas,
|
||||
bounds image.Rectangle,
|
||||
pressed bool,
|
||||
state theme.State,
|
||||
state tomo.State,
|
||||
) {
|
||||
state.Pressed = pressed
|
||||
pattern := element.theme.Theme.Pattern (
|
||||
theme.PatternButton, state, theme.C("fun", "sharpKey"))
|
||||
artist.DrawBounds(element.core, pattern, bounds)
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, sharpCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
var iconCase = tomo.C("tomo", "icon")
|
||||
|
||||
// Icon is an element capable of displaying a singular icon.
|
||||
type Icon struct {
|
||||
entity tomo.Entity
|
||||
id tomo.Icon
|
||||
size tomo.IconSize
|
||||
}
|
||||
|
||||
// Icon creates a new icon element.
|
||||
func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) {
|
||||
element = &Icon {
|
||||
id: id,
|
||||
size: size,
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Icon) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// SetIcon sets the element's icon.
|
||||
func (element *Icon) SetIcon (id tomo.Icon, size tomo.IconSize) {
|
||||
element.id = id
|
||||
element.size = size
|
||||
if element.entity == nil { return }
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
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 artist.Canvas) {
|
||||
if element.entity == nil { return }
|
||||
|
||||
bounds := element.entity.Bounds()
|
||||
state := tomo.State { }
|
||||
element.entity.Theme().
|
||||
Pattern(tomo.PatternBackground, state, iconCase).
|
||||
Draw(destination, bounds)
|
||||
icon := element.icon()
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
offset := image.Pt (
|
||||
(bounds.Dx() - iconBounds.Dx()) / 2,
|
||||
(bounds.Dy() - iconBounds.Dy()) / 2)
|
||||
icon.Draw (
|
||||
destination,
|
||||
element.entity.Theme().Color(tomo.ColorForeground, state, iconCase),
|
||||
bounds.Min.Add(offset))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Icon) icon () artist.Icon {
|
||||
return element.entity.Theme().Icon(element.id, element.size, iconCase)
|
||||
}
|
||||
|
||||
func (element *Icon) updateMinimumSize () {
|
||||
icon := element.icon()
|
||||
if icon == nil {
|
||||
element.entity.SetMinimumSize(0, 0)
|
||||
} else {
|
||||
bounds := icon.Bounds()
|
||||
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
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
|
||||
|
||||
// Image is an element capable of displaying an image.
|
||||
type Image struct {
|
||||
entity tomo.Entity
|
||||
buffer artist.Canvas
|
||||
}
|
||||
|
||||
// NewImage creates a new image element.
|
||||
func NewImage (image image.Image) (element *Image) {
|
||||
element = &Image { buffer: artist.FromImage(image) }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
bounds := element.buffer.Bounds()
|
||||
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Image) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Image) Draw (destination artist.Canvas) {
|
||||
if element.entity == nil { return }
|
||||
(patterns.Texture { Canvas: element.buffer }).
|
||||
Draw(destination, element.entity.Bounds())
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
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/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
||||
|
||||
var labelCase = tomo.C("tomo", "label")
|
||||
|
||||
// Label is a simple text box.
|
||||
type Label struct {
|
||||
entity tomo.Entity
|
||||
|
||||
align textdraw.Align
|
||||
wrap bool
|
||||
text string
|
||||
drawer textdraw.Drawer
|
||||
|
||||
forcedColumns int
|
||||
forcedRows int
|
||||
minHeight int
|
||||
}
|
||||
|
||||
// NewLabel creates a new label.
|
||||
func NewLabel (text string) (element *Label) {
|
||||
element = &Label { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, labelCase))
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
// NewLabelWrapped creates a new label with text wrapping on.
|
||||
func NewLabelWrapped (text string) (element *Label) {
|
||||
element = NewLabel(text)
|
||||
element.SetWrap(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Label) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Label) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
if element.wrap {
|
||||
element.drawer.SetMaxWidth(bounds.Dx())
|
||||
element.drawer.SetMaxHeight(bounds.Dy())
|
||||
}
|
||||
|
||||
element.entity.DrawBackground(destination)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
foreground := element.entity.Theme().Color (
|
||||
tomo.ColorForeground,
|
||||
tomo.State { }, labelCase)
|
||||
element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min))
|
||||
}
|
||||
|
||||
// Copy copies the label's textto the clipboard.
|
||||
func (element *Label) Copy () {
|
||||
window := element.entity.Window()
|
||||
if window != nil {
|
||||
window.Copy(data.Bytes(data.MimePlain, []byte(element.text)))
|
||||
}
|
||||
}
|
||||
|
||||
// EmCollapse forces a minimum width and height upon the label. The width is
|
||||
// measured in emspaces, and the height is measured in lines. If a zero value is
|
||||
// given for a dimension, its minimum will be determined by the label's content.
|
||||
// If the label's content is greater than these dimensions, it will be truncated
|
||||
// to fit.
|
||||
func (element *Label) EmCollapse (columns int, rows int) {
|
||||
element.forcedColumns = columns
|
||||
element.forcedRows = rows
|
||||
element.updateMinimumSize()
|
||||
}
|
||||
|
||||
// FlexibleHeightFor returns the reccomended height for this element based on
|
||||
// the given width in order to allow the text to wrap properly.
|
||||
func (element *Label) FlexibleHeightFor (width int) (height int) {
|
||||
if element.wrap {
|
||||
return element.drawer.ReccomendedHeightFor(width)
|
||||
} else {
|
||||
return element.minHeight
|
||||
}
|
||||
}
|
||||
|
||||
// SetText sets the label's text.
|
||||
func (element *Label) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetWrap sets wether or not the label's text wraps. If the text is set to
|
||||
// wrap, the element will have a minimum size of a single character and
|
||||
// automatically wrap its text. If the text is set to not wrap, the element will
|
||||
// have a minimum size that fits its text.
|
||||
func (element *Label) SetWrap (wrap bool) {
|
||||
if wrap == element.wrap { return }
|
||||
if !wrap {
|
||||
element.drawer.SetMaxWidth(0)
|
||||
element.drawer.SetMaxHeight(0)
|
||||
}
|
||||
element.wrap = wrap
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetAlign sets the alignment method of the label.
|
||||
func (element *Label) SetAlign (align textdraw.Align) {
|
||||
if align == element.align { return }
|
||||
element.align = align
|
||||
element.drawer.SetAlign(align)
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Label) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, labelCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Label) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button == input.ButtonRight {
|
||||
element.contextMenu(position)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Label) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *Label) contextMenu (position image.Point) {
|
||||
window := element.entity.Window()
|
||||
menu, err := window.NewMenu(image.Rectangle { position, position })
|
||||
if err != nil { return }
|
||||
|
||||
closeAnd := func (callback func ()) func () {
|
||||
return func () { callback(); menu.Close() }
|
||||
}
|
||||
|
||||
copyButton := NewButton("Copy")
|
||||
copyButton.ShowText(false)
|
||||
copyButton.SetIcon(tomo.IconCopy)
|
||||
copyButton.OnClick(closeAnd(element.Copy))
|
||||
|
||||
menu.Adopt (NewHBox (
|
||||
SpaceNone,
|
||||
copyButton,
|
||||
))
|
||||
copyButton.Focus()
|
||||
menu.Show()
|
||||
}
|
||||
|
||||
func (element *Label) updateMinimumSize () {
|
||||
var width, height int
|
||||
|
||||
if element.wrap {
|
||||
em := element.drawer.Em().Round()
|
||||
if em < 1 {
|
||||
em = element.entity.Theme().Padding(tomo.PatternBackground, labelCase)[0]
|
||||
}
|
||||
width, height = em, element.drawer.LineHeight().Round()
|
||||
element.entity.NotifyFlexibleHeightChange()
|
||||
} else {
|
||||
bounds := element.drawer.LayoutBounds()
|
||||
width, height = bounds.Dx(), bounds.Dy()
|
||||
}
|
||||
|
||||
if element.forcedColumns > 0 {
|
||||
width =
|
||||
element.drawer.Em().
|
||||
Mul(fixed.I(element.forcedColumns)).Floor()
|
||||
}
|
||||
|
||||
if element.forcedRows > 0 {
|
||||
height =
|
||||
element.drawer.LineHeight().
|
||||
Mul(fixed.I(element.forcedRows)).Floor()
|
||||
}
|
||||
|
||||
element.minHeight = height
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
package basicElements
|
||||
package elements
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
// Numeric is a type constraint representing a number.
|
||||
type Numeric interface {
|
||||
@@ -10,24 +12,29 @@ type Numeric interface {
|
||||
// LerpSlider is a slider that has a minimum and maximum value, and who's value
|
||||
// can be any numeric type.
|
||||
type LerpSlider[T Numeric] struct {
|
||||
*Slider
|
||||
slider
|
||||
min T
|
||||
max T
|
||||
}
|
||||
|
||||
// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. If
|
||||
// vertical is set to true, the slider will be vertical instead of horizontal.
|
||||
func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *LerpSlider[T]) {
|
||||
if min > max {
|
||||
temp := max
|
||||
max = min
|
||||
min = temp
|
||||
}
|
||||
// NewVLerpSlider creates a new horizontal LerpSlider with a minimum and maximum
|
||||
// value.
|
||||
func NewVLerpSlider[T Numeric] (min, max T, value T) (element *LerpSlider[T]) {
|
||||
element = NewHLerpSlider(min, max, value)
|
||||
element.vertical = true
|
||||
return
|
||||
}
|
||||
|
||||
// NewHLerpSlider creates a new horizontal LerpSlider with a minimum and maximum
|
||||
// value.
|
||||
func NewHLerpSlider[T Numeric] (min, max T, value T) (element *LerpSlider[T]) {
|
||||
if min > max { min, max = max, min }
|
||||
element = &LerpSlider[T] {
|
||||
Slider: NewSlider(0, vertical),
|
||||
min: min,
|
||||
max: max,
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.construct()
|
||||
element.SetValue(value)
|
||||
return
|
||||
}
|
||||
@@ -35,13 +42,13 @@ func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *Ler
|
||||
// SetValue sets the slider's value.
|
||||
func (element *LerpSlider[T]) SetValue (value T) {
|
||||
value -= element.min
|
||||
element.Slider.SetValue(float64(value) / float64(element.Range()))
|
||||
element.slider.SetValue(float64(value) / float64(element.Range()))
|
||||
}
|
||||
|
||||
// Value returns the slider's value.
|
||||
func (element *LerpSlider[T]) Value () (value T) {
|
||||
return T (
|
||||
float64(element.Slider.Value()) * float64(element.Range())) +
|
||||
float64(element.slider.Value()) * float64(element.Range())) +
|
||||
element.min
|
||||
}
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
package elements
|
||||
|
||||
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/ability"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
|
||||
|
||||
type list struct {
|
||||
container
|
||||
entity tomo.Entity
|
||||
|
||||
c tomo.Case
|
||||
|
||||
enabled bool
|
||||
scroll image.Point
|
||||
contentBounds image.Rectangle
|
||||
selected int
|
||||
|
||||
forcedMinimumWidth int
|
||||
forcedMinimumHeight int
|
||||
|
||||
onClick func ()
|
||||
onSelectionChange func ()
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
type List struct {
|
||||
list
|
||||
}
|
||||
|
||||
type FlowList struct {
|
||||
list
|
||||
}
|
||||
|
||||
func NewList (children ...tomo.Element) (element *List) {
|
||||
element = &List { }
|
||||
element.c = tomo.C("tomo", "list")
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.container.entity = element.entity
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init(children...)
|
||||
return
|
||||
}
|
||||
|
||||
func NewFlowList (children ...tomo.Element) (element *FlowList) {
|
||||
element = &FlowList { }
|
||||
element.c = tomo.C("tomo", "flowList")
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.container.entity = element.entity
|
||||
element.minimumSize = element.updateMinimumSize
|
||||
element.init(children...)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *list) init (children ...tomo.Element) {
|
||||
element.selected = -1
|
||||
element.enabled = true
|
||||
element.container.init()
|
||||
element.Adopt(children...)
|
||||
}
|
||||
|
||||
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.entity.Theme().Pattern(tomo.PatternSunken, element.state(), element.c)
|
||||
artutil.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...)
|
||||
}
|
||||
|
||||
func (element *List) Layout () {
|
||||
if element.scroll.Y > element.maxScrollHeight() {
|
||||
element.scroll.Y = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
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 { }
|
||||
|
||||
dot := bounds.Min.Sub(element.scroll)
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
width := bounds.Dx()
|
||||
height := int(entry.minSize)
|
||||
|
||||
childBounds := tomo.Bounds (
|
||||
dot.X, dot.Y,
|
||||
width, height)
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||
|
||||
dot.Y += height
|
||||
dot.Y += margin.Y
|
||||
}
|
||||
|
||||
element.contentBounds =
|
||||
element.contentBounds.Sub(element.contentBounds.Min)
|
||||
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *FlowList) Layout () {
|
||||
if element.scroll.Y > element.maxScrollHeight() {
|
||||
element.scroll.Y = element.maxScrollHeight()
|
||||
}
|
||||
|
||||
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 { }
|
||||
|
||||
dot := bounds.Min.Sub(element.scroll)
|
||||
xStart := dot.X
|
||||
rowHeight := 0
|
||||
|
||||
nextLine := func () {
|
||||
dot.X = xStart
|
||||
dot.Y += margin.Y
|
||||
dot.Y += rowHeight
|
||||
rowHeight = 0
|
||||
}
|
||||
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
width := int(entry.minBreadth)
|
||||
height := int(entry.minSize)
|
||||
if width + dot.X > bounds.Max.X {
|
||||
nextLine()
|
||||
}
|
||||
if typedChild, ok := child.(ability.Flexible); ok {
|
||||
height = typedChild.FlexibleHeightFor(width)
|
||||
}
|
||||
if rowHeight < height {
|
||||
rowHeight = height
|
||||
}
|
||||
|
||||
childBounds := tomo.Bounds (
|
||||
dot.X, dot.Y,
|
||||
width, height)
|
||||
element.entity.PlaceChild(index, childBounds)
|
||||
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||
|
||||
dot.X += width + margin.X
|
||||
}
|
||||
|
||||
element.contentBounds =
|
||||
element.contentBounds.Sub(element.contentBounds.Min)
|
||||
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) Selected () ability.Selectable {
|
||||
if element.selected == -1 { return nil }
|
||||
child, ok := element.entity.Child(element.selected).(ability.Selectable)
|
||||
if !ok { return nil }
|
||||
return child
|
||||
}
|
||||
|
||||
func (element *list) Select (child ability.Selectable) {
|
||||
index := element.entity.IndexOf(child)
|
||||
if element.selected == index { return }
|
||||
element.selectNone()
|
||||
element.selected = index
|
||||
element.entity.SelectChild(index, true)
|
||||
if element.onSelectionChange != nil {
|
||||
element.onSelectionChange()
|
||||
}
|
||||
element.scrollToSelected()
|
||||
}
|
||||
|
||||
func (element *list) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
func (element *list) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *list) Focus () {
|
||||
element.entity.Focus()
|
||||
}
|
||||
|
||||
func (element *list) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *list) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.enabled { return }
|
||||
element.Focus()
|
||||
element.selectNone()
|
||||
}
|
||||
|
||||
func (element *list) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *list) HandleChildMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element,
|
||||
) {
|
||||
if !element.enabled { return }
|
||||
element.Focus()
|
||||
if child, ok := child.(ability.Selectable); ok {
|
||||
element.Select(child)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) HandleChildMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
child tomo.Element,
|
||||
) {
|
||||
if !position.In(child.Entity().Bounds()) { return }
|
||||
if element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) HandleChildFlexibleHeightChange (child ability.Flexible) {
|
||||
element.minimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *list) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
index := -1
|
||||
switch key {
|
||||
case input.KeyUp, input.KeyLeft:
|
||||
index = element.selected - 1
|
||||
case input.KeyDown, input.KeyRight:
|
||||
index = element.selected + 1
|
||||
case input.KeyEnter:
|
||||
if element.onClick != nil {
|
||||
element.onClick()
|
||||
}
|
||||
}
|
||||
if index >= 0 && index < element.entity.CountChildren() {
|
||||
element.selectNone()
|
||||
element.selected = index
|
||||
element.entity.SelectChild(index, true)
|
||||
if element.onSelectionChange != nil {
|
||||
element.onSelectionChange()
|
||||
}
|
||||
element.scrollToSelected()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
func (element *list) DrawBackground (destination artist.Canvas) {
|
||||
element.entity.DrawBackground(destination)
|
||||
}
|
||||
|
||||
func (element *list) HandleThemeChange () {
|
||||
element.minimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Collapse forces a minimum width and height upon the list. If a zero value is
|
||||
// given for a dimension, its minimum will be determined by the list's content.
|
||||
// If the list's height goes beyond the forced size, it will need to be accessed
|
||||
// via scrolling. If an entry's width goes beyond the forced size, its text will
|
||||
// be truncated so that it fits.
|
||||
func (element *list) Collapse (width, height int) {
|
||||
if
|
||||
element.forcedMinimumWidth == width &&
|
||||
element.forcedMinimumHeight == height {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
element.forcedMinimumWidth = width
|
||||
element.forcedMinimumHeight = height
|
||||
|
||||
element.minimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *list) ScrollContentBounds () image.Rectangle {
|
||||
return element.contentBounds
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's
|
||||
// viewport relative to ScrollBounds.
|
||||
func (element *list) ScrollViewportBounds () image.Rectangle {
|
||||
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
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *list) ScrollTo (position image.Point) {
|
||||
if position.Y < 0 {
|
||||
position.Y = 0
|
||||
}
|
||||
maxScrollHeight := element.maxScrollHeight()
|
||||
if position.Y > maxScrollHeight {
|
||||
position.Y = maxScrollHeight
|
||||
}
|
||||
element.scroll = position
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||
// bounds, content bounds, or scroll axes change.
|
||||
func (element *list) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
func (element *list) OnClick (callback func ()) {
|
||||
element.onClick = callback
|
||||
}
|
||||
|
||||
func (element *list) OnSelectionChange (callback func ()) {
|
||||
element.onSelectionChange = callback
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *list) ScrollAxes () (horizontal, vertical bool) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (element *list) selectNone () {
|
||||
if element.selected >= 0 {
|
||||
element.entity.SelectChild(element.selected, false)
|
||||
if element.onSelectionChange != nil {
|
||||
element.onSelectionChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) scrollToSelected () {
|
||||
if element.selected < 0 { return }
|
||||
target := element.entity.Child(element.selected).Entity().Bounds()
|
||||
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
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
} else if target.Max.Y > bounds.Max.Y {
|
||||
element.scroll.Y += target.Max.Y - bounds.Max.Y
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) state () tomo.State {
|
||||
return tomo.State {
|
||||
Focused: element.entity.Focused(),
|
||||
Disabled: !element.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func (element *list) maxScrollHeight () (height int) {
|
||||
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 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *List) updateMinimumSize () {
|
||||
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
|
||||
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
|
||||
|
||||
width := 0
|
||||
height := 0
|
||||
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||
if index > 0 { height += margin.Y }
|
||||
|
||||
child := element.entity.Child(index)
|
||||
entry := element.scratch[child]
|
||||
|
||||
entryWidth, entryHeight := element.entity.ChildMinimumSize(index)
|
||||
entry.minBreadth = float64(entryWidth)
|
||||
entry.minSize = float64(entryHeight)
|
||||
element.scratch[child] = entry
|
||||
|
||||
height += entryHeight
|
||||
if width < entryWidth { width = entryWidth }
|
||||
}
|
||||
|
||||
width += padding.Horizontal()
|
||||
height += padding.Vertical()
|
||||
|
||||
if element.forcedMinimumWidth > 0 {
|
||||
width = element.forcedMinimumWidth
|
||||
}
|
||||
if element.forcedMinimumHeight > 0 {
|
||||
height = element.forcedMinimumHeight
|
||||
}
|
||||
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *FlowList) updateMinimumSize () {
|
||||
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)
|
||||
if width > minimumWidth {
|
||||
minimumWidth = width
|
||||
}
|
||||
|
||||
key := element.entity.Child(index)
|
||||
entry := element.scratch[key]
|
||||
entry.minSize = float64(height)
|
||||
entry.minBreadth = float64(width)
|
||||
element.scratch[key] = entry
|
||||
}
|
||||
element.entity.SetMinimumSize (
|
||||
minimumWidth + padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
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
|
||||
}
|
||||
|
||||
// NewProgressBar creates a new progress bar displaying the given progress
|
||||
// level.
|
||||
func NewProgressBar (progress float64) (element *ProgressBar) {
|
||||
if progress < 0 { progress = 0 }
|
||||
if progress > 1 { progress = 1 }
|
||||
element = &ProgressBar { progress: progress }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *ProgressBar) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *ProgressBar) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
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.entity.Theme().Pattern(tomo.PatternMercury, tomo.State { }, progressBarCase)
|
||||
mercury.Draw(destination, meterBounds)
|
||||
}
|
||||
|
||||
// SetProgress sets the progress level of the bar.
|
||||
func (element *ProgressBar) SetProgress (progress float64) {
|
||||
if progress < 0 { progress = 0 }
|
||||
if progress > 1 { progress = 1 }
|
||||
if progress == element.progress { return }
|
||||
element.progress = progress
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ProgressBar) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ProgressBar) updateMinimumSize() {
|
||||
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())
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package elements
|
||||
|
||||
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/ability"
|
||||
|
||||
var scrollCase = tomo.C("tomo", "scroll")
|
||||
|
||||
// ScrollMode specifies which sides of a Scroll have scroll bars.
|
||||
type ScrollMode int; const (
|
||||
ScrollNeither ScrollMode = 0
|
||||
ScrollVertical ScrollMode = 1
|
||||
ScrollHorizontal ScrollMode = 2
|
||||
ScrollBoth ScrollMode = ScrollVertical | ScrollHorizontal
|
||||
)
|
||||
|
||||
// Includes returns whether a scroll mode has been or'd with another scroll
|
||||
// mode.
|
||||
func (mode ScrollMode) Includes (sub ScrollMode) bool {
|
||||
return (mode & sub) > 0
|
||||
}
|
||||
|
||||
// Scroll adds scroll bars to any scrollable element. It also captures scroll
|
||||
// wheel input.
|
||||
type Scroll struct {
|
||||
entity tomo.Entity
|
||||
|
||||
child ability.Scrollable
|
||||
horizontal *ScrollBar
|
||||
vertical *ScrollBar
|
||||
}
|
||||
|
||||
// NewScroll creates a new scroll element.
|
||||
func NewScroll (mode ScrollMode, child ability.Scrollable) (element *Scroll) {
|
||||
element = &Scroll { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
|
||||
if mode.Includes(ScrollHorizontal) {
|
||||
element.horizontal = NewHScrollBar()
|
||||
element.horizontal.OnScroll (func (viewport image.Point) {
|
||||
if element.child != nil {
|
||||
element.child.ScrollTo(viewport)
|
||||
}
|
||||
if element.vertical != nil {
|
||||
element.vertical.SetBounds (
|
||||
element.child.ScrollContentBounds(),
|
||||
element.child.ScrollViewportBounds())
|
||||
}
|
||||
})
|
||||
element.entity.Adopt(element.horizontal)
|
||||
}
|
||||
if mode.Includes(ScrollVertical) {
|
||||
element.vertical = NewVScrollBar()
|
||||
element.vertical.OnScroll (func (viewport image.Point) {
|
||||
if element.child != nil {
|
||||
element.child.ScrollTo(viewport)
|
||||
}
|
||||
if element.horizontal != nil {
|
||||
element.horizontal.SetBounds (
|
||||
element.child.ScrollContentBounds(),
|
||||
element.child.ScrollViewportBounds())
|
||||
}
|
||||
})
|
||||
element.entity.Adopt(element.vertical)
|
||||
}
|
||||
|
||||
element.Adopt(child)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Scroll) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination 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.entity.Theme().Pattern(tomo.PatternDead, state, scrollCase)
|
||||
deadArea.Draw(artist.Cut(destination, bounds), bounds)
|
||||
}
|
||||
}
|
||||
|
||||
// Layout causes this element to perform a layout operation.
|
||||
func (element *Scroll) Layout () {
|
||||
bounds := element.entity.Bounds()
|
||||
child := bounds
|
||||
|
||||
iHorizontal := element.entity.IndexOf(element.horizontal)
|
||||
iVertical := element.entity.IndexOf(element.vertical)
|
||||
iChild := element.entity.IndexOf(element.child)
|
||||
|
||||
var horizontal, vertical image.Rectangle
|
||||
|
||||
if element.horizontal != nil {
|
||||
_, hMinHeight := element.entity.ChildMinimumSize(iHorizontal)
|
||||
child.Max.Y -= hMinHeight
|
||||
}
|
||||
if element.vertical != nil {
|
||||
vMinWidth, _ := element.entity.ChildMinimumSize(iVertical)
|
||||
child.Max.X -= vMinWidth
|
||||
}
|
||||
|
||||
horizontal.Min.X = bounds.Min.X
|
||||
horizontal.Max.X = child.Max.X
|
||||
horizontal.Min.Y = child.Max.Y
|
||||
horizontal.Max.Y = bounds.Max.Y
|
||||
|
||||
vertical.Min.X = child.Max.X
|
||||
vertical.Max.X = bounds.Max.X
|
||||
vertical.Min.Y = bounds.Min.Y
|
||||
vertical.Max.Y = child.Max.Y
|
||||
|
||||
if element.horizontal != nil {
|
||||
element.entity.PlaceChild (iHorizontal, horizontal)
|
||||
}
|
||||
if element.vertical != nil {
|
||||
element.entity.PlaceChild(iVertical, vertical)
|
||||
}
|
||||
if element.child != nil {
|
||||
element.entity.PlaceChild(iChild, child)
|
||||
}
|
||||
}
|
||||
|
||||
// DrawBackground draws this element's background pattern to the specified
|
||||
// destination 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 ability.Scrollable) {
|
||||
if element.child != nil {
|
||||
element.entity.Disown(element.entity.IndexOf(element.child))
|
||||
}
|
||||
if child != nil {
|
||||
element.entity.Adopt(child)
|
||||
}
|
||||
element.child = child
|
||||
|
||||
element.updateEnabled()
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Child returns this element's child. If there is no child, this method will
|
||||
// return nil.
|
||||
func (element *Scroll) Child () ability.Scrollable {
|
||||
return element.child
|
||||
}
|
||||
|
||||
func (element *Scroll) HandleChildMinimumSizeChange (tomo.Element) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Scroll) HandleChildScrollBoundsChange (ability.Scrollable) {
|
||||
element.updateEnabled()
|
||||
viewportBounds := element.child.ScrollViewportBounds()
|
||||
contentBounds := element.child.ScrollContentBounds()
|
||||
if element.horizontal != nil {
|
||||
element.horizontal.SetBounds(contentBounds, viewportBounds)
|
||||
}
|
||||
if element.vertical != nil {
|
||||
element.vertical.SetBounds(contentBounds, viewportBounds)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Scroll) HandleScroll (
|
||||
position image.Point,
|
||||
deltaX, deltaY float64,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
horizontal, vertical := element.child.ScrollAxes()
|
||||
if !horizontal { deltaX = 0 }
|
||||
if !vertical { deltaY = 0 }
|
||||
element.scrollChildBy(int(deltaX), int(deltaY))
|
||||
}
|
||||
|
||||
func (element *Scroll) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Scroll) updateMinimumSize () {
|
||||
var width, height int
|
||||
|
||||
if element.child != nil {
|
||||
width, height = element.entity.ChildMinimumSize (
|
||||
element.entity.IndexOf(element.child))
|
||||
}
|
||||
if element.horizontal != nil {
|
||||
hMinWidth, hMinHeight := element.entity.ChildMinimumSize (
|
||||
element.entity.IndexOf(element.horizontal))
|
||||
height += hMinHeight
|
||||
if hMinWidth > width {
|
||||
width = hMinWidth
|
||||
}
|
||||
}
|
||||
if element.vertical != nil {
|
||||
vMinWidth, vMinHeight := element.entity.ChildMinimumSize (
|
||||
element.entity.IndexOf(element.vertical))
|
||||
width += vMinWidth
|
||||
if vMinHeight > height {
|
||||
height = vMinHeight
|
||||
}
|
||||
}
|
||||
element.entity.SetMinimumSize(width, height)
|
||||
}
|
||||
|
||||
func (element *Scroll) updateEnabled () {
|
||||
horizontal, vertical := false, false
|
||||
if element.child != nil {
|
||||
horizontal, vertical = element.child.ScrollAxes()
|
||||
}
|
||||
if element.horizontal != nil {
|
||||
element.horizontal.SetEnabled(horizontal)
|
||||
}
|
||||
if element.vertical != nil {
|
||||
element.vertical.SetEnabled(vertical)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Scroll) scrollChildBy (x, y int) {
|
||||
if element.child == nil { return }
|
||||
scrollPoint :=
|
||||
element.child.ScrollViewportBounds().Min.
|
||||
Add(image.Pt(x, y))
|
||||
element.child.ScrollTo(scrollPoint)
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
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
|
||||
// scrollable element. Instead of having a value from zero to one, it stores
|
||||
// viewport and content boundaries. When the user drags the scroll bar handle,
|
||||
// the scroll bar calls the OnScroll callback assigned to it with the position
|
||||
// the user is trying to move the handle to. A program can check to see if this
|
||||
// value is valid, move the viewport, and give the scroll bar the new viewport
|
||||
// bounds (which will then cause it to move the handle).
|
||||
//
|
||||
// Typically, you wont't want to use a ScrollBar by itself. A ScrollContainer is
|
||||
// better for most cases.
|
||||
type ScrollBar struct {
|
||||
entity tomo.Entity
|
||||
|
||||
c tomo.Case
|
||||
|
||||
vertical bool
|
||||
enabled bool
|
||||
dragging bool
|
||||
dragOffset image.Point
|
||||
track image.Rectangle
|
||||
bar image.Rectangle
|
||||
|
||||
contentBounds image.Rectangle
|
||||
viewportBounds image.Rectangle
|
||||
|
||||
onScroll func (viewport image.Point)
|
||||
}
|
||||
|
||||
// NewVScrollBar creates a new vertical scroll bar.
|
||||
func NewVScrollBar () (element *ScrollBar) {
|
||||
element = &ScrollBar {
|
||||
vertical: true,
|
||||
enabled: true,
|
||||
}
|
||||
element.c = tomo.C("tomo", "scrollBarVertical")
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// NewHScrollBar creates a new horizontal scroll bar.
|
||||
func NewHScrollBar () (element *ScrollBar) {
|
||||
element = &ScrollBar {
|
||||
enabled: true,
|
||||
}
|
||||
element.c = tomo.C("tomo", "scrollBarHorizontal")
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *ScrollBar) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *ScrollBar) Draw (destination artist.Canvas) {
|
||||
element.recalculate()
|
||||
|
||||
bounds := element.entity.Bounds()
|
||||
state := tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Pressed: element.dragging,
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
velocity := element.entity.Config().ScrollVelocity()
|
||||
|
||||
if position.In(element.bar) {
|
||||
// the mouse is pressed down within the bar's handle
|
||||
element.dragging = true
|
||||
element.entity.Invalidate()
|
||||
element.dragOffset =
|
||||
position.Sub(element.bar.Min).
|
||||
Add(element.entity.Bounds().Min)
|
||||
element.dragTo(position)
|
||||
} else {
|
||||
// the mouse is pressed down within the bar's gutter
|
||||
switch button {
|
||||
case input.ButtonLeft:
|
||||
// start scrolling at this point, but set the offset to
|
||||
// the middle of the handle
|
||||
element.dragging = true
|
||||
element.dragOffset = element.fallbackDragOffset()
|
||||
element.dragTo(position)
|
||||
|
||||
case input.ButtonMiddle:
|
||||
// page up/down on middle click
|
||||
viewport := 0
|
||||
if element.vertical {
|
||||
viewport = element.viewportBounds.Dy()
|
||||
} else {
|
||||
viewport = element.viewportBounds.Dx()
|
||||
}
|
||||
if element.isAfterHandle(position) {
|
||||
element.scrollBy(viewport)
|
||||
} else {
|
||||
element.scrollBy(-viewport)
|
||||
}
|
||||
|
||||
case input.ButtonRight:
|
||||
// inch up/down on right click
|
||||
if element.isAfterHandle(position) {
|
||||
element.scrollBy(velocity)
|
||||
} else {
|
||||
element.scrollBy(-velocity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if element.dragging {
|
||||
element.dragging = false
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleMotion (position image.Point) {
|
||||
if element.dragging {
|
||||
element.dragTo(position)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleScroll (
|
||||
position image.Point,
|
||||
deltaX, deltaY float64,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if element.vertical {
|
||||
element.scrollBy(int(deltaY))
|
||||
} else {
|
||||
element.scrollBy(int(deltaX))
|
||||
}
|
||||
}
|
||||
|
||||
// SetEnabled sets whether or not the scroll bar can be interacted with.
|
||||
func (element *ScrollBar) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// Enabled returns whether or not the element is enabled.
|
||||
func (element *ScrollBar) Enabled () (enabled bool) {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetBounds sets the content and viewport bounds of the scroll bar.
|
||||
func (element *ScrollBar) SetBounds (content, viewport image.Rectangle) {
|
||||
element.contentBounds = content
|
||||
element.viewportBounds = viewport
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// OnScroll sets a function to be called when the user tries to move the scroll
|
||||
// bar's handle. The callback is passed a point representing the new viewport
|
||||
// position. For the scroll bar's position to visually update, the callback must
|
||||
// check if the position is valid and call ScrollBar.SetBounds with the new
|
||||
// viewport bounds.
|
||||
func (element *ScrollBar) OnScroll (callback func (viewport image.Point)) {
|
||||
element.onScroll = callback
|
||||
}
|
||||
|
||||
func (element *ScrollBar) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ScrollBar) isAfterHandle (point image.Point) bool {
|
||||
if element.vertical {
|
||||
return point.Y > element.bar.Min.Y
|
||||
} else {
|
||||
return point.X > element.bar.Min.X
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) fallbackDragOffset () image.Point {
|
||||
if element.vertical {
|
||||
return element.entity.Bounds().Min.
|
||||
Add(image.Pt(0, element.bar.Dy() / 2))
|
||||
} else {
|
||||
return element.entity.Bounds().Min.
|
||||
Add(image.Pt(element.bar.Dx() / 2, 0))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) scrollBy (delta int) {
|
||||
deltaPoint := image.Point { }
|
||||
if element.vertical {
|
||||
deltaPoint.Y = delta
|
||||
} else {
|
||||
deltaPoint.X = delta
|
||||
}
|
||||
if element.onScroll != nil {
|
||||
element.onScroll(element.viewportBounds.Min.Add(deltaPoint))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) dragTo (point image.Point) {
|
||||
point = point.Sub(element.dragOffset)
|
||||
var scrollX, scrollY float64
|
||||
|
||||
if element.vertical {
|
||||
ratio :=
|
||||
float64(element.contentBounds.Dy()) /
|
||||
float64(element.track.Dy())
|
||||
scrollX = float64(element.viewportBounds.Min.X)
|
||||
scrollY = float64(point.Y) * ratio
|
||||
} else {
|
||||
ratio :=
|
||||
float64(element.contentBounds.Dx()) /
|
||||
float64(element.track.Dx())
|
||||
scrollX = float64(point.X) * ratio
|
||||
scrollY = float64(element.viewportBounds.Min.Y)
|
||||
}
|
||||
|
||||
if element.onScroll != nil {
|
||||
element.onScroll(image.Pt(int(scrollX), int(scrollY)))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) recalculate () {
|
||||
if element.vertical {
|
||||
element.recalculateVertical()
|
||||
} else {
|
||||
element.recalculateHorizontal()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) recalculateVertical () {
|
||||
bounds := element.entity.Bounds()
|
||||
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
|
||||
element.track = padding.Apply(bounds)
|
||||
|
||||
contentBounds := element.contentBounds
|
||||
viewportBounds := element.viewportBounds
|
||||
if element.Enabled() {
|
||||
element.bar.Min.X = element.track.Min.X
|
||||
element.bar.Max.X = element.track.Max.X
|
||||
|
||||
ratio :=
|
||||
float64(element.track.Dy()) /
|
||||
float64(contentBounds.Dy())
|
||||
element.bar.Min.Y = int(float64(viewportBounds.Min.Y) * ratio)
|
||||
element.bar.Max.Y = int(float64(viewportBounds.Max.Y) * ratio)
|
||||
|
||||
element.bar.Min.Y += element.track.Min.Y
|
||||
element.bar.Max.Y += element.track.Min.Y
|
||||
}
|
||||
|
||||
// if the handle is out of bounds, don't display it
|
||||
if element.bar.Dy() >= element.track.Dy() {
|
||||
element.bar = image.Rectangle { }
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) recalculateHorizontal () {
|
||||
bounds := element.entity.Bounds()
|
||||
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
|
||||
element.track = padding.Apply(bounds)
|
||||
|
||||
contentBounds := element.contentBounds
|
||||
viewportBounds := element.viewportBounds
|
||||
if element.Enabled() {
|
||||
element.bar.Min.Y = element.track.Min.Y
|
||||
element.bar.Max.Y = element.track.Max.Y
|
||||
|
||||
ratio :=
|
||||
float64(element.track.Dx()) /
|
||||
float64(contentBounds.Dx())
|
||||
element.bar.Min.X = int(float64(viewportBounds.Min.X) * ratio)
|
||||
element.bar.Max.X = int(float64(viewportBounds.Max.X) * ratio)
|
||||
|
||||
element.bar.Min.X += element.track.Min.X
|
||||
element.bar.Max.X += element.track.Min.X
|
||||
}
|
||||
|
||||
// if the handle is out of bounds, don't display it
|
||||
if element.bar.Dx() >= element.track.Dx() {
|
||||
element.bar = image.Rectangle { }
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ScrollBar) updateMinimumSize () {
|
||||
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(),
|
||||
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
|
||||
} else {
|
||||
element.entity.SetMinimumSize (
|
||||
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
|
||||
gutterPadding.Vertical() + handlePadding.Vertical())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
// Slider is a slider control with a floating point value between zero and one.
|
||||
type Slider struct {
|
||||
slider
|
||||
}
|
||||
|
||||
// NewVSlider creates a new horizontal slider with the specified value.
|
||||
func NewVSlider (value float64) (element *Slider) {
|
||||
element = NewHSlider(value)
|
||||
element.vertical = true
|
||||
return
|
||||
}
|
||||
|
||||
// NewHSlider creates a new horizontal slider with the specified value.
|
||||
func NewHSlider (value float64) (element *Slider) {
|
||||
element = &Slider { }
|
||||
element.value = value
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.construct()
|
||||
return
|
||||
}
|
||||
|
||||
type slider struct {
|
||||
entity tomo.Entity
|
||||
|
||||
c tomo.Case
|
||||
|
||||
value float64
|
||||
vertical bool
|
||||
dragging bool
|
||||
enabled bool
|
||||
dragOffset int
|
||||
track image.Rectangle
|
||||
bar image.Rectangle
|
||||
|
||||
onSlide func ()
|
||||
onRelease func ()
|
||||
}
|
||||
|
||||
func (element *slider) construct () {
|
||||
element.enabled = true
|
||||
if element.vertical {
|
||||
element.c = tomo.C("tomo", "sliderVertical")
|
||||
} else {
|
||||
element.c = tomo.C("tomo", "sliderHorizontal")
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *slider) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *slider) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.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)
|
||||
barOffset :=
|
||||
float64(element.track.Dy() - barSize) *
|
||||
(1 - element.value)
|
||||
element.bar = element.bar.Add(image.Pt(0, int(barOffset)))
|
||||
} else {
|
||||
barSize := element.track.Dy()
|
||||
element.bar = image.Rect(0, 0, barSize, barSize).Add(element.track.Min)
|
||||
barOffset :=
|
||||
float64(element.track.Dx() - barSize) *
|
||||
element.value
|
||||
element.bar = element.bar.Add(image.Pt(int(barOffset), 0))
|
||||
}
|
||||
|
||||
state := tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.dragging,
|
||||
}
|
||||
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.
|
||||
func (element *slider) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this slider can be dragged or not.
|
||||
func (element *slider) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this slider can be dragged or not.
|
||||
func (element *slider) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *slider) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *slider) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
if button == input.ButtonLeft {
|
||||
element.dragging = true
|
||||
element.value = element.valueFor(position.X, position.Y)
|
||||
if element.onSlide != nil {
|
||||
element.onSlide()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *slider) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft || !element.dragging { return }
|
||||
element.dragging = false
|
||||
if element.onRelease != nil {
|
||||
element.onRelease()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *slider) HandleMotion (position image.Point) {
|
||||
if element.dragging {
|
||||
element.dragging = true
|
||||
element.value = element.valueFor(position.X, position.Y)
|
||||
if element.onSlide != nil {
|
||||
element.onSlide()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *slider) HandleScroll (
|
||||
position image.Point,
|
||||
deltaX, deltaY float64,
|
||||
modifiers input.Modifiers,
|
||||
) { }
|
||||
|
||||
func (element *slider) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
switch key {
|
||||
case input.KeyUp:
|
||||
element.changeValue(0.1)
|
||||
case input.KeyDown:
|
||||
element.changeValue(-0.1)
|
||||
case input.KeyRight:
|
||||
if element.vertical {
|
||||
element.changeValue(-0.1)
|
||||
} else {
|
||||
element.changeValue(0.1)
|
||||
}
|
||||
case input.KeyLeft:
|
||||
if element.vertical {
|
||||
element.changeValue(0.1)
|
||||
} else {
|
||||
element.changeValue(-0.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *slider) HandleKeyUp (key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
// Value returns the slider's value.
|
||||
func (element *slider) Value () (value float64) {
|
||||
return element.value
|
||||
}
|
||||
|
||||
// SetValue sets the slider's value.
|
||||
func (element *slider) SetValue (value float64) {
|
||||
if value < 0 { value = 0 }
|
||||
if value > 1 { value = 1 }
|
||||
|
||||
if element.value == value { return }
|
||||
|
||||
element.value = value
|
||||
if element.onRelease != nil {
|
||||
element.onRelease()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// OnSlide sets a function to be called every time the slider handle changes
|
||||
// position while being dragged.
|
||||
func (element *slider) OnSlide (callback func ()) {
|
||||
element.onSlide = callback
|
||||
}
|
||||
|
||||
// OnRelease sets a function to be called when the handle stops being dragged.
|
||||
func (element *slider) OnRelease (callback func ()) {
|
||||
element.onRelease = callback
|
||||
}
|
||||
|
||||
func (element *slider) HandleThemeChange () {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
|
||||
func (element *slider) changeValue (delta float64) {
|
||||
element.value += delta
|
||||
if element.value < 0 {
|
||||
element.value = 0
|
||||
}
|
||||
if element.value > 1 {
|
||||
element.value = 1
|
||||
}
|
||||
if element.onRelease != nil {
|
||||
element.onRelease()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *slider) valueFor (x, y int) (value float64) {
|
||||
if element.vertical {
|
||||
value =
|
||||
float64(y - element.track.Min.Y - element.bar.Dy() / 2) /
|
||||
float64(element.track.Dy() - element.bar.Dy())
|
||||
value = 1 - value
|
||||
} else {
|
||||
value =
|
||||
float64(x - element.track.Min.X - element.bar.Dx() / 2) /
|
||||
float64(element.track.Dx() - element.bar.Dx())
|
||||
}
|
||||
|
||||
if value < 0 { value = 0 }
|
||||
if value > 1 { value = 1 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *slider) updateMinimumSize () {
|
||||
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(),
|
||||
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
|
||||
} else {
|
||||
element.entity.SetMinimumSize (
|
||||
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
|
||||
gutterPadding.Vertical() + handlePadding.Vertical())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package elements
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
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
|
||||
}
|
||||
|
||||
// NewSpacer creates a new spacer.
|
||||
func NewSpacer () (element *Spacer) {
|
||||
element = &Spacer { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// NewLine creates a new line separator.
|
||||
func NewLine () (element *Spacer) {
|
||||
element = NewSpacer()
|
||||
element.SetLine(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Spacer) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *Spacer) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
if element.line {
|
||||
pattern := element.entity.Theme().Pattern (
|
||||
tomo.PatternLine,
|
||||
tomo.State { }, spacerCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
} else {
|
||||
pattern := element.entity.Theme().Pattern (
|
||||
tomo.PatternBackground,
|
||||
tomo.State { }, spacerCase)
|
||||
pattern.Draw(destination, bounds)
|
||||
}
|
||||
}
|
||||
|
||||
/// SetLine sets whether or not the spacer will appear as a colored line.
|
||||
func (element *Spacer) SetLine (line bool) {
|
||||
if element.line == line { return }
|
||||
element.line = line
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Spacer) HandleThemeChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Spacer) updateMinimumSize () {
|
||||
if element.line {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternLine, spacerCase)
|
||||
element.entity.SetMinimumSize (
|
||||
padding.Horizontal(),
|
||||
padding.Vertical())
|
||||
} else {
|
||||
element.entity.SetMinimumSize(1, 1)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,24 @@
|
||||
package basicElements
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
|
||||
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 {
|
||||
*core.Core
|
||||
*core.FocusableCore
|
||||
core core.CoreControl
|
||||
focusableControl core.FocusableCoreControl
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
checked bool
|
||||
text string
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onToggle func ()
|
||||
}
|
||||
|
||||
@@ -31,50 +26,111 @@ type Switch struct {
|
||||
func NewSwitch (text string, on bool) (element *Switch) {
|
||||
element = &Switch {
|
||||
checked: on,
|
||||
text: text,
|
||||
text: text,
|
||||
enabled: true,
|
||||
}
|
||||
element.theme.Case = theme.C("basic", "switch")
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.FocusableCore,
|
||||
element.focusableControl = core.NewFocusableCore(element.redo)
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, switchCase))
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Switch) HandleMouseDown (x, y int, button input.Button) {
|
||||
// Entity returns this element's entity.
|
||||
func (element *Switch) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination 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)
|
||||
|
||||
state := tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
On: element.checked,
|
||||
}
|
||||
|
||||
element.entity.DrawBackground(destination)
|
||||
|
||||
if element.checked {
|
||||
handleBounds.Min.X += bounds.Dy()
|
||||
handleBounds.Max.X += bounds.Dy()
|
||||
if element.pressed {
|
||||
handleBounds.Min.X -= 2
|
||||
handleBounds.Max.X -= 2
|
||||
}
|
||||
} else {
|
||||
if element.pressed {
|
||||
handleBounds.Min.X += 2
|
||||
handleBounds.Max.X += 2
|
||||
}
|
||||
}
|
||||
|
||||
gutterPattern := element.entity.Theme().Pattern (
|
||||
tomo.PatternGutter, state, switchCase)
|
||||
gutterPattern.Draw(destination, gutterBounds)
|
||||
|
||||
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.entity.Theme().Margin(tomo.PatternBackground, switchCase).X,
|
||||
})
|
||||
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, switchCase)
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
|
||||
func (element *Switch) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Switch) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
element.pressed = true
|
||||
element.redo()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Switch) HandleMouseUp (x, y int, button input.Button) {
|
||||
func (element *Switch) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft || !element.pressed { return }
|
||||
|
||||
element.pressed = false
|
||||
within := image.Point { x, y }.
|
||||
In(element.Bounds())
|
||||
within := position.In(element.entity.Bounds())
|
||||
if within {
|
||||
element.checked = !element.checked
|
||||
}
|
||||
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
if within && element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Switch) HandleMouseMove (x, y int) { }
|
||||
func (element *Switch) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
|
||||
func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.redo()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +138,7 @@ func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.checked = !element.checked
|
||||
element.redo()
|
||||
element.entity.Invalidate()
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
@@ -99,9 +155,21 @@ func (element *Switch) Value () (on bool) {
|
||||
return element.checked
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this switch can be flipped or not.
|
||||
// Focus gives this element input focus.
|
||||
func (element *Switch) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this switch is enabled or not.
|
||||
func (element *Switch) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this switch can be toggled or not.
|
||||
func (element *Switch) SetEnabled (enabled bool) {
|
||||
element.focusableControl.SetEnabled(enabled)
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetText sets the checkbox's label text.
|
||||
@@ -111,33 +179,15 @@ func (element *Switch) SetText (text string) {
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *Switch) SetTheme (new theme.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
element.drawer.SetFace (element.theme.FontFace (
|
||||
theme.FontStyleRegular,
|
||||
theme.FontSizeNormal))
|
||||
func (element *Switch) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, switchCase))
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *Switch) SetConfig (new config.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *Switch) redo () {
|
||||
if element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Switch) updateMinimumSize () {
|
||||
@@ -145,62 +195,12 @@ func (element *Switch) updateMinimumSize () {
|
||||
lineHeight := element.drawer.LineHeight().Round()
|
||||
|
||||
if element.text == "" {
|
||||
element.core.SetMinimumSize(lineHeight * 2, lineHeight)
|
||||
element.entity.SetMinimumSize(lineHeight * 2, lineHeight)
|
||||
} else {
|
||||
element.core.SetMinimumSize (
|
||||
element.entity.SetMinimumSize (
|
||||
lineHeight * 2 +
|
||||
element.theme.Margin(theme.PatternBackground).X +
|
||||
element.entity.Theme().Margin(tomo.PatternBackground, switchCase).X +
|
||||
textBounds.Dx(),
|
||||
lineHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Switch) draw () {
|
||||
bounds := element.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)
|
||||
|
||||
state := theme.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
Pressed: element.pressed,
|
||||
}
|
||||
backgroundPattern := element.theme.Pattern (
|
||||
theme.PatternBackground, state)
|
||||
backgroundPattern.Draw(element.core, bounds)
|
||||
|
||||
if element.checked {
|
||||
handleBounds.Min.X += bounds.Dy()
|
||||
handleBounds.Max.X += bounds.Dy()
|
||||
if element.pressed {
|
||||
handleBounds.Min.X -= 2
|
||||
handleBounds.Max.X -= 2
|
||||
}
|
||||
} else {
|
||||
if element.pressed {
|
||||
handleBounds.Min.X += 2
|
||||
handleBounds.Max.X += 2
|
||||
}
|
||||
}
|
||||
|
||||
gutterPattern := element.theme.Pattern (
|
||||
theme.PatternGutter, state)
|
||||
artist.DrawBounds(element.core, gutterPattern, gutterBounds)
|
||||
|
||||
handlePattern := element.theme.Pattern (
|
||||
theme.PatternHandle, state)
|
||||
artist.DrawBounds(element.core, handlePattern, handleBounds)
|
||||
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset := bounds.Min.Add(image.Point {
|
||||
X: bounds.Dy() * 2 +
|
||||
element.theme.Margin(theme.PatternBackground).X,
|
||||
})
|
||||
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
|
||||
foreground := element.theme.Color (
|
||||
theme.ColorForeground, state)
|
||||
element.drawer.Draw(element.core, foreground, offset)
|
||||
}
|
||||
+82
-68
@@ -4,71 +4,73 @@ import "fmt"
|
||||
import "time"
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
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/defaultfont"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/artutil"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
|
||||
|
||||
// Artist is an element that displays shapes and patterns drawn by the artist
|
||||
// package in order to test it.
|
||||
type Artist struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
entity tomo.Entity
|
||||
}
|
||||
|
||||
// NewArtist creates a new artist test element.
|
||||
func NewArtist () (element *Artist) {
|
||||
element = &Artist { }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.core.SetMinimumSize(240, 240)
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.entity.SetMinimumSize(240, 240)
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Artist) draw () {
|
||||
bounds := element.Bounds()
|
||||
patterns.Uhex(0x000000FF).Draw(element.core, bounds)
|
||||
func (element *Artist) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
func (element *Artist) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
patterns.Uhex(0x000000FF).Draw(destination, bounds)
|
||||
|
||||
drawStart := time.Now()
|
||||
|
||||
// 0, 0 - 3, 0
|
||||
for x := 0; x < 4; x ++ {
|
||||
element.colorLines(x + 1, element.cellAt(x, 0).Bounds())
|
||||
element.colorLines(destination, x + 1, element.cellAt(destination, x, 0).Bounds())
|
||||
}
|
||||
|
||||
// 4, 0
|
||||
c40 := element.cellAt(4, 0)
|
||||
shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1)
|
||||
c40 := element.cellAt(destination, 4, 0)
|
||||
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(0, 1)
|
||||
shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1)
|
||||
shapes.FillColorEllipse(element.core, artist.Hex(0x00FF00FF), c01.Bounds())
|
||||
c01 := element.cellAt(destination, 0, 1)
|
||||
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(x, 1)
|
||||
c := element.cellAt(destination, x, 1)
|
||||
shapes.StrokeColorRectangle (
|
||||
element.core, artist.Hex(0x888888FF),
|
||||
destination, artutil.Hex(0x888888FF),
|
||||
c.Bounds(), 1)
|
||||
shapes.StrokeColorEllipse (
|
||||
element.core,
|
||||
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)
|
||||
}
|
||||
|
||||
// 4, 1
|
||||
c41 := element.cellAt(4, 1)
|
||||
c41 := element.cellAt(destination, 4, 1)
|
||||
shatterPos := c41.Bounds().Min
|
||||
rocks := []image.Rectangle {
|
||||
image.Rect(3, 12, 13, 23).Add(shatterPos),
|
||||
@@ -79,117 +81,129 @@ func (element *Artist) draw () {
|
||||
}
|
||||
tiles := shatter.Shatter(c41.Bounds(), rocks...)
|
||||
for index, tile := range tiles {
|
||||
artist.DrawBounds (
|
||||
element.core,
|
||||
[]artist.Pattern {
|
||||
patterns.Uhex(0xFF0000FF),
|
||||
patterns.Uhex(0x00FF00FF),
|
||||
patterns.Uhex(0xFF00FFFF),
|
||||
patterns.Uhex(0xFFFF00FF),
|
||||
patterns.Uhex(0x00FFFFFF),
|
||||
} [index % 5], tile)
|
||||
[]artist.Pattern {
|
||||
patterns.Uhex(0xFF0000FF),
|
||||
patterns.Uhex(0x00FF00FF),
|
||||
patterns.Uhex(0xFF00FFFF),
|
||||
patterns.Uhex(0xFFFF00FF),
|
||||
patterns.Uhex(0x00FFFFFF),
|
||||
} [index % 5].Draw(destination, tile)
|
||||
}
|
||||
|
||||
// 0, 2
|
||||
c02 := element.cellAt(0, 2)
|
||||
shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
|
||||
shapes.FillEllipse(c02, c41)
|
||||
c02 := element.cellAt(destination, 0, 2)
|
||||
shapes.StrokeColorRectangle(c02, artutil.Hex(0x888888FF), c02.Bounds(), 1)
|
||||
shapes.FillEllipse(c02, c41, c02.Bounds())
|
||||
|
||||
// 1, 2
|
||||
c12 := element.cellAt(1, 2)
|
||||
shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
|
||||
shapes.StrokeEllipse(c12, c41, 5)
|
||||
c12 := element.cellAt(destination, 1, 2)
|
||||
shapes.StrokeColorRectangle(c12, artutil.Hex(0x888888FF), c12.Bounds(), 1)
|
||||
shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5)
|
||||
|
||||
// 2, 2
|
||||
c22 := element.cellAt(2, 2)
|
||||
shapes.FillRectangle(c22, c41)
|
||||
c22 := element.cellAt(destination, 2, 2)
|
||||
shapes.FillRectangle(c22, c41, c22.Bounds())
|
||||
|
||||
// 3, 2
|
||||
c32 := element.cellAt(3, 2)
|
||||
shapes.StrokeRectangle(c32, c41, 5)
|
||||
c32 := element.cellAt(destination, 3, 2)
|
||||
shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5)
|
||||
|
||||
// 4, 2
|
||||
c42 := element.cellAt(4, 2)
|
||||
c42 := element.cellAt(destination, 4, 2)
|
||||
|
||||
// 0, 3
|
||||
c03 := element.cellAt(0, 3)
|
||||
c03 := element.cellAt(destination, 0, 3)
|
||||
patterns.Border {
|
||||
Canvas: element.thingy(c42),
|
||||
Inset: artist.Inset { 8, 8, 8, 8 },
|
||||
}.Draw(c03, c03.Bounds())
|
||||
|
||||
// 1, 3
|
||||
c13 := element.cellAt(1, 3)
|
||||
c13 := element.cellAt(destination, 1, 3)
|
||||
patterns.Border {
|
||||
Canvas: element.thingy(c42),
|
||||
Inset: artist.Inset { 8, 8, 8, 8 },
|
||||
}.Draw(c13, c13.Bounds().Inset(10))
|
||||
|
||||
// 2, 3
|
||||
c23 := element.cellAt(destination, 2, 3)
|
||||
patterns.Border {
|
||||
Canvas: element.thingy(c42),
|
||||
Inset: artist.Inset { 8, 8, 8, 8 },
|
||||
}.Draw(c23, c23.Bounds())
|
||||
patterns.Border {
|
||||
Canvas: element.thingy(c42),
|
||||
Inset: artist.Inset { 8, 8, 8, 8 },
|
||||
}.Draw(artist.Cut(c23, c23.Bounds().Inset(16)), c23.Bounds())
|
||||
|
||||
// how long did that take to render?
|
||||
drawTime := time.Since(drawStart)
|
||||
textDrawer := textdraw.Drawer { }
|
||||
textDrawer.SetFace(defaultfont.FaceRegular)
|
||||
textDrawer.SetFace(element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
tomo.C("tomo", "artist")))
|
||||
textDrawer.SetText ([]rune (fmt.Sprintf (
|
||||
"%dms\n%dus",
|
||||
drawTime.Milliseconds(),
|
||||
drawTime.Microseconds())))
|
||||
textDrawer.Draw (
|
||||
element.core, artist.Hex(0xFFFFFFFF),
|
||||
destination, artutil.Hex(0xFFFFFFFF),
|
||||
image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
|
||||
}
|
||||
|
||||
func (element *Artist) colorLines (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)
|
||||
shapes.ColorLine(element.core, c, weight, bounds.Min, bounds.Max)
|
||||
c := artutil.Hex(0xFFFFFFFF)
|
||||
shapes.ColorLine(destination, c, weight, bounds.Min, bounds.Max)
|
||||
shapes.ColorLine (
|
||||
element.core, c, weight,
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Max.X, bounds.Min.Y),
|
||||
image.Pt(bounds.Min.X, bounds.Max.Y))
|
||||
shapes.ColorLine (
|
||||
element.core, c, weight,
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Max.X, bounds.Min.Y + 16),
|
||||
image.Pt(bounds.Min.X, bounds.Max.Y - 16))
|
||||
shapes.ColorLine (
|
||||
element.core, c, weight,
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Min.X, bounds.Min.Y + 16),
|
||||
image.Pt(bounds.Max.X, bounds.Max.Y - 16))
|
||||
shapes.ColorLine (
|
||||
element.core, c, weight,
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Min.X + 20, bounds.Min.Y),
|
||||
image.Pt(bounds.Max.X - 20, bounds.Max.Y))
|
||||
shapes.ColorLine (
|
||||
element.core, c, weight,
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Max.X - 20, bounds.Min.Y),
|
||||
image.Pt(bounds.Min.X + 20, bounds.Max.Y))
|
||||
shapes.ColorLine (
|
||||
element.core, c, weight,
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
|
||||
image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2))
|
||||
shapes.ColorLine (
|
||||
element.core, c, weight,
|
||||
destination, c, weight,
|
||||
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y),
|
||||
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y))
|
||||
}
|
||||
|
||||
func (element *Artist) cellAt (x, y int) (canvas.Canvas) {
|
||||
bounds := element.Bounds()
|
||||
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 (element.core, 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)
|
||||
}
|
||||
|
||||
+50
-58
@@ -1,94 +1,86 @@
|
||||
package testing
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/config"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||
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.
|
||||
type Mouse struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
drawing bool
|
||||
entity tomo.Entity
|
||||
pressed bool
|
||||
lastMousePos image.Point
|
||||
|
||||
config config.Config
|
||||
theme theme.Theme
|
||||
c theme.Case
|
||||
}
|
||||
|
||||
// NewMouse creates a new mouse test element.
|
||||
func NewMouse () (element *Mouse) {
|
||||
element = &Mouse { c: theme.C("testing", "mouse") }
|
||||
element.Core, element.core = core.NewCore(element.draw)
|
||||
element.core.SetMinimumSize(32, 32)
|
||||
element = &Mouse { }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.entity.SetMinimumSize(32, 32)
|
||||
return
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *Mouse) SetTheme (new theme.Theme) {
|
||||
element.theme = new
|
||||
element.redo()
|
||||
func (element *Mouse) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *Mouse) SetConfig (new config.Config) {
|
||||
element.config = new
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *Mouse) redo () {
|
||||
if !element.core.HasImage() { return }
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
|
||||
func (element *Mouse) draw () {
|
||||
bounds := element.Bounds()
|
||||
accent := element.theme.Color (
|
||||
theme.ColorAccent,
|
||||
theme.State { },
|
||||
element.c)
|
||||
shapes.FillColorRectangle(element.core, accent, bounds)
|
||||
func (element *Mouse) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
accent := element.entity.Theme().Color (
|
||||
tomo.ColorAccent,
|
||||
tomo.State { },
|
||||
mouseCase)
|
||||
shapes.FillColorRectangle(destination, accent, bounds)
|
||||
shapes.StrokeColorRectangle (
|
||||
element.core,
|
||||
artist.Hex(0x000000FF),
|
||||
destination,
|
||||
artutil.Hex(0x000000FF),
|
||||
bounds, 1)
|
||||
shapes.ColorLine (
|
||||
element.core, 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 (
|
||||
element.core, 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, artutil.Hex(0x000000FF), 1,
|
||||
midpoint, element.lastMousePos)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Mouse) HandleMouseDown (x, y int, button input.Button) {
|
||||
element.drawing = true
|
||||
element.lastMousePos = image.Pt(x, y)
|
||||
func (element *Mouse) HandleThemeChange (new tomo.Theme) {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Mouse) HandleMouseUp (x, y int, button input.Button) {
|
||||
element.drawing = false
|
||||
mousePos := image.Pt(x, y)
|
||||
element.core.DamageRegion (shapes.ColorLine (
|
||||
element.core, artist.Hex(0x000000FF), 1,
|
||||
element.lastMousePos, mousePos))
|
||||
element.lastMousePos = mousePos
|
||||
func (element *Mouse) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
element.pressed = true
|
||||
element.lastMousePos = position
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Mouse) HandleMouseMove (x, y int) {
|
||||
if !element.drawing { return }
|
||||
mousePos := image.Pt(x, y)
|
||||
element.core.DamageRegion (shapes.ColorLine (
|
||||
element.core, artist.Hex(0x000000FF), 1,
|
||||
element.lastMousePos, mousePos))
|
||||
element.lastMousePos = mousePos
|
||||
func (element *Mouse) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
element.pressed = false
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *Mouse) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||
func (element *Mouse) HandleMotion (position image.Point) {
|
||||
if !element.pressed { return }
|
||||
element.lastMousePos = position
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
package elements
|
||||
|
||||
import "io"
|
||||
import "time"
|
||||
import "image"
|
||||
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/textdraw"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
||||
|
||||
var textBoxCase = tomo.C("tomo", "textBox")
|
||||
|
||||
// TextBox is a single-line text input.
|
||||
type TextBox struct {
|
||||
entity tomo.Entity
|
||||
|
||||
enabled bool
|
||||
lastClick time.Time
|
||||
dragging int
|
||||
dot textmanip.Dot
|
||||
scroll int
|
||||
placeholder string
|
||||
text []rune
|
||||
|
||||
placeholderDrawer textdraw.Drawer
|
||||
valueDrawer textdraw.Drawer
|
||||
|
||||
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
|
||||
onChange func ()
|
||||
onEnter func ()
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
// NewTextBox creates a new text box with the specified placeholder text, and
|
||||
// a value. When the value is empty, the placeholder will be displayed in gray
|
||||
// text.
|
||||
func NewTextBox (placeholder, value string) (element *TextBox) {
|
||||
element = &TextBox { enabled: true }
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.placeholder = placeholder
|
||||
element.placeholderDrawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, textBoxCase))
|
||||
element.valueDrawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, textBoxCase))
|
||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||
element.updateMinimumSize()
|
||||
element.SetValue(value)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *TextBox) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *TextBox) Draw (destination artist.Canvas) {
|
||||
bounds := element.entity.Bounds()
|
||||
|
||||
state := element.state()
|
||||
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.entity.Theme().Color(tomo.ColorAccent, state, textBoxCase)
|
||||
canon := element.dot.Canon()
|
||||
foff := fixedutil.Pt(offset)
|
||||
start := element.valueDrawer.PositionAt(canon.Start).Add(foff)
|
||||
end := element.valueDrawer.PositionAt(canon.End).Add(foff)
|
||||
end.Y += element.valueDrawer.LineHeight()
|
||||
shapes.FillColorRectangle (
|
||||
innerCanvas,
|
||||
accent,
|
||||
image.Rectangle {
|
||||
fixedutil.RoundPt(start),
|
||||
fixedutil.RoundPt(end),
|
||||
})
|
||||
}
|
||||
|
||||
if len(element.text) == 0 {
|
||||
// draw placeholder
|
||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||
foreground := element.entity.Theme().Color (
|
||||
tomo.ColorForeground,
|
||||
tomo.State { Disabled: true }, textBoxCase)
|
||||
element.placeholderDrawer.Draw (
|
||||
innerCanvas,
|
||||
foreground,
|
||||
offset.Sub(textBounds.Min))
|
||||
} else {
|
||||
// draw input value
|
||||
textBounds := element.valueDrawer.LayoutBounds()
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
|
||||
element.valueDrawer.Draw (
|
||||
innerCanvas,
|
||||
foreground,
|
||||
offset.Sub(textBounds.Min))
|
||||
}
|
||||
|
||||
if element.entity.Focused() && element.dot.Empty() {
|
||||
// draw cursor
|
||||
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
|
||||
cursorPosition := fixedutil.RoundPt (
|
||||
element.valueDrawer.PositionAt(element.dot.End))
|
||||
shapes.ColorLine (
|
||||
innerCanvas,
|
||||
foreground, 1,
|
||||
cursorPosition.Add(offset),
|
||||
image.Pt (
|
||||
cursorPosition.X,
|
||||
cursorPosition.Y + element.valueDrawer.
|
||||
LineHeight().Round()).Add(offset))
|
||||
}
|
||||
}
|
||||
|
||||
// Layout causes the element to perform a layout operation.
|
||||
func (element *TextBox) Layout () {
|
||||
element.scrollToCursor()
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
|
||||
switch button {
|
||||
case input.ButtonLeft:
|
||||
runeIndex := element.atPosition(position)
|
||||
if runeIndex == -1 { return }
|
||||
|
||||
if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() {
|
||||
element.dragging = 2
|
||||
element.dot = textmanip.WordAround(element.text, runeIndex)
|
||||
} else {
|
||||
element.dragging = 1
|
||||
element.dot = textmanip.EmptyDot(runeIndex)
|
||||
element.lastClick = time.Now()
|
||||
}
|
||||
|
||||
element.entity.Invalidate()
|
||||
case input.ButtonRight:
|
||||
element.contextMenu(position)
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button == input.ButtonLeft {
|
||||
element.dragging = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleMotion (position image.Point) {
|
||||
if !element.Enabled() { return }
|
||||
|
||||
switch element.dragging {
|
||||
case 1:
|
||||
runeIndex := element.atPosition(position)
|
||||
if runeIndex > -1 {
|
||||
element.dot.End = runeIndex
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
case 2:
|
||||
runeIndex := element.atPosition(position)
|
||||
if runeIndex > -1 {
|
||||
if runeIndex < element.dot.Start {
|
||||
element.dot.End =
|
||||
runeIndex -
|
||||
textmanip.WordToLeft (
|
||||
element.text,
|
||||
runeIndex)
|
||||
} else {
|
||||
element.dot.End =
|
||||
runeIndex +
|
||||
textmanip.WordToRight (
|
||||
element.text,
|
||||
runeIndex)
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) textOffset () image.Point {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
|
||||
bounds := element.entity.Bounds()
|
||||
innerBounds := padding.Apply(bounds)
|
||||
textHeight := element.valueDrawer.LineHeight().Round()
|
||||
return bounds.Min.Add (image.Pt (
|
||||
padding[artist.SideLeft] - element.scroll,
|
||||
padding[artist.SideTop] + (innerBounds.Dy() - textHeight) / 2))
|
||||
}
|
||||
|
||||
func (element *TextBox) atPosition (position image.Point) int {
|
||||
offset := element.textOffset()
|
||||
textBoundsMin := element.valueDrawer.LayoutBounds().Min
|
||||
return element.valueDrawer.AtPosition (
|
||||
fixedutil.Pt(position.Sub(offset).Add(textBoundsMin)))
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) {
|
||||
if element.onKeyDown != nil && element.onKeyDown(key, modifiers) {
|
||||
return
|
||||
}
|
||||
|
||||
scrollMemory := element.scroll
|
||||
textChanged := false
|
||||
switch {
|
||||
case key == input.KeyEnter:
|
||||
if element.onEnter != nil {
|
||||
element.onEnter()
|
||||
}
|
||||
|
||||
case key == input.KeyBackspace:
|
||||
if len(element.text) < 1 { break }
|
||||
element.text, element.dot = textmanip.Backspace (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
textChanged = true
|
||||
|
||||
case key == input.KeyDelete:
|
||||
if len(element.text) < 1 { break }
|
||||
element.text, element.dot = textmanip.Delete (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
textChanged = true
|
||||
|
||||
case key == input.KeyLeft:
|
||||
if modifiers.Shift {
|
||||
element.dot = textmanip.SelectLeft (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
} else {
|
||||
element.dot = textmanip.MoveLeft (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
}
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
|
||||
case key == input.KeyRight:
|
||||
if modifiers.Shift {
|
||||
element.dot = textmanip.SelectRight (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
} else {
|
||||
element.dot = textmanip.MoveRight (
|
||||
element.text,
|
||||
element.dot,
|
||||
modifiers.Control)
|
||||
}
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
|
||||
case key == 'a' && modifiers.Control:
|
||||
element.dot.Start = 0
|
||||
element.dot.End = len(element.text)
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
|
||||
case key == 'x' && modifiers.Control: element.Cut()
|
||||
case key == 'c' && modifiers.Control: element.Copy()
|
||||
case key == 'v' && modifiers.Control: element.Paste()
|
||||
|
||||
case key == input.KeyMenu:
|
||||
pos := fixedutil.RoundPt(element.valueDrawer.PositionAt(element.dot.End)).
|
||||
Add(element.textOffset())
|
||||
pos.Y += element.valueDrawer.LineHeight().Round()
|
||||
element.contextMenu(pos)
|
||||
|
||||
case key.Printable():
|
||||
element.text, element.dot = textmanip.Type (
|
||||
element.text,
|
||||
element.dot,
|
||||
rune(key))
|
||||
textChanged = true
|
||||
}
|
||||
|
||||
if textChanged {
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
if (textChanged || scrollMemory != element.scroll) {
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Cut cuts the selected text in the text box and places it in the clipboard.
|
||||
func (element *TextBox) Cut () {
|
||||
var lifted []rune
|
||||
element.text, element.dot, lifted = textmanip.Lift (
|
||||
element.text,
|
||||
element.dot)
|
||||
if lifted != nil {
|
||||
element.clipboardPut(lifted)
|
||||
element.notifyAsyncTextChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Copy copies the selected text in the text box and places it in the clipboard.
|
||||
func (element *TextBox) Copy () {
|
||||
element.clipboardPut(element.dot.Slice(element.text))
|
||||
}
|
||||
|
||||
// Paste pastes text data from the clipboard into the text box.
|
||||
func (element *TextBox) Paste () {
|
||||
window := element.entity.Window()
|
||||
if window == nil { return }
|
||||
window.Paste (func (d data.Data, err error) {
|
||||
if err != nil { return }
|
||||
reader, ok := d[data.MimePlain]
|
||||
if !ok { return }
|
||||
bytes, _ := io.ReadAll(reader)
|
||||
element.text, element.dot = textmanip.Type (
|
||||
element.text,
|
||||
element.dot,
|
||||
[]rune(string(bytes))...)
|
||||
element.notifyAsyncTextChange()
|
||||
})
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
// SetPlaceholder sets the element's placeholder text.
|
||||
func (element *TextBox) SetPlaceholder (placeholder string) {
|
||||
if element.placeholder == placeholder { return }
|
||||
|
||||
element.placeholder = placeholder
|
||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetValue sets the input's value.
|
||||
func (element *TextBox) SetValue (text string) {
|
||||
// if element.text == text { return }
|
||||
|
||||
element.text = []rune(text)
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
if element.dot.End > element.valueDrawer.Length() {
|
||||
element.dot = textmanip.EmptyDot(element.valueDrawer.Length())
|
||||
}
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// Value returns the input's value.
|
||||
func (element *TextBox) Value () (value string) {
|
||||
return string(element.text)
|
||||
}
|
||||
|
||||
// Filled returns whether or not this element has a value.
|
||||
func (element *TextBox) Filled () (filled bool) {
|
||||
return len(element.text) > 0
|
||||
}
|
||||
|
||||
// OnKeyDown specifies a function to be called when a key is pressed within the
|
||||
// text input.
|
||||
func (element *TextBox) OnKeyDown (
|
||||
callback func (key input.Key, modifiers input.Modifiers) (handled bool),
|
||||
) {
|
||||
element.onKeyDown = callback
|
||||
}
|
||||
|
||||
// OnEnter specifies a function to be called when the enter key is pressed
|
||||
// within this input.
|
||||
func (element *TextBox) OnEnter (callback func ()) {
|
||||
element.onEnter = callback
|
||||
}
|
||||
|
||||
// OnChange specifies a function to be called when the value of this input
|
||||
// changes.
|
||||
func (element *TextBox) OnChange (callback func ()) {
|
||||
element.onChange = callback
|
||||
}
|
||||
|
||||
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||
// bounds, content bounds, or scroll axes change.
|
||||
func (element *TextBox) OnScrollBoundsChange (callback func ()) {
|
||||
element.onScrollBoundsChange = callback
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *TextBox) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this label can be edited or not.
|
||||
func (element *TextBox) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this label can be edited or not.
|
||||
func (element *TextBox) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// ScrollContentBounds returns the full content size of the element.
|
||||
func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) {
|
||||
bounds = element.valueDrawer.LayoutBounds()
|
||||
return bounds.Sub(bounds.Min)
|
||||
}
|
||||
|
||||
// ScrollViewportBounds returns the size and position of the element's viewport
|
||||
// relative to ScrollBounds.
|
||||
func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
|
||||
return image.Rect (
|
||||
element.scroll,
|
||||
0,
|
||||
element.scroll + element.scrollViewportWidth(),
|
||||
0)
|
||||
}
|
||||
|
||||
// ScrollTo scrolls the viewport to the specified point relative to
|
||||
// ScrollBounds.
|
||||
func (element *TextBox) ScrollTo (position image.Point) {
|
||||
// constrain to minimum
|
||||
element.scroll = position.X
|
||||
if element.scroll < 0 { element.scroll = 0 }
|
||||
|
||||
// constrain to maximum
|
||||
contentBounds := element.ScrollContentBounds()
|
||||
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
|
||||
if element.scroll > maxPosition { element.scroll = maxPosition }
|
||||
|
||||
element.entity.Invalidate()
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
|
||||
return true, false
|
||||
}
|
||||
|
||||
func (element *TextBox) HandleThemeChange () {
|
||||
face := element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
textBoxCase)
|
||||
element.placeholderDrawer.SetFace(face)
|
||||
element.valueDrawer.SetFace(face)
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *TextBox) contextMenu (position image.Point) {
|
||||
window := element.entity.Window()
|
||||
menu, err := window.NewMenu(image.Rectangle { position, position })
|
||||
if err != nil { return }
|
||||
|
||||
closeAnd := func (callback func ()) func () {
|
||||
return func () { callback(); menu.Close() }
|
||||
}
|
||||
|
||||
cutButton := NewButton("Cut")
|
||||
cutButton.ShowText(false)
|
||||
cutButton.SetIcon(tomo.IconCut)
|
||||
cutButton.SetEnabled(!element.dot.Empty())
|
||||
cutButton.OnClick(closeAnd(element.Cut))
|
||||
|
||||
copyButton := NewButton("Copy")
|
||||
copyButton.ShowText(false)
|
||||
copyButton.SetIcon(tomo.IconCopy)
|
||||
copyButton.SetEnabled(!element.dot.Empty())
|
||||
copyButton.OnClick(closeAnd(element.Copy))
|
||||
|
||||
pasteButton := NewButton("Paste")
|
||||
pasteButton.ShowText(false)
|
||||
pasteButton.SetIcon(tomo.IconPaste)
|
||||
pasteButton.OnClick(closeAnd(element.Paste))
|
||||
|
||||
menu.Adopt (NewHBox (
|
||||
SpaceNone,
|
||||
pasteButton,
|
||||
copyButton,
|
||||
cutButton,
|
||||
))
|
||||
pasteButton.Focus()
|
||||
menu.Show()
|
||||
}
|
||||
|
||||
func (element *TextBox) runOnChange () {
|
||||
if element.onChange != nil {
|
||||
element.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) scrollViewportWidth () (width int) {
|
||||
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
|
||||
return padding.Apply(element.entity.Bounds()).Dx()
|
||||
}
|
||||
|
||||
func (element *TextBox) scrollToCursor () {
|
||||
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()
|
||||
cursorPosition := fixedutil.RoundPt (
|
||||
element.valueDrawer.PositionAt(element.dot.End))
|
||||
cursorPosition.X -= element.scroll
|
||||
maxX := bounds.Max.X
|
||||
minX := maxX
|
||||
if cursorPosition.X > maxX {
|
||||
element.scroll += cursorPosition.X - maxX
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
element.entity.Invalidate()
|
||||
} else if cursorPosition.X < minX {
|
||||
element.scroll -= minX - cursorPosition.X
|
||||
if element.scroll < 0 { element.scroll = 0 }
|
||||
element.entity.NotifyScrollBoundsChange()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) updateMinimumSize () {
|
||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
|
||||
element.entity.SetMinimumSize (
|
||||
padding.Horizontal() + textBounds.Dx(),
|
||||
padding.Vertical() +
|
||||
element.placeholderDrawer.LineHeight().Round())
|
||||
}
|
||||
|
||||
func (element *TextBox) notifyAsyncTextChange () {
|
||||
element.runOnChange()
|
||||
element.valueDrawer.SetText(element.text)
|
||||
element.scrollToCursor()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *TextBox) clipboardPut (text []rune) {
|
||||
window := element.entity.Window()
|
||||
if window != nil {
|
||||
window.Copy(data.Bytes(data.MimePlain, []byte(string(text))))
|
||||
}
|
||||
}
|
||||
|
||||
func (element *TextBox) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package elements
|
||||
|
||||
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/textdraw"
|
||||
|
||||
var toggleButtonCase = tomo.C("tomo", "toggleButton")
|
||||
|
||||
// ToggleButton is a togglable button.
|
||||
type ToggleButton struct {
|
||||
entity tomo.Entity
|
||||
drawer textdraw.Drawer
|
||||
|
||||
enabled bool
|
||||
pressed bool
|
||||
on bool
|
||||
text string
|
||||
|
||||
showText bool
|
||||
hasIcon bool
|
||||
iconId tomo.Icon
|
||||
|
||||
onToggle func ()
|
||||
}
|
||||
|
||||
// NewToggleButton creates a new toggle button with the specified label text.
|
||||
func NewToggleButton (text string, on bool) (element *ToggleButton) {
|
||||
element = &ToggleButton {
|
||||
showText: true,
|
||||
enabled: true,
|
||||
on: on,
|
||||
}
|
||||
element.entity = tomo.GetBackend().NewEntity(element)
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal,
|
||||
toggleButtonCase))
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *ToggleButton) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
}
|
||||
|
||||
// Draw causes the element to draw to the specified destination canvas.
|
||||
func (element *ToggleButton) Draw (destination artist.Canvas) {
|
||||
state := element.state()
|
||||
bounds := element.entity.Bounds()
|
||||
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, toggleButtonCase)
|
||||
|
||||
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
|
||||
|
||||
pattern.Draw(destination, bounds)
|
||||
lampPattern.Draw(destination, lampBounds)
|
||||
|
||||
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,
|
||||
bounds.Dy() / 2).Add(bounds.Min)
|
||||
|
||||
if element.showText {
|
||||
textBounds := element.drawer.LayoutBounds()
|
||||
offset.X -= textBounds.Dx() / 2
|
||||
offset.Y -= textBounds.Dy() / 2
|
||||
offset.Y -= textBounds.Min.Y
|
||||
offset.X -= textBounds.Min.X
|
||||
}
|
||||
|
||||
if element.hasIcon {
|
||||
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase)
|
||||
if icon != nil {
|
||||
iconBounds := icon.Bounds()
|
||||
addedWidth := iconBounds.Dx()
|
||||
iconOffset := offset
|
||||
|
||||
if element.showText {
|
||||
addedWidth += margin.X
|
||||
}
|
||||
|
||||
iconOffset.X -= addedWidth / 2
|
||||
iconOffset.Y =
|
||||
bounds.Min.Y +
|
||||
(bounds.Dy() -
|
||||
iconBounds.Dy()) / 2
|
||||
if element.pressed {
|
||||
iconOffset = iconOffset.Add(sink)
|
||||
}
|
||||
offset.X += addedWidth / 2
|
||||
|
||||
icon.Draw(destination, foreground, iconOffset)
|
||||
}
|
||||
}
|
||||
|
||||
if element.showText {
|
||||
if element.pressed {
|
||||
offset = offset.Add(sink)
|
||||
}
|
||||
element.drawer.Draw(destination, foreground, offset)
|
||||
}
|
||||
}
|
||||
|
||||
// OnToggle sets the function to be called when the button is toggled.
|
||||
func (element *ToggleButton) OnToggle (callback func ()) {
|
||||
element.onToggle = callback
|
||||
}
|
||||
|
||||
// Value reports whether or not the button is currently on.
|
||||
func (element *ToggleButton) Value () (on bool) {
|
||||
return element.on
|
||||
}
|
||||
|
||||
// Focus gives this element input focus.
|
||||
func (element *ToggleButton) Focus () {
|
||||
if !element.entity.Focused() { element.entity.Focus() }
|
||||
}
|
||||
|
||||
// Enabled returns whether this button is enabled or not.
|
||||
func (element *ToggleButton) Enabled () bool {
|
||||
return element.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether this button can be toggled or not.
|
||||
func (element *ToggleButton) SetEnabled (enabled bool) {
|
||||
if element.enabled == enabled { return }
|
||||
element.enabled = enabled
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetText sets the button's label text.
|
||||
func (element *ToggleButton) SetText (text string) {
|
||||
if element.text == text { return }
|
||||
element.text = text
|
||||
element.drawer.SetText([]rune(text))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetIcon sets the icon of the button. Passing theme.IconNone removes the
|
||||
// current icon if it exists.
|
||||
func (element *ToggleButton) SetIcon (id tomo.Icon) {
|
||||
if id == tomo.IconNone {
|
||||
element.hasIcon = false
|
||||
} else {
|
||||
if element.hasIcon && element.iconId == id { return }
|
||||
element.hasIcon = true
|
||||
element.iconId = id
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// ShowText sets whether or not the button's text will be displayed.
|
||||
func (element *ToggleButton) ShowText (showText bool) {
|
||||
if element.showText == showText { return }
|
||||
element.showText = showText
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleThemeChange () {
|
||||
element.drawer.SetFace (element.entity.Theme().FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal, toggleButtonCase))
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleFocusChange () {
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleMouseDown (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if !element.Enabled() { return }
|
||||
element.Focus()
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleMouseUp (
|
||||
position image.Point,
|
||||
button input.Button,
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
within := position.In(element.entity.Bounds())
|
||||
if element.Enabled() && within {
|
||||
element.on = !element.on
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
if key == input.KeyEnter {
|
||||
element.pressed = true
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ToggleButton) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||
if key == input.KeyEnter && element.pressed {
|
||||
element.pressed = false
|
||||
element.entity.Invalidate()
|
||||
if !element.Enabled() { return }
|
||||
element.on = !element.on
|
||||
if element.onToggle != nil {
|
||||
element.onToggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *ToggleButton) updateMinimumSize () {
|
||||
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.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase)
|
||||
if icon != nil {
|
||||
bounds := icon.Bounds()
|
||||
if element.showText {
|
||||
minimumSize.Max.X += bounds.Dx()
|
||||
minimumSize.Max.X += margin.X
|
||||
} else {
|
||||
minimumSize.Max.X = bounds.Dx()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
minimumSize.Max.X += lampPadding.Horizontal()
|
||||
minimumSize = padding.Inverse().Apply(minimumSize)
|
||||
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||
}
|
||||
|
||||
func (element *ToggleButton) state () tomo.State {
|
||||
return tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.entity.Focused(),
|
||||
Pressed: element.pressed,
|
||||
On: element.on,
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
|
||||
// Window represents a top-level container generated by the currently running
|
||||
// backend. It can contain a single element. It is hidden by default, and must
|
||||
// be explicitly shown with the Show() method. If it contains no element, it
|
||||
// displays a black (or transprent) background.
|
||||
type Window interface {
|
||||
// Adopt sets the root element of the window. There can only be one of
|
||||
// these at one time.
|
||||
Adopt (child Element)
|
||||
|
||||
// Child returns the root element of the window.
|
||||
Child () (child Element)
|
||||
|
||||
// SetTitle sets the title that appears on the window's title bar. This
|
||||
// method might have no effect with some backends.
|
||||
SetTitle (title string)
|
||||
|
||||
// SetIcon taks in a list different sizes of the same icon and selects
|
||||
// the best one to display on the window title bar, dock, or whatever is
|
||||
// applicable for the given backend. This method might have no effect
|
||||
// for some backends.
|
||||
SetIcon (sizes []image.Image)
|
||||
|
||||
// Show shows the window. The window starts off hidden, so this must be
|
||||
// called after initial setup to make sure it is visible.
|
||||
Show ()
|
||||
|
||||
// Hide hides the window.
|
||||
Hide ()
|
||||
|
||||
// Close closes the window.
|
||||
Close ()
|
||||
|
||||
// OnClose specifies a function to be called when the window is closed.
|
||||
OnClose (func ())
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package tomo
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
// 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
|
||||
|
||||
// Window returns the window that the element is in.
|
||||
Window () Window
|
||||
|
||||
// SetMinimumSize reports to the system what the element's minimum size
|
||||
// can be. The minimum size of child elements should be taken into
|
||||
// account when calculating this.
|
||||
SetMinimumSize (width, height int)
|
||||
|
||||
// DrawBackground asks the parent element to draw its background pattern
|
||||
// to a canvas. This should be used for transparent elements like text
|
||||
// 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 (artist.Canvas)
|
||||
|
||||
// --- Behaviors relating to parenting ---
|
||||
|
||||
// Adopt adds an element as a child.
|
||||
Adopt (child Element)
|
||||
|
||||
// Insert inserts an element in the child list at the specified
|
||||
// location.
|
||||
Insert (index int, child Element)
|
||||
|
||||
// Disown removes the child at the specified index.
|
||||
Disown (index int)
|
||||
|
||||
// IndexOf returns the index of the specified child.
|
||||
IndexOf (child Element) int
|
||||
|
||||
// Child returns the child at the specified index.
|
||||
Child (index int) Element
|
||||
|
||||
// CountChildren returns the amount of children the element has.
|
||||
CountChildren () int
|
||||
|
||||
// PlaceChild sets the size and position of the child at the specified
|
||||
// index to a bounding rectangle.
|
||||
PlaceChild (index int, bounds image.Rectangle)
|
||||
|
||||
// SelectChild marks a child as selected or unselected, if it is
|
||||
// selectable.
|
||||
SelectChild (index int, selected bool)
|
||||
|
||||
// ChildMinimumSize returns the minimum size of the child at the
|
||||
// specified index.
|
||||
ChildMinimumSize (index int) (width, height int)
|
||||
|
||||
// --- Behaviors relating to input focus ---
|
||||
|
||||
// Focused returns whether the element currently has input focus.
|
||||
Focused () bool
|
||||
|
||||
// Focus sets this element as focused. If this succeeds, the element will
|
||||
// recieve a HandleFocus call.
|
||||
Focus ()
|
||||
|
||||
// FocusNext causes the focus to move to the next element. If this
|
||||
// succeeds, the element will recieve a HandleUnfocus call.
|
||||
FocusNext ()
|
||||
|
||||
// FocusPrevious causes the focus to move to the next element. If this
|
||||
// succeeds, the element will recieve a HandleUnfocus call.
|
||||
FocusPrevious ()
|
||||
|
||||
// Selected returns whether this element is currently selected.
|
||||
Selected () bool
|
||||
|
||||
// --- 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 ()
|
||||
|
||||
// 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,22 +0,0 @@
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
import _ "net/http/pprof"
|
||||
import "net/http"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(480, 360)
|
||||
window.SetTitle("Draw Test")
|
||||
window.Adopt(testing.NewArtist())
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
go func () {
|
||||
http.ListenAndServe("localhost:9090", nil)
|
||||
} ()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
window.SetTitle("example button")
|
||||
button := basicElements.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()
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
window.SetTitle("Checkboxes")
|
||||
|
||||
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
|
||||
window.Adopt(container)
|
||||
|
||||
container.Adopt (basicElements.NewLabel (
|
||||
"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.", true), true)
|
||||
container.Adopt(basicElements.NewSpacer(true), false)
|
||||
container.Adopt(basicElements.NewCheckbox("Oh god", false), false)
|
||||
container.Adopt(basicElements.NewCheckbox("Can you hear them", true), false)
|
||||
container.Adopt(basicElements.NewCheckbox("They are in the walls", false), false)
|
||||
container.Adopt(basicElements.NewCheckbox("They are coming for us", false), false)
|
||||
disabledCheckbox := basicElements.NewCheckbox("We are but their helpless prey", false)
|
||||
disabledCheckbox.SetEnabled(false)
|
||||
container.Adopt(disabledCheckbox, false)
|
||||
vsync := basicElements.NewCheckbox("Enable vsync", false)
|
||||
vsync.OnToggle (func () {
|
||||
if vsync.Value() {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindInfo,
|
||||
"Ha!",
|
||||
"That doesn't do anything.")
|
||||
}
|
||||
})
|
||||
container.Adopt(vsync, false)
|
||||
button := basicElements.NewButton("What")
|
||||
button.OnClick(tomo.Stop)
|
||||
container.Adopt(button, false)
|
||||
button.Focus()
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import "io"
|
||||
import "image"
|
||||
import _ "image/png"
|
||||
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"
|
||||
|
||||
var validImageTypes = []data.Mime {
|
||||
data.M("image", "png"),
|
||||
data.M("image", "gif"),
|
||||
data.M("image", "jpeg"),
|
||||
}
|
||||
|
||||
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)
|
||||
textInput := elements.NewTextBox("", "")
|
||||
controlRow := elements.NewHBox(elements.SpaceMargin)
|
||||
copyButton := elements.NewButton("Copy")
|
||||
copyButton.SetIcon(tomo.IconCopy)
|
||||
pasteButton := elements.NewButton("Paste")
|
||||
pasteButton.SetIcon(tomo.IconPaste)
|
||||
pasteImageButton := elements.NewButton("Image")
|
||||
pasteImageButton.SetIcon(tomo.IconPictures)
|
||||
|
||||
imageClipboardCallback := func (clipboard data.Data, err error) {
|
||||
if err != nil {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Error",
|
||||
"Cannot get clipboard:\n" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var imageData io.Reader
|
||||
var ok bool
|
||||
for mime, reader := range clipboard {
|
||||
for _, mimeCheck := range validImageTypes {
|
||||
if mime == mimeCheck {
|
||||
imageData = reader
|
||||
ok = true
|
||||
}}}
|
||||
|
||||
if !ok {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Clipboard Empty",
|
||||
"No image data in clipboard")
|
||||
return
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(imageData)
|
||||
if err != nil {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Error",
|
||||
"Cannot decode image:\n" + err.Error())
|
||||
return
|
||||
}
|
||||
imageWindow(window, img)
|
||||
}
|
||||
clipboardCallback := func (clipboard data.Data, err error) {
|
||||
if err != nil {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Error",
|
||||
"Cannot get clipboard:\n" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
textData, ok := clipboard[data.MimePlain]
|
||||
if !ok {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Clipboard Empty",
|
||||
"No text data in clipboard")
|
||||
return
|
||||
}
|
||||
|
||||
text, _ := io.ReadAll(textData)
|
||||
textInput.SetValue(string(text))
|
||||
}
|
||||
copyButton.OnClick (func () {
|
||||
window.Copy(data.Text(textInput.Value()))
|
||||
})
|
||||
pasteButton.OnClick (func () {
|
||||
window.Paste(clipboardCallback, data.MimePlain)
|
||||
})
|
||||
pasteImageButton.OnClick (func () {
|
||||
window.Paste(imageClipboardCallback, validImageTypes...)
|
||||
})
|
||||
|
||||
container.AdoptExpand(textInput)
|
||||
controlRow.AdoptExpand(copyButton)
|
||||
controlRow.AdoptExpand(pasteButton)
|
||||
controlRow.AdoptExpand(pasteImageButton)
|
||||
container.Adopt(controlRow)
|
||||
window.Adopt(container)
|
||||
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
func imageWindow (parent tomo.Window, image image.Image) {
|
||||
window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0))
|
||||
window.SetTitle("Clipboard Image")
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
closeButton := elements.NewButton("Ok")
|
||||
closeButton.SetIcon(tomo.IconYes)
|
||||
closeButton.OnClick(window.Close)
|
||||
|
||||
container.AdoptExpand(elements.NewImage(image))
|
||||
container.Adopt(closeButton)
|
||||
window.Adopt(container)
|
||||
|
||||
closeButton.Focus()
|
||||
window.Show()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
window.SetTitle("dialog")
|
||||
|
||||
container := basicElements.NewContainer(basicLayouts.Dialog { true, true })
|
||||
window.Adopt(container)
|
||||
|
||||
container.Adopt(basicElements.NewLabel("you will explode", true), true)
|
||||
cancel := basicElements.NewButton("Cancel")
|
||||
cancel.SetEnabled(false)
|
||||
container.Adopt(cancel, false)
|
||||
okButton := basicElements.NewButton("OK")
|
||||
container.Adopt(okButton, false)
|
||||
okButton.Focus()
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
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"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
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 { return err }
|
||||
logo, _, err := image.Decode(file)
|
||||
file.Close()
|
||||
if err != nil { return err }
|
||||
|
||||
document := elements.NewDocument()
|
||||
document.Adopt (
|
||||
elements.NewLabelWrapped (
|
||||
"A document container is a vertically stacked container " +
|
||||
"capable of properly laying out flexible elements such as " +
|
||||
"text-wrapped labels. You can also include normal elements " +
|
||||
"like:"),
|
||||
elements.NewButton("Buttons,"),
|
||||
elements.NewCheckbox("Checkboxes,", true),
|
||||
elements.NewTextBox("", "And text boxes."),
|
||||
elements.NewLine(),
|
||||
elements.NewLabelWrapped (
|
||||
"Document containers are meant to be placed inside of a " +
|
||||
"ScrollContainer, like this one."),
|
||||
elements.NewLabelWrapped (
|
||||
"You could use document containers to do things like display various " +
|
||||
"forms of hypertext (like HTML, gemtext, markdown, etc.), " +
|
||||
"lay out a settings menu with descriptive label text between " +
|
||||
"control groups like in iOS, or list comment or chat histories."),
|
||||
elements.NewImage(logo),
|
||||
elements.NewLabelWrapped (
|
||||
"You can also choose whether each element is on its own line " +
|
||||
"(sort of like an HTML/CSS block element) or on a line with " +
|
||||
"other adjacent elements (like an HTML/CSS inline element)."))
|
||||
document.AdoptInline (
|
||||
elements.NewButton("Just"),
|
||||
elements.NewButton("like"),
|
||||
elements.NewButton("this."))
|
||||
document.Adopt (elements.NewLabelWrapped (
|
||||
"Oh, you're a switch? Then name all of these switches:"))
|
||||
for i := 0; i < 30; i ++ {
|
||||
document.AdoptInline(elements.NewSwitch("", false))
|
||||
}
|
||||
|
||||
window.Adopt(elements.NewScroll(elements.ScrollVertical, document))
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +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/ezprof/ez"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
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(nasin.Stop)
|
||||
window.Show()
|
||||
ez.Prof()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
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, err := os.UserHomeDir()
|
||||
if err != nil { return err }
|
||||
|
||||
controlBar := elements.NewHBox(elements.SpaceNone)
|
||||
backButton := elements.NewButton("Back")
|
||||
backButton.SetIcon(tomo.IconBackward)
|
||||
backButton.ShowText(false)
|
||||
forwardButton := elements.NewButton("Forward")
|
||||
forwardButton.SetIcon(tomo.IconForward)
|
||||
forwardButton.ShowText(false)
|
||||
refreshButton := elements.NewButton("Refresh")
|
||||
refreshButton.SetIcon(tomo.IconRefresh)
|
||||
refreshButton.ShowText(false)
|
||||
upwardButton := elements.NewButton("Go Up")
|
||||
upwardButton.SetIcon(tomo.IconUpward)
|
||||
upwardButton.ShowText(false)
|
||||
locationInput := elements.NewTextBox("Location", "")
|
||||
|
||||
statusBar := elements.NewHBox(elements.SpaceMargin)
|
||||
directory, _ := elements.NewFile(homeDir, nil)
|
||||
baseName := elements.NewLabel(filepath.Base(homeDir))
|
||||
|
||||
directoryView, _ := elements.NewDirectory(homeDir, nil)
|
||||
updateStatus := func () {
|
||||
filePath, _ := directoryView.Location()
|
||||
directory.SetLocation(filePath, nil)
|
||||
locationInput.SetValue(filePath)
|
||||
baseName.SetText(filepath.Base(filePath))
|
||||
}
|
||||
choose := func (filePath string) {
|
||||
directoryView.SetLocation(filePath, nil)
|
||||
updateStatus()
|
||||
}
|
||||
directoryView.OnChoose(choose)
|
||||
locationInput.OnEnter (func () {
|
||||
choose(locationInput.Value())
|
||||
})
|
||||
choose(homeDir)
|
||||
backButton.OnClick (func () {
|
||||
directoryView.Backward()
|
||||
updateStatus()
|
||||
})
|
||||
forwardButton.OnClick (func () {
|
||||
directoryView.Forward()
|
||||
updateStatus()
|
||||
})
|
||||
refreshButton.OnClick (func () {
|
||||
directoryView.Update()
|
||||
updateStatus()
|
||||
})
|
||||
upwardButton.OnClick (func () {
|
||||
filePath, _ := directoryView.Location()
|
||||
choose(filepath.Dir(filePath))
|
||||
})
|
||||
|
||||
controlBar.Adopt(backButton, forwardButton, refreshButton, upwardButton)
|
||||
controlBar.AdoptExpand(locationInput)
|
||||
statusBar.Adopt(directory, baseName)
|
||||
|
||||
container.Adopt(controlBar)
|
||||
container.AdoptExpand (
|
||||
elements.NewScroll(elements.ScrollVertical, directoryView))
|
||||
container.Adopt(statusBar)
|
||||
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/flow"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
window.SetTitle("adventure")
|
||||
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
|
||||
window.Adopt(container)
|
||||
|
||||
var world flow.Flow
|
||||
world.Transition = container.DisownAll
|
||||
world.Stages = map [string] func () {
|
||||
"start": func () {
|
||||
label := basicElements.NewLabel (
|
||||
"you are standing next to a river.", true)
|
||||
|
||||
button0 := basicElements.NewButton("go in the river")
|
||||
button0.OnClick(world.SwitchFunc("wet"))
|
||||
button1 := basicElements.NewButton("walk along the river")
|
||||
button1.OnClick(world.SwitchFunc("house"))
|
||||
button2 := basicElements.NewButton("turn around")
|
||||
button2.OnClick(world.SwitchFunc("bear"))
|
||||
|
||||
container.Warp ( func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button0, false)
|
||||
container.Adopt(button1, false)
|
||||
container.Adopt(button2, false)
|
||||
button0.Focus()
|
||||
})
|
||||
},
|
||||
"wet": func () {
|
||||
label := basicElements.NewLabel (
|
||||
"you get completely soaked.\n" +
|
||||
"you die of hypothermia.", true)
|
||||
|
||||
button0 := basicElements.NewButton("try again")
|
||||
button0.OnClick(world.SwitchFunc("start"))
|
||||
button1 := basicElements.NewButton("exit")
|
||||
button1.OnClick(tomo.Stop)
|
||||
|
||||
container.Warp (func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button0, false)
|
||||
container.Adopt(button1, false)
|
||||
button0.Focus()
|
||||
})
|
||||
},
|
||||
"house": func () {
|
||||
label := basicElements.NewLabel (
|
||||
"you are standing in front of a delapidated " +
|
||||
"house.", true)
|
||||
|
||||
button1 := basicElements.NewButton("go inside")
|
||||
button1.OnClick(world.SwitchFunc("inside"))
|
||||
button0 := basicElements.NewButton("turn back")
|
||||
button0.OnClick(world.SwitchFunc("start"))
|
||||
|
||||
container.Warp (func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button1, false)
|
||||
container.Adopt(button0, false)
|
||||
button1.Focus()
|
||||
})
|
||||
},
|
||||
"inside": func () {
|
||||
label := basicElements.NewLabel (
|
||||
"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.", true)
|
||||
|
||||
button0 := basicElements.NewButton("go back outside")
|
||||
button0.OnClick(world.SwitchFunc("house"))
|
||||
|
||||
container.Warp (func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button0, false)
|
||||
button0.Focus()
|
||||
})
|
||||
},
|
||||
"bear": func () {
|
||||
label := basicElements.NewLabel (
|
||||
"you come face to face with a bear.\n" +
|
||||
"it eats you (it was hungry).", true)
|
||||
|
||||
button0 := basicElements.NewButton("try again")
|
||||
button0.OnClick(world.SwitchFunc("start"))
|
||||
button1 := basicElements.NewButton("exit")
|
||||
button1.OnClick(tomo.Stop)
|
||||
|
||||
container.Warp (func () {
|
||||
container.Adopt(label, true)
|
||||
container.Adopt(button0, false)
|
||||
container.Adopt(button1, false)
|
||||
button0.Focus()
|
||||
})
|
||||
},
|
||||
}
|
||||
world.Switch("start")
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
||||
+17
-15
@@ -1,41 +1,43 @@
|
||||
package main
|
||||
|
||||
import "os"
|
||||
import "time"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
|
||||
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/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
os.Exit(0)
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
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")
|
||||
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
|
||||
window.SetApplicationName("TomoClock")
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
window.Adopt(container)
|
||||
|
||||
clock := fun.NewAnalogClock(time.Now())
|
||||
container.Adopt(clock, true)
|
||||
label := basicElements.NewLabel(formatTime(), false)
|
||||
container.Adopt(label, false)
|
||||
label := elements.NewLabel(formatTime())
|
||||
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) {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func tick (label *basicElements.Label, clock *fun.AnalogClock) {
|
||||
func tick (label *elements.Label, clock *fun.AnalogClock) {
|
||||
for {
|
||||
tomo.Do (func () {
|
||||
nasin.Do (func () {
|
||||
label.SetText(formatTime())
|
||||
clock.SetTime(time.Now())
|
||||
})
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(360, 2)
|
||||
window.SetTitle("horizontal stack")
|
||||
|
||||
container := basicElements.NewContainer(basicLayouts.Horizontal { true, true })
|
||||
window.Adopt(container)
|
||||
|
||||
container.Adopt(basicElements.NewLabel("this is sample text", true), true)
|
||||
container.Adopt(basicElements.NewLabel("this is sample text", true), true)
|
||||
container.Adopt(basicElements.NewLabel("this is sample text", true), true)
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/nasin"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
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)
|
||||
window.Adopt(container)
|
||||
|
||||
container.Adopt (
|
||||
elements.NewLabel("Just some of the wonderful icons we have:"),
|
||||
elements.NewLine())
|
||||
container.AdoptExpand (
|
||||
icons(tomo.IconHome, tomo.IconHistory),
|
||||
icons(tomo.IconFile, tomo.IconNetwork),
|
||||
icons(tomo.IconOpen, tomo.IconRemoveFavorite),
|
||||
icons(tomo.IconCursor, tomo.IconDistort))
|
||||
|
||||
closeButton := elements.NewButton("Yes verynice")
|
||||
closeButton.SetIcon(tomo.IconYes)
|
||||
closeButton.OnClick(window.Close)
|
||||
container.Adopt(closeButton)
|
||||
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
func icons (min, max tomo.Icon) (container *elements.Box) {
|
||||
container = elements.NewHBox(elements.SpaceMargin)
|
||||
for index := min; index <= max; index ++ {
|
||||
container.AdoptExpand(elements.NewIcon(index, tomo.IconSizeSmall))
|
||||
}
|
||||
return
|
||||
}
|
||||
+28
-23
@@ -6,53 +6,58 @@ 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/layouts/basic"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
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(err); return }
|
||||
if err != nil { return err }
|
||||
logo, _, err := image.Decode(file)
|
||||
file.Close()
|
||||
if err != nil { fatalError(err); return }
|
||||
if err != nil { return err }
|
||||
|
||||
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
|
||||
logoImage := basicElements.NewImage(logo)
|
||||
button := basicElements.NewButton("Show me a gopher instead")
|
||||
button.OnClick (func () { container.Warp (func () {
|
||||
container.DisownAll()
|
||||
gopher, _, err :=
|
||||
image.Decode(bytes.NewReader(gopher.GopherPng()))
|
||||
if err != nil { fatalError(err); return }
|
||||
container.Adopt(basicElements.NewImage(gopher),true)
|
||||
}) })
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
logoImage := elements.NewImage(logo)
|
||||
button := elements.NewButton("Show me a gopher instead")
|
||||
button.OnClick (func () {
|
||||
window.SetTitle("Not the Tomo Logo")
|
||||
container.DisownAll()
|
||||
gopher, _, err :=
|
||||
image.Decode(bytes.NewReader(gopher.GopherPng()))
|
||||
if err != nil { fatalError(window, err); return }
|
||||
container.AdoptExpand(elements.NewImage(gopher))
|
||||
})
|
||||
|
||||
container.Adopt(logoImage, true)
|
||||
container.Adopt(button, false)
|
||||
container.AdoptExpand(logoImage)
|
||||
container.Adopt(button)
|
||||
window.Adopt(container)
|
||||
|
||||
button.Focus()
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
func fatalError (err error) {
|
||||
func fatalError (window tomo.Window, err error) {
|
||||
popups.NewDialog (
|
||||
popups.DialogKindError,
|
||||
window,
|
||||
"Error",
|
||||
err.Error(),
|
||||
popups.Button {
|
||||
Name: "OK",
|
||||
OnPress: tomo.Stop,
|
||||
OnPress: nasin.Stop,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+33
-22
@@ -1,32 +1,40 @@
|
||||
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/layouts/basic"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(2, 2)
|
||||
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 := basicElements.NewContainer(basicLayouts.Vertical { true, true })
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
window.Adopt(container)
|
||||
|
||||
// create inputs
|
||||
firstName := basicElements.NewTextBox("First name", "")
|
||||
lastName := basicElements.NewTextBox("Last name", "")
|
||||
fingerLength := basicElements.NewTextBox("Length of fingers", "")
|
||||
button := basicElements.NewButton("Ok")
|
||||
firstName := elements.NewTextBox("First name", "")
|
||||
lastName := elements.NewTextBox("Last name", "")
|
||||
fingerLength := elements.NewTextBox("Length of fingers", "")
|
||||
purpose := elements.NewComboBox (
|
||||
"",
|
||||
"Gaslight",
|
||||
"Gatekeep",
|
||||
"Girlboss")
|
||||
button := elements.NewButton("Ok")
|
||||
|
||||
button.SetEnabled(false)
|
||||
button.OnClick (func () {
|
||||
// create a dialog displaying the results
|
||||
popups.NewDialog (
|
||||
popups.DialogKindInfo,
|
||||
window,
|
||||
"Profile",
|
||||
firstName.Value() + " " + lastName.Value() +
|
||||
"'s fingers\nmeasure in at " + fingerLength.Value() +
|
||||
@@ -36,22 +44,25 @@ func run () {
|
||||
// enable the Ok button if all three inputs have text in them
|
||||
check := func () {
|
||||
button.SetEnabled (
|
||||
firstName.Filled() &&
|
||||
lastName.Filled() &&
|
||||
fingerLength.Filled())
|
||||
firstName.Filled() &&
|
||||
lastName.Filled() &&
|
||||
fingerLength.Filled() &&
|
||||
purpose.Filled())
|
||||
}
|
||||
firstName.OnChange(check)
|
||||
lastName.OnChange(check)
|
||||
fingerLength.OnChange(check)
|
||||
purpose.OnChange(check)
|
||||
|
||||
// add elements to container
|
||||
container.Adopt(basicElements.NewLabel("Choose your words carefully.", false), true)
|
||||
container.Adopt(firstName, false)
|
||||
container.Adopt(lastName, false)
|
||||
container.Adopt(fingerLength, false)
|
||||
container.Adopt(basicElements.NewSpacer(true), false)
|
||||
container.Adopt(button, false)
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
container.AdoptExpand(elements.NewLabel("Choose your words carefully."))
|
||||
container.Adopt (
|
||||
firstName, lastName,
|
||||
fingerLength,
|
||||
elements.NewLabel("Purpose:"),
|
||||
purpose,
|
||||
elements.NewLine(), button)
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
+11
-7
@@ -1,19 +1,23 @@
|
||||
package main
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/nasin"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(480, 2)
|
||||
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(basicElements.NewLabel(text, true))
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Adopt(elements.NewLabelWrapped(text))
|
||||
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."
|
||||
|
||||
+45
-36
@@ -1,64 +1,73 @@
|
||||
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/layouts/basic"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||
|
||||
func main () {
|
||||
tomo.Run(run)
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
func run () {
|
||||
window, _ := tomo.NewWindow(300, 2)
|
||||
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 := basicElements.NewContainer(basicLayouts.Horizontal { true, true })
|
||||
container := elements.NewHBox(elements.SpaceBoth)
|
||||
window.Adopt(container)
|
||||
|
||||
var currentPage elements.Element
|
||||
turnPage := func (newPage elements.Element) {
|
||||
container.Warp (func () {
|
||||
if currentPage != nil {
|
||||
container.Disown(currentPage)
|
||||
}
|
||||
container.Adopt(newPage, true)
|
||||
currentPage = newPage
|
||||
})
|
||||
var currentPage tomo.Element
|
||||
turnPage := func (newPage tomo.Element) {
|
||||
if currentPage != nil {
|
||||
container.Disown(currentPage)
|
||||
}
|
||||
container.AdoptExpand(newPage)
|
||||
currentPage = newPage
|
||||
}
|
||||
|
||||
intro := basicElements.NewLabel (
|
||||
intro := elements.NewLabelWrapped (
|
||||
"The List element can be easily used as a sidebar. " +
|
||||
"Click on entries to flip pages!", true)
|
||||
button := basicElements.NewButton("I do nothing!")
|
||||
"Click on entries to flip pages!")
|
||||
button := elements.NewButton("I do nothing!")
|
||||
button.OnClick (func () {
|
||||
popups.NewDialog(popups.DialogKindInfo, "", "Sike!")
|
||||
popups.NewDialog(popups.DialogKindInfo, window, "", "Sike!")
|
||||
})
|
||||
mouse := testing.NewMouse()
|
||||
input := basicElements.NewTextBox("Write some text", "")
|
||||
form := basicElements.NewContainer(basicLayouts.Vertical { true, false})
|
||||
form.Adopt(basicElements.NewLabel("I have:", false), false)
|
||||
form.Adopt(basicElements.NewSpacer(true), false)
|
||||
form.Adopt(basicElements.NewCheckbox("Skin", true), false)
|
||||
form.Adopt(basicElements.NewCheckbox("Blood", false), false)
|
||||
form.Adopt(basicElements.NewCheckbox("Bone", false), false)
|
||||
input := elements.NewTextBox("Write some text", "")
|
||||
form := elements.NewVBox (
|
||||
elements.SpaceMargin,
|
||||
elements.NewLabel("I have:"),
|
||||
elements.NewLine(),
|
||||
elements.NewCheckbox("Skin", true),
|
||||
elements.NewCheckbox("Blood", false),
|
||||
elements.NewCheckbox("Bone", false))
|
||||
art := testing.NewArtist()
|
||||
|
||||
list := basicElements.NewList (
|
||||
basicElements.NewListEntry("button", func () { turnPage(button) }),
|
||||
basicElements.NewListEntry("mouse", func () { turnPage(mouse) }),
|
||||
basicElements.NewListEntry("input", func () { turnPage(input) }),
|
||||
basicElements.NewListEntry("form", func () { turnPage(form) }),
|
||||
basicElements.NewListEntry("art", func () { turnPage(art) }))
|
||||
list.OnNoEntrySelected(func () { turnPage (intro) })
|
||||
makePage := func (name string, callback func ()) ability.Selectable {
|
||||
cell := elements.NewCell(elements.NewLabel(name))
|
||||
cell.OnSelectionChange (func () {
|
||||
if cell.Selected() { callback() }
|
||||
})
|
||||
return cell
|
||||
}
|
||||
|
||||
list := elements.NewList (
|
||||
makePage("button", func () { turnPage(button) }),
|
||||
makePage("mouse", func () { turnPage(mouse) }),
|
||||
makePage("input", func () { turnPage(input) }),
|
||||
makePage("form", func () { turnPage(form) }),
|
||||
makePage("art", func () { turnPage(art) }))
|
||||
list.Collapse(96, 0)
|
||||
|
||||
container.Adopt(list, false)
|
||||
container.Adopt(list)
|
||||
turnPage(intro)
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.OnClose(nasin.Stop)
|
||||
window.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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"
|
||||
|
||||
func main () {
|
||||
nasin.Run(Application { })
|
||||
}
|
||||
|
||||
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 (
|
||||
elements.SpaceBoth,
|
||||
elements.NewLabel("Main window"))
|
||||
window.Adopt(container)
|
||||
|
||||
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) {
|
||||
window, _ := parent.NewPanel(bounds)
|
||||
title := fmt.Sprint("Panel #", id)
|
||||
window.SetTitle(title)
|
||||
container := elements.NewVBox (
|
||||
elements.SpaceBoth,
|
||||
elements.NewLabel(title))
|
||||
window.Adopt(container)
|
||||
window.Show()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user