Author SHA1 Message Date
Sasha Koshka 6d9bc4f868 Removed some redundant draw calls 2023-03-01 13:43:36 -05:00
Sasha Koshka 1f2e8aa677 Some final theme tweaks 2023-03-01 13:06:34 -05:00
Sasha Koshka ef59f46559 Made the progress bar look nicer 2023-02-28 19:00:34 -05:00
Sasha Koshka 829f1525b8 Not even gonna bother writing a good name 2023-02-28 17:15:20 -05:00
Sasha Koshka b1d15fb4ec this piano is DOPE and PHAT and WAY COOL 2023-02-28 00:17:05 -05:00
Sasha Koshka ee45b2fa60 Theming tweaks and rendering fixes 2023-02-27 17:00:28 -05:00
Sasha Koshka 8dd506a007 Textures now render properly 2023-02-27 16:38:33 -05:00
Sasha Koshka de10cde630 Add image textures to theme 2023-02-27 12:48:44 -05:00
sashakoshka 449922851f Fix list not drawing background 2023-02-26 22:56:20 -05:00
sashakoshka 26787d8670 Fixed TextBox 2023-02-26 22:48:14 -05:00
sashakoshka cda2d1f0ae Default elements compile 2023-02-26 22:20:17 -05:00
sashakoshka 241c297626 whee back in busineess 2023-02-26 14:27:38 -05:00
sashakoshka 2859dc3313 Cleaned out the old theme code and moved padding and margins to theme 2023-02-26 00:44:44 -05:00
Sasha Koshka 7e51dc5e5a Documented artist package 2023-02-25 23:04:51 -05:00
sashakoshka 81090267a6 Created new patterns 2023-02-25 18:41:16 -05:00
sashakoshka bf2fdb5eaa Ellipse and rectangle have both color and source routines 2023-02-24 16:31:42 -05:00
sashakoshka 211219eb01 Ellipse and line share code 2023-02-24 02:51:24 -05:00
sashakoshka 79ab1c8ac0 Existing shape routines have been reimplemented 2023-02-24 02:26:34 -05:00
Sasha Koshka d167559830 Got rectangles all sorted 2023-02-23 20:55:19 -05:00
Sasha Koshka 48237f5687 Add AllocateSample 2023-02-23 17:44:53 -05:00
Sasha Koshka 0ba3c982c4 Added some utility functions to pattern 2023-02-23 15:00:44 -05:00
Sasha Koshka c7e44633b1 Updated Pattern interface 2023-02-23 14:44:54 -05:00
sashakoshka b575413a0a For later 2023-02-23 12:12:25 -05:00
Sasha Koshka 29e4a7572b Added health and stamina 2023-02-21 18:53:19 -05:00
Sasha Koshka ddb960571f Fixed texture warping when too close to walls 2023-02-21 18:15:41 -05:00
Sasha Koshka ce1d938f7a Fixed the wierd wall overlap 2023-02-21 17:57:52 -05:00
Sasha Koshka 20fa445cdd backrooms!!!!! 2023-02-21 16:48:56 -05:00
sashakoshka e966771f5b The raycaster is faster but more bg=uggyh agghgfghgfhgfgh 2023-02-21 13:30:32 -05:00
sashakoshka e9e1ccc35e Added basic raycaster demo. I have no idea why I did this. 2023-02-20 01:52:50 -05:00
sashakoshka 0c39c2dd57 Button takes advantage of the rendering hints 2023-02-16 22:41:07 -05:00
Sasha Koshka f8240fb518 Created FillRectangleShatter for convenience 2023-02-16 18:19:36 -05:00
Sasha Koshka fc0a9292d9 Added rendering optimization hints to themes 2023-02-16 18:00:15 -05:00
Sasha Koshka b9cbf83a18 Added the collapse behavior as an interface 2023-02-16 17:35:53 -05:00
Sasha Koshka 270b49f825 Removed that annoying log message on window close 2023-02-16 17:22:33 -05:00
Sasha Koshka e3369ab3d4 AAAAART! 2023-02-16 14:57:46 -05:00
Sasha Koshka 50e9c3b1c9 The null rune at the end is fake now 2023-02-16 14:43:36 -05:00
Sasha Koshka e2e846a0e5 AHHHHH!!! 2023-02-16 14:39:51 -05:00
Sasha Koshka fa934fa485 Keyboard text selection is now no longer broken lmao 2023-02-16 14:09:23 -05:00
sashakoshka 56dc9ba54c this just j 2023-02-16 12:35:31 -05:00
sashakoshka 7235c86e22 TypeSetter properly adds a null char onto the end of its text 2023-02-16 02:22:32 -05:00
sashakoshka 367aee4570 Improved accuracy of TypeSetter again 2023-02-16 01:55:00 -05:00
sashakoshka bd55b6c17d Improved accuracy of type setter 2023-02-15 20:16:49 -05:00
sashakoshka a0e7bf1373 Integrated the new text drawer 2023-02-15 18:45:58 -05:00
sashakoshka 234503f104 Added fixed precision point utilities 2023-02-15 18:41:03 -05:00
sashakoshka ae551c47ea Replace TextDrawer with more capable system 2023-02-15 18:17:17 -05:00
Sasha Koshka 0c22977693 TextDrawer does not separate whitespace from printables 2023-02-14 18:11:11 -05:00
Sasha Koshka 4d87972235 Hot themeing tweaks 2023-02-14 17:21:05 -05:00
Sasha Koshka d59b7d812d Stop some redundant rendering in the piano 2023-02-14 17:05:13 -05:00
Sasha Koshka 09f782953e Use FillRectangleClip in List and Container 2023-02-14 16:53:28 -05:00
Sasha Koshka fa42cf1f5f Added a new FillRectangleClip function 2023-02-14 15:47:41 -05:00
sashakoshka dcaf9919e4 Fix thos issue 2023-02-14 02:14:52 -05:00
sashakoshka d18da8b07a Rudimentary text selection with the mouse 2023-02-13 18:29:49 -05:00
sashakoshka 88502cf628 Improved keyboard selection somewhat 2023-02-13 15:26:21 -05:00
sashakoshka 21abd147bf Rudimentary text selection with keybaord keys 2023-02-13 12:55:51 -05:00
sashakoshka 4bc8566820 Textmanip now operates on a dot instead of a cursor 2023-02-13 01:52:31 -05:00
sashakoshka 8ac5108211 Elements are no longer images 2023-02-13 01:49:33 -05:00
sashakoshka 7f0462d588 Changed the order of the Theme.Pattern method 2023-02-12 10:58:23 -05:00
sashakoshka 82e92f1e2e Icons are now no longer patterns, they are images 2023-02-12 10:55:32 -05:00
sashakoshka 9e8e986977 Changes to how scroll bars respond to the mouse
- Left clicking on the gutter jumps to that position
- Right clicking on the gutter scrolls incrementally towards that
  position
- Middle clicking on the gutter pages up or down to that position
2023-02-11 22:17:03 -05:00
sashakoshka 2d9a941da8 Lists no longer have stale scroll values when enlarged 2023-02-11 21:45:04 -05:00
sashakoshka c64ce8da67 Container shatters its background before drawing 2023-02-11 21:17:43 -05:00
sashakoshka a893831a21 Added a shatter function to subtract rectangles from a rectangle 2023-02-11 21:07:35 -05:00
sashakoshka 7f1c3ae870 Added documentation for the sliders 2023-02-11 17:04:50 -05:00
sashakoshka d7a6193c04 Added gain slider 2023-02-11 01:46:12 -05:00
sashakoshka a74f9809af Awesome labels 2023-02-11 01:27:28 -05:00
sashakoshka f9032a9a95 Oh yeah babey 2023-02-11 01:06:47 -05:00
sashakoshka 0e3de11203 Fixed a focus issue with ScrollContainer 2023-02-11 00:58:54 -05:00
sashakoshka 981c11bd44 Fixed the list widget 2023-02-11 00:18:21 -05:00
sashakoshka dce0321e9b Added a Select() method to List 2023-02-10 22:26:34 -05:00
sashakoshka 5e448edb21 Added sliders and made the ADSR controllabe with them 2023-02-10 21:55:59 -05:00
sashakoshka c33faa402b Made the supersaw a bit better and actually a supersaw 2023-02-10 15:08:20 -05:00
sashakoshka 182cb1e35b The piano now has an internal ADSR 2023-02-09 23:52:27 -05:00
Sasha Koshka cfc2b5e130 Image element for showing images 2023-02-09 18:34:53 -05:00
Sasha Koshka 6e7cf285cc Fixed issue with X backend not recognizing key repeats 2023-02-09 17:26:36 -05:00
Sasha Koshka e3aea7fc9e Better piano keybinds 2023-02-09 16:36:38 -05:00
Sasha Koshka 5446ffe40b h a r m o n y 2023-02-09 16:15:02 -05:00
Sasha Koshka 06e97461fa Note.Octave returns an Octave 2023-02-09 15:06:41 -05:00
Sasha Koshka b38232ee24 More documentation! 2023-02-09 15:05:13 -05:00
Sasha Koshka 2cd670f4cd Improved element documentation 2023-02-09 14:50:24 -05:00
Sasha Koshka c7bebabed5 Fixed issue where containers would not select themselves prperly 2023-02-09 14:25:55 -05:00
sashakoshka b15c260dfc Improved piano styling 2023-02-09 11:38:01 -05:00
sashakoshka 16ce15621e Moar waveforms!!!! 2023-02-09 02:04:58 -05:00
sashakoshka 16a0e76145 Removed a bunch of redundant draw calls
Most were related to a but with the keynav api
2023-02-09 01:30:14 -05:00
sashakoshka ce20b7d02c Piano can now play square waves 2023-02-09 00:01:39 -05:00
sashakoshka c5ee7c8cdb The piano plays sound 2023-02-08 23:41:31 -05:00
sashakoshka 5c7e243566 Merge pull request 'restructure-config' (#8) from restructure-config into main
Reviewed-on: sashakoshka/tomo#8
2023-02-09 02:07:08 +00:00
sashakoshka bec8b817c8 Added a piano widget because why not really 2023-02-08 21:05:36 -05:00
sashakoshka 6cc0f36000 Migrated the clock 2023-02-08 15:12:18 -05:00
sashakoshka a0e57921a4 Oh my jod 2023-02-08 14:36:14 -05:00
sashakoshka 6936353516 asuhfdjkshlk 2023-02-08 00:22:40 -05:00
sashakoshka 3998d842b1 Half-done migration of basic elements 2023-02-07 11:27:59 -05:00
sashakoshka 0bdbaa39ca Artist and test examples work 2023-02-03 18:32:22 -05:00
sashakoshka f8ebe5b1e4 Core provides convenience methods for easy theme access 2023-02-03 18:28:01 -05:00
sashakoshka 8d90dbdc92 Element core now deals with Config and Theme objects 2023-02-03 18:07:10 -05:00
sashakoshka 43fea5c8ba Tomo will call the parse functions in Theme and Config 2023-02-03 17:50:45 -05:00
sashakoshka 2ff32ca8ea Added thing to get standard directories 2023-02-03 17:06:51 -05:00
sashakoshka d79701d01b X backend conforms to new API 2023-02-03 01:35:59 -05:00
sashakoshka bdf599f93c Backends must now accept Config and Theme 2023-02-03 01:25:45 -05:00
sashakoshka 8ccaa0faba Added Themeable and Configurable element interfaces 2023-02-03 01:14:03 -05:00
Sasha Koshka 83b8040520 Theme stub 2023-02-03 00:57:18 -05:00
Sasha Koshka 4722656c7d Config stub 2023-02-02 18:20:02 -05:00
Sasha Koshka 14d1836209 Merge branch 'main' of git.tebibyte.media:sashakoshka/tomo 2023-02-02 17:58:51 -05:00
Sasha Koshka 36b995c514 Added link to github mirror 2023-02-02 17:57:56 -05:00
sashakoshka 46574cfb10 Merge pull request 'atomize-modules' (#7) from atomize-modules into main
Reviewed-on: sashakoshka/tomo#7
2023-02-02 22:51:23 +00:00
sashakoshka 8606968c74 Separate config and theme 2023-02-02 15:19:56 -05:00
sashakoshka 892c74a9da Updated everything else to match 2023-02-02 01:48:38 -05:00
sashakoshka 99942466f8 Updated X backend to match 2023-02-02 01:47:55 -05:00
sashakoshka da6fe2c845 Updated layouts to match 2023-02-02 01:47:31 -05:00
sashakoshka 04d2ea4767 Atomized the functionality of the base tomo package 2023-02-02 01:47:01 -05:00
sashakoshka f71f789b60 BasicCanvas.Reallocate refuses to work on cut canvases 2023-02-01 01:52:50 -05:00
sashakoshka 8f0f2be9e9 Reduce allocation of X buffers and canvases 2023-02-01 01:47:08 -05:00
125 changed files with 6292 additions and 3184 deletions
+2
View File
@@ -1,5 +1,7 @@
# ![tomo](assets/banner.png) # ![tomo](assets/banner.png)
This repository is [mirrored on GitHub](https://github.com/sashakoshka/tomo).
Please note: Tomo is in early development. Some features may not work properly, Please note: Tomo is in early development. Some features may not work properly,
and its API may change without notice. and its API may change without notice.
-46
View File
@@ -1,46 +0,0 @@
package artist
import "image/color"
// Beveled is a pattern that has a highlight section and a shadow section.
type Beveled [2]Pattern
// AtWhen satisfies the Pattern interface.
func (pattern Beveled) AtWhen (x, y, width, height int) (c color.RGBA) {
return QuadBeveled {
pattern[0],
pattern[1],
pattern[1],
pattern[0],
}.AtWhen(x, y, width, height)
}
// QuadBeveled is like Beveled, but with four sides. A pattern can be specified
// for each one.
type QuadBeveled [4]Pattern
// AtWhen satisfies the Pattern interface.
func (pattern QuadBeveled) AtWhen (x, y, width, height int) (c color.RGBA) {
bottom := y > height / 2
right := x > width / 2
top := !bottom
left := !right
side := 0
switch {
case top && left:
if x < y { side = 3 } else { side = 0 }
case top && right:
if width - x > y { side = 0 } else { side = 1 }
case bottom && left:
if x < height - y { side = 3 } else { side = 2 }
case bottom && right:
if width - x > height - y { side = 2 } else { side = 1 }
}
return pattern[side].AtWhen(x, y, width, height)
}
-111
View File
@@ -1,111 +0,0 @@
package artist
import "image"
import "image/color"
// Bordered is a pattern with a border and a fill.
type Bordered struct {
Fill Pattern
Stroke
}
// AtWhen satisfies the Pattern interface.
func (pattern Bordered) AtWhen (x, y, width, height int) (c color.RGBA) {
outerBounds := image.Rectangle { Max: image.Point { width, height }}
innerBounds := outerBounds.Inset(pattern.Weight)
if (image.Point { x, y }).In (innerBounds) {
return pattern.Fill.AtWhen (
x - pattern.Weight,
y - pattern.Weight,
innerBounds.Dx(), innerBounds.Dy())
} else {
return pattern.Stroke.AtWhen(x, y, width, height)
}
}
// Stroke represents a stoke that has a weight and a pattern.
type Stroke struct {
Weight int
Pattern
}
type borderInternal struct {
weight int
stroke Pattern
bounds image.Rectangle
dx, dy int
}
// MultiBordered is a pattern that allows multiple borders of different lengths
// to be inset within one another. The final border is treated as a fill color,
// and its weight does not matter.
type MultiBordered struct {
borders []borderInternal
lastWidth, lastHeight int
maxBorder int
}
// NewMultiBordered creates a new MultiBordered pattern from the given list of
// borders.
func NewMultiBordered (borders ...Stroke) (multi *MultiBordered) {
internalBorders := make([]borderInternal, len(borders))
for index, border := range borders {
internalBorders[index].weight = border.Weight
internalBorders[index].stroke = border.Pattern
}
return &MultiBordered { borders: internalBorders }
}
// AtWhen satisfies the Pattern interface.
func (multi *MultiBordered) AtWhen (x, y, width, height int) (c color.RGBA) {
if multi.lastWidth != width || multi.lastHeight != height {
multi.recalculate(width, height)
}
point := image.Point { x, y }
for index := multi.maxBorder; index >= 0; index -- {
border := multi.borders[index]
if point.In(border.bounds) {
return border.stroke.AtWhen (
point.X - border.bounds.Min.X,
point.Y - border.bounds.Min.Y,
border.dx, border.dy)
}
}
return
}
func (multi *MultiBordered) recalculate (width, height int) {
bounds := image.Rect (0, 0, width, height)
multi.maxBorder = 0
for index, border := range multi.borders {
multi.maxBorder = index
multi.borders[index].bounds = bounds
multi.borders[index].dx = bounds.Dx()
multi.borders[index].dy = bounds.Dy()
bounds = bounds.Inset(border.weight)
if bounds.Empty() { break }
}
}
// Padded is a pattern that surrounds a central fill pattern with a border that
// can have a different width for each side.
type Padded struct {
Fill Pattern
Stroke Pattern
Sides []int
}
// AtWhen satisfies the Pattern interface.
func (pattern Padded) AtWhen (x, y, width, height int) (c color.RGBA) {
innerBounds := image.Rect (
pattern.Sides[3], pattern.Sides[0],
width - pattern.Sides[1], height - pattern.Sides[2])
if (image.Point { x, y }).In (innerBounds) {
return pattern.Fill.AtWhen (
x - pattern.Sides[3],
y - pattern.Sides[0],
innerBounds.Dx(), innerBounds.Dy())
} else {
return pattern.Stroke.AtWhen(x, y, width, height)
}
}
-51
View File
@@ -1,51 +0,0 @@
package artist
import "image/color"
// Checkered is a pattern that produces a grid of two alternating colors.
type Checkered struct {
First Pattern
Second Pattern
CellWidth, CellHeight int
}
// AtWhen satisfies the Pattern interface.
func (pattern Checkered) AtWhen (x, y, width, height int) (c color.RGBA) {
twidth := pattern.CellWidth * 2
theight := pattern.CellHeight * 2
x %= twidth
y %= theight
if x < 0 { x += twidth }
if y < 0 { x += theight }
n := 0
if x >= pattern.CellWidth { n ++ }
if y >= pattern.CellHeight { n ++ }
x %= pattern.CellWidth
y %= pattern.CellHeight
if n % 2 == 0 {
return pattern.First.AtWhen (
x, y, pattern.CellWidth, pattern.CellHeight)
} else {
return pattern.Second.AtWhen (
x, y, pattern.CellWidth, pattern.CellHeight)
}
}
// Tiled is a pattern that tiles another pattern accross a grid.
type Tiled struct {
Pattern
CellWidth, CellHeight int
}
// AtWhen satisfies the Pattern interface.
func (pattern Tiled) AtWhen (x, y, width, height int) (c color.RGBA) {
x %= pattern.CellWidth
y %= pattern.CellHeight
if x < 0 { x += pattern.CellWidth }
if y < 0 { y += pattern.CellHeight }
return pattern.Pattern.AtWhen (
x, y, pattern.CellWidth, pattern.CellHeight)
}
-33
View File
@@ -1,33 +0,0 @@
package artist
import "math"
import "image/color"
// EllipticallyBordered is a pattern with a border and a fill that is elliptical
// in shape.
type EllipticallyBordered struct {
Fill Pattern
Stroke
}
// AtWhen satisfies the Pattern interface.
func (pattern EllipticallyBordered) AtWhen (x, y, width, height int) (c color.RGBA) {
xf := (float64(x) + 0.5) / float64(width ) * 2 - 1
yf := (float64(y) + 0.5) / float64(height) * 2 - 1
distance := math.Sqrt(xf * xf + yf * yf)
var radius float64
if width < height {
// vertical
radius = 1 - float64(pattern.Weight * 2) / float64(width)
} else {
// horizontal
radius = 1 - float64(pattern.Weight * 2) / float64(height)
}
if distance < radius {
return pattern.Fill.AtWhen(x, y, width, height)
} else {
return pattern.Stroke.AtWhen(x, y, width, height)
}
}
+12
View File
@@ -0,0 +1,12 @@
package artist
import "image/color"
// Hex creates a color.RGBA value from an RGBA integer value.
func Hex (color uint32) (c color.RGBA) {
c.A = uint8(color)
c.B = uint8(color >> 8)
c.G = uint8(color >> 16)
c.R = uint8(color >> 24)
return
}
+2
View File
@@ -0,0 +1,2 @@
// Package artist provides a simple 2D drawing library for canvas.Canvas.
package artist
-30
View File
@@ -1,30 +0,0 @@
package artist
import "math"
import "image/color"
// Dotted is a pattern that produces a grid of circles.
type Dotted struct {
Background Pattern
Foreground Pattern
Size int
Spacing int
}
// AtWhen satisfies the Pattern interface.
func (pattern Dotted) AtWhen (x, y, width, height int) (c color.RGBA) {
xm := x % pattern.Spacing
ym := y % pattern.Spacing
if xm < 0 { xm += pattern.Spacing }
if ym < 0 { xm += pattern.Spacing }
radius := float64(pattern.Size) / 2
spacing := float64(pattern.Spacing) / 2 - 0.5
xf := float64(xm) - spacing
yf := float64(ym) - spacing
if math.Sqrt(xf * xf + yf * yf) > radius {
return pattern.Background.AtWhen(x, y, width, height)
} else {
return pattern.Foreground.AtWhen(x, y, width, height)
}
}
-145
View File
@@ -1,145 +0,0 @@
package artist
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
// FillEllipse draws a filled ellipse with the specified pattern.
func FillEllipse (
destination tomo.Canvas,
source Pattern,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
bounds = bounds.Canon()
data, stride := destination.Buffer()
realWidth, realHeight := bounds.Dx(), bounds.Dy()
bounds = bounds.Intersect(destination.Bounds()).Canon()
if bounds.Empty() { return }
updatedRegion = bounds
width, height := bounds.Dx(), bounds.Dy()
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
xf := (float64(x) + 0.5) / float64(realWidth) - 0.5
yf := (float64(y) + 0.5) / float64(realHeight) - 0.5
if math.Sqrt(xf * xf + yf * yf) <= 0.5 {
data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] =
source.AtWhen(x, y, realWidth, realHeight)
}
}}
return
}
// StrokeEllipse draws the outline of an ellipse with the specified line weight
// and pattern.
func StrokeEllipse (
destination tomo.Canvas,
source Pattern,
weight int,
bounds image.Rectangle,
) {
if weight < 1 { return }
data, stride := destination.Buffer()
bounds = bounds.Canon().Inset(weight - 1)
width, height := bounds.Dx(), bounds.Dy()
context := ellipsePlottingContext {
data: data,
stride: stride,
source: source,
width: width,
height: height,
weight: weight,
bounds: bounds,
}
bounds.Max.X -= 1
bounds.Max.Y -= 1
radii := image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)
center := bounds.Min.Add(radii)
x := float64(0)
y := float64(radii.Y)
// region 1 decision parameter
decision1 :=
float64(radii.Y * radii.Y) -
float64(radii.X * radii.X * radii.Y) +
(0.25 * float64(radii.X) * float64(radii.X))
decisionX := float64(2 * radii.Y * radii.Y * int(x))
decisionY := float64(2 * radii.X * radii.X * int(y))
// draw region 1
for decisionX < decisionY {
context.plot( int(x) + center.X, int(y) + center.Y)
context.plot(-int(x) + center.X, int(y) + center.Y)
context.plot( int(x) + center.X, -int(y) + center.Y)
context.plot(-int(x) + center.X, -int(y) + center.Y)
if (decision1 < 0) {
x ++
decisionX += float64(2 * radii.Y * radii.Y)
decision1 += decisionX + float64(radii.Y * radii.Y)
} else {
x ++
y --
decisionX += float64(2 * radii.Y * radii.Y)
decisionY -= float64(2 * radii.X * radii.X)
decision1 +=
decisionX - decisionY +
float64(radii.Y * radii.Y)
}
}
// region 2 decision parameter
decision2 :=
float64(radii.Y * radii.Y) * (x + 0.5) * (x + 0.5) +
float64(radii.X * radii.X) * (y - 1) * (y - 1) -
float64(radii.X * radii.X * radii.Y * radii.Y)
// draw region 2
for y >= 0 {
context.plot( int(x) + center.X, int(y) + center.Y)
context.plot(-int(x) + center.X, int(y) + center.Y)
context.plot( int(x) + center.X, -int(y) + center.Y)
context.plot(-int(x) + center.X, -int(y) + center.Y)
if decision2 > 0 {
y --
decisionY -= float64(2 * radii.X * radii.X)
decision2 += float64(radii.X * radii.X) - decisionY
} else {
y --
x ++
decisionX += float64(2 * radii.Y * radii.Y)
decisionY -= float64(2 * radii.X * radii.X)
decision2 +=
decisionX - decisionY +
float64(radii.X * radii.X)
}
}
}
type ellipsePlottingContext struct {
data []color.RGBA
stride int
source Pattern
width, height int
weight int
bounds image.Rectangle
}
func (context ellipsePlottingContext) plot (x, y int) {
if (image.Point { x, y }).In(context.bounds) {
squareAround (
context.data, context.stride, context.source, x, y,
context.width, context.height, context.weight)
}
}
-45
View File
@@ -1,45 +0,0 @@
package artist
import "image/color"
// Gradient is a pattern that interpolates between two colors.
type Gradient struct {
First Pattern
Second Pattern
Orientation
}
// AtWhen satisfies the Pattern interface.
func (pattern Gradient) AtWhen (x, y, width, height int) (c color.RGBA) {
var position float64
switch pattern.Orientation {
case OrientationVertical:
position = float64(y) / float64(height)
case OrientationDiagonalRight:
position = (float64(width - x) / float64(width) +
float64(y) / float64(height)) / 2
case OrientationHorizontal:
position = float64(x) / float64(width)
case OrientationDiagonalLeft:
position = (float64(x) / float64(width) +
float64(y) / float64(height)) / 2
}
firstColor := pattern.First.AtWhen(x, y, width, height)
secondColor := pattern.Second.AtWhen(x, y, width, height)
return LerpRGBA(firstColor, secondColor, position)
}
// Lerp linearally interpolates between two integer values.
func Lerp (first, second int, fac float64) (n int) {
return int(float64(first) * (1 - fac) + float64(second) * fac)
}
// LerpRGBA linearally interpolates between two color.RGBA values.
func LerpRGBA (first, second color.RGBA, fac float64) (c color.RGBA) {
return color.RGBA {
R: uint8(Lerp(int(first.R), int(second.R), fac)),
G: uint8(Lerp(int(first.G), int(second.G), fac)),
B: uint8(Lerp(int(first.G), int(second.B), fac)),
}
}
+60
View File
@@ -0,0 +1,60 @@
package artist
import "image"
// Side represents one side of a rectangle.
type Side int; const (
SideTop Side = iota
SideRight
SideBottom
SideLeft
)
// Inset represents an inset amount for all four sides of a rectangle. The top
// side is at index zero, the right at index one, the bottom at index two, and
// the left at index three. These values may be negative.
type Inset [4]int
// 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
// further, an empty rectangle near its center will be returned.
func (inset Inset) Apply (bigger image.Rectangle) (smaller image.Rectangle) {
smaller = bigger
if smaller.Dx() < inset[3] + inset[1] {
smaller.Min.X = (smaller.Min.X + smaller.Max.X) / 2
smaller.Max.X = smaller.Min.X
} else {
smaller.Min.X += inset[3]
smaller.Max.X -= inset[1]
}
if smaller.Dy() < inset[0] + inset[2] {
smaller.Min.Y = (smaller.Min.Y + smaller.Max.Y) / 2
smaller.Max.Y = smaller.Min.Y
} else {
smaller.Min.Y += inset[0]
smaller.Max.Y -= inset[2]
}
return
}
// Inverse returns a negated version of the inset.
func (inset Inset) Inverse () (prime Inset) {
return Inset {
inset[0] * -1,
inset[1] * -1,
inset[2] * -1,
inset[3] * -1,
}
}
// Horizontal returns the sum of SideRight and SideLeft.
func (inset Inset) Horizontal () int {
return inset[SideRight] + inset[SideLeft]
}
// Vertical returns the sum of SideTop and SideBottom.
func (inset Inset) Vertical () int {
return inset[SideTop] + inset[SideBottom]
}
-143
View File
@@ -1,143 +0,0 @@
package artist
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
// TODO: draw thick lines more efficiently
// Line draws a line from one point to another with the specified weight and
// pattern.
func Line (
destination tomo.Canvas,
source Pattern,
weight int,
min image.Point,
max image.Point,
) (
updatedRegion image.Rectangle,
) {
updatedRegion = image.Rectangle { Min: min, Max: max }.Canon()
updatedRegion.Max.X ++
updatedRegion.Max.Y ++
width := updatedRegion.Dx()
height := updatedRegion.Dy()
if abs(max.Y - min.Y) <
abs(max.X - min.X) {
if max.X < min.X {
temp := min
min = max
max = temp
}
lineLow(destination, source, weight, min, max, width, height)
} else {
if max.Y < min.Y {
temp := min
min = max
max = temp
}
lineHigh(destination, source, weight, min, max, width, height)
}
return
}
func lineLow (
destination tomo.Canvas,
source Pattern,
weight int,
min image.Point,
max image.Point,
width, height int,
) {
data, stride := destination.Buffer()
bounds := destination.Bounds()
deltaX := max.X - min.X
deltaY := max.Y - min.Y
yi := 1
if deltaY < 0 {
yi = -1
deltaY *= -1
}
D := (2 * deltaY) - deltaX
y := min.Y
for x := min.X; x < max.X; x ++ {
if !(image.Point { x, y }).In(bounds) { break }
squareAround(data, stride, source, x, y, width, height, weight)
// data[x + y * stride] = source.AtWhen(x, y, width, height)
if D > 0 {
y += yi
D += 2 * (deltaY - deltaX)
} else {
D += 2 * deltaY
}
}
}
func lineHigh (
destination tomo.Canvas,
source Pattern,
weight int,
min image.Point,
max image.Point,
width, height int,
) {
data, stride := destination.Buffer()
bounds := destination.Bounds()
deltaX := max.X - min.X
deltaY := max.Y - min.Y
xi := 1
if deltaX < 0 {
xi = -1
deltaX *= -1
}
D := (2 * deltaX) - deltaY
x := min.X
for y := min.Y; y < max.Y; y ++ {
if !(image.Point { x, y }).In(bounds) { break }
squareAround(data, stride, source, x, y, width, height, weight)
// data[x + y * stride] = source.AtWhen(x, y, width, height)
if D > 0 {
x += xi
D += 2 * (deltaX - deltaY)
} else {
D += 2 * deltaX
}
}
}
func abs (in int) (out int) {
if in < 0 { in *= -1}
out = in
return
}
// TODO: this method of doing things sucks and can cause a segfault. we should
// not be doing it this way
func squareAround (
data []color.RGBA,
stride int,
source Pattern,
x, y, patternWidth, patternHeight, diameter int,
) {
minY := y - diameter + 1
minX := x - diameter + 1
maxY := y + diameter
maxX := x + diameter
for y = minY; y < maxY; y ++ {
for x = minX; x < maxX; x ++ {
data[x + y * stride] =
source.AtWhen(x, y, patternWidth, patternHeight)
}}
}
-33
View File
@@ -1,33 +0,0 @@
package artist
import "image/color"
// Noisy is a pattern that randomly interpolates between two patterns in a
// deterministic fashion.
type Noisy struct {
Low Pattern
High Pattern
Seed uint32
Harsh bool
}
// AtWhen satisfies the pattern interface.
func (pattern Noisy) AtWhen (x, y, width, height int) (c color.RGBA) {
// FIXME: this will occasionally generate "clumps"
special := uint32(x + y * 348905)
special += (pattern.Seed + 1) * 15485863
random := (special * special * special % 2038074743)
fac := float64(random) / 2038074743.0
if pattern.Harsh {
if fac > 0.5 {
return pattern.High.AtWhen(x, y, width, height)
} else {
return pattern.Low.AtWhen(x, y, width, height)
}
} else {
return LerpRGBA (
pattern.Low.AtWhen(x, y, width, height),
pattern.High.AtWhen(x, y, width, height), fac)
}
}
+62 -7
View File
@@ -1,12 +1,67 @@
package artist package artist
import "image/color" import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
// Pattern is capable of generating a pattern pixel by pixel. // Pattern is capable of drawing to a canvas within the bounds of a given
// clipping rectangle.
type Pattern interface { type Pattern interface {
// AtWhen returns the color of the pixel located at (x, y) relative to // Draw draws to destination, using the bounds of destination as a width
// the origin point of the pattern (0, 0), when the pattern has the // and height for things like gradients, bevels, etc. The pattern may
// specified width and height. Patterns may ignore the width and height // not draw outside the union of destination.Bounds() and clip. The
// parameters, but it may be useful for some patterns such as gradients. // clipping rectangle effectively takes a subset of the pattern. To
AtWhen (x, y, width, height int) (color.RGBA) // 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 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
} }
+90
View File
@@ -0,0 +1,90 @@
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
// a source canvas into nine sections...
//
// Inset[1]
// ┌──┴──┐
// ┌─┌─────┬─────┬─────┐
// Inset[0]─┤ │ 0 │ 1 │ 2 │
// └─├─────┼─────┼─────┤
// │ 3 │ 4 │ 5 │
// ├─────┼─────┼─────┤─┐
// │ 6 │ 7 │ 8 │ ├─Inset[2]
// └─────┴─────┴─────┘─┘
// └──┬──┘
// Inset[3]
//
// ... Where the bounds of section 4 are defined as the application of the
// pattern's inset to the canvas's bounds. The bounds of the other eight
// 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
// 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.Inset
}
// Draw draws the border pattern onto the destination canvas within the clipping
// bounds.
func (pattern Border) Draw (destination canvas.Canvas, clip image.Rectangle) {
bounds := clip.Canon().Intersect(destination.Bounds())
if bounds.Empty() { return }
srcSections := nonasect(pattern.Bounds(), pattern.Inset)
srcTextures := [9]Texture { }
for index, section := range srcSections {
srcTextures[index].Canvas = canvas.Cut(pattern, section)
}
dstSections := nonasect(destination.Bounds(), pattern.Inset)
for index, section := range dstSections {
srcTextures[index].Draw(canvas.Cut(destination, section), clip)
}
}
func nonasect (bounds image.Rectangle, inset artist.Inset) [9]image.Rectangle {
center := inset.Apply(bounds)
return [9]image.Rectangle {
// top
image.Rectangle {
bounds.Min,
center.Min },
image.Rect (
center.Min.X, bounds.Min.Y,
center.Max.X, center.Min.Y),
image.Rect (
center.Max.X, bounds.Min.Y,
bounds.Max.X, center.Min.Y),
// center
image.Rect (
bounds.Min.X, center.Min.Y,
center.Min.X, center.Max.Y),
center,
image.Rect (
center.Max.X, center.Min.Y,
bounds.Max.X, center.Max.Y),
// bottom
image.Rect (
bounds.Min.X, center.Max.Y,
center.Min.X, bounds.Max.Y),
image.Rect (
center.Min.X, center.Max.Y,
center.Max.X, bounds.Max.Y),
image.Rect (
center.Max.X, center.Max.Y,
bounds.Max.X, bounds.Max.Y),
}
}
+3
View File
@@ -0,0 +1,3 @@
// Package patterns provides a basic set of types that satisfy the
// artist.Pattern interface.
package patterns
+41
View File
@@ -0,0 +1,41 @@
package patterns
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// Texture is a pattern that tiles the content of a canvas both horizontally and
// vertically.
type Texture struct {
canvas.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 }
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]
}}
}
func wrap (value, min, max int) int {
difference := max - min
value = (value - min) % difference
if value < 0 { value += difference }
return value + min
}
+20
View File
@@ -0,0 +1,20 @@
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"
// 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)
}
// Uhex creates a new Uniform pattern from an RGBA integer value.
func Uhex (color uint32) (uniform Uniform) {
return Uniform(artist.Hex(color))
}
-95
View File
@@ -1,95 +0,0 @@
package artist
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
// Paste transfers one canvas onto another, offset by the specified point.
func Paste (
destination tomo.Canvas,
source tomo.Canvas,
offset image.Point,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer()
sourceBounds :=
source.Bounds().Canon().
Intersect(destination.Bounds().Sub(offset))
if sourceBounds.Empty() { return }
updatedRegion = sourceBounds.Add(offset)
for y := sourceBounds.Min.Y; y < sourceBounds.Max.Y; y ++ {
for x := sourceBounds.Min.X; x < sourceBounds.Max.X; x ++ {
dstData[x + offset.X + (y + offset.Y) * dstStride] =
srcData[x + y * srcStride]
}}
return
}
// FillRectangle draws a filled rectangle with the specified pattern.
func FillRectangle (
destination tomo.Canvas,
source Pattern,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
data, stride := destination.Buffer()
realBounds := bounds
bounds = bounds.Canon().Intersect(destination.Bounds()).Canon()
if bounds.Empty() { return }
updatedRegion = bounds
realWidth, realHeight := realBounds.Dx(), realBounds.Dy()
patternOffset := realBounds.Min.Sub(bounds.Min)
width, height := bounds.Dx(), bounds.Dy()
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] =
source.AtWhen (
x - patternOffset.X, y - patternOffset.Y,
realWidth, realHeight)
}}
return
}
// StrokeRectangle draws the outline of a rectangle with the specified line
// weight and pattern.
func StrokeRectangle (
destination tomo.Canvas,
source Pattern,
weight int,
bounds image.Rectangle,
) {
bounds = bounds.Canon()
insetBounds := bounds.Inset(weight)
if insetBounds.Empty() {
FillRectangle(destination, source, bounds)
return
}
// top
FillRectangle (destination, source, image.Rect (
bounds.Min.X, bounds.Min.Y,
bounds.Max.X, insetBounds.Min.Y))
// bottom
FillRectangle (destination, source, image.Rect (
bounds.Min.X, insetBounds.Max.Y,
bounds.Max.X, bounds.Max.Y))
// left
FillRectangle (destination, source, image.Rect (
bounds.Min.X, insetBounds.Min.Y,
insetBounds.Min.X, insetBounds.Max.Y))
// right
FillRectangle (destination, source, image.Rect (
insetBounds.Max.X, insetBounds.Min.Y,
bounds.Max.X, insetBounds.Max.Y))
}
+11
View File
@@ -0,0 +1,11 @@
// Package shapes provides some basic shape drawing routines.
//
// A word about using patterns with shape routines:
//
// Most drawing routines have a version that samples from other canvases, and a
// version that samples from a solid color. None of these routines can use
// patterns directly, but it is entirely possible to have a pattern draw to an
// off-screen canvas and then draw a shape based on that canvas. As a little
// bonus, you can save the canvas for later so you don't have to render the
// pattern again when you need to redraw the shape.
package shapes
+228
View File
@@ -0,0 +1,228 @@
package shapes
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// 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
// ellipse and the bounds of source as the "clipping rectangle". Line up the Min
// of both canvases.
func FillEllipse (
destination canvas.Canvas,
source canvas.Canvas,
) (
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())
realBounds := destination.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) {
offsetPoint := point.Add(offset)
dstIndex := point.X + point.Y * dstStride
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
dstData[dstIndex] = srcData[srcIndex]
}
}}
return
}
func StrokeEllipse (
destination canvas.Canvas,
source canvas.Canvas,
weight int,
) {
if weight < 1 { return }
dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer()
bounds := destination.Bounds().Inset(weight - 1)
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
realBounds := destination.Bounds()
if bounds.Empty() { return }
context := ellipsePlottingContext {
plottingContext: plottingContext {
dstData: dstData,
dstStride: dstStride,
srcData: srcData,
srcStride: srcStride,
weight: weight,
offset: offset,
bounds: realBounds,
},
radii: image.Pt(bounds.Dx() / 2, bounds.Dy() / 2),
}
context.center = bounds.Min.Add(context.radii)
context.plotEllipse()
}
type ellipsePlottingContext struct {
plottingContext
radii image.Point
center image.Point
}
func (context ellipsePlottingContext) plotEllipse () {
x := float64(0)
y := float64(context.radii.Y)
// region 1 decision parameter
decision1 :=
float64(context.radii.Y * context.radii.Y) -
float64(context.radii.X * context.radii.X * context.radii.Y) +
(0.25 * float64(context.radii.X) * float64(context.radii.X))
decisionX := float64(2 * context.radii.Y * context.radii.Y * int(x))
decisionY := float64(2 * context.radii.X * context.radii.X * int(y))
// draw region 1
for decisionX < decisionY {
points := []image.Point {
image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y),
image.Pt( int(x) + context.center.X, -int(y) + context.center.Y),
image.Pt(-int(x) + context.center.X, int(y) + context.center.Y),
image.Pt( int(x) + context.center.X, int(y) + context.center.Y),
}
if context.srcData == nil {
context.plotColor(points[0])
context.plotColor(points[1])
context.plotColor(points[2])
context.plotColor(points[3])
} else {
context.plotSource(points[0])
context.plotSource(points[1])
context.plotSource(points[2])
context.plotSource(points[3])
}
if (decision1 < 0) {
x ++
decisionX += float64(2 * context.radii.Y * context.radii.Y)
decision1 += decisionX + float64(context.radii.Y * context.radii.Y)
} else {
x ++
y --
decisionX += float64(2 * context.radii.Y * context.radii.Y)
decisionY -= float64(2 * context.radii.X * context.radii.X)
decision1 +=
decisionX - decisionY +
float64(context.radii.Y * context.radii.Y)
}
}
// region 2 decision parameter
decision2 :=
float64(context.radii.Y * context.radii.Y) * (x + 0.5) * (x + 0.5) +
float64(context.radii.X * context.radii.X) * (y - 1) * (y - 1) -
float64(context.radii.X * context.radii.X * context.radii.Y * context.radii.Y)
// draw region 2
for y >= 0 {
points := []image.Point {
image.Pt( int(x) + context.center.X, int(y) + context.center.Y),
image.Pt(-int(x) + context.center.X, int(y) + context.center.Y),
image.Pt( int(x) + context.center.X, -int(y) + context.center.Y),
image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y),
}
if context.srcData == nil {
context.plotColor(points[0])
context.plotColor(points[1])
context.plotColor(points[2])
context.plotColor(points[3])
} else {
context.plotSource(points[0])
context.plotSource(points[1])
context.plotSource(points[2])
context.plotSource(points[3])
}
if decision2 > 0 {
y --
decisionY -= float64(2 * context.radii.X * context.radii.X)
decision2 += float64(context.radii.X * context.radii.X) - decisionY
} else {
y --
x ++
decisionX += float64(2 * context.radii.Y * context.radii.Y)
decisionY -= float64(2 * context.radii.X * context.radii.X)
decision2 +=
decisionX - decisionY +
float64(context.radii.X * context.radii.X)
}
}
}
// FillColorEllipse fills an ellipse within the destination canvas with a solid
// color.
func FillColorEllipse (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
realBounds := bounds
bounds = bounds.Intersect(destination.Bounds()).Canon()
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) {
dstData[point.X + point.Y * dstStride] = color
}
}}
return
}
// StrokeColorEllipse is similar to FillColorEllipse, but it draws an inset
// outline of an ellipse instead.
func StrokeColorEllipse (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
weight int,
) (
updatedRegion image.Rectangle,
) {
if weight < 1 { return }
dstData, dstStride := destination.Buffer()
insetBounds := bounds.Inset(weight - 1)
context := ellipsePlottingContext {
plottingContext: plottingContext {
dstData: dstData,
dstStride: dstStride,
color: color,
weight: weight,
bounds: bounds.Intersect(destination.Bounds()),
},
radii: image.Pt(insetBounds.Dx() / 2, insetBounds.Dy() / 2),
}
context.center = insetBounds.Min.Add(context.radii)
context.plotEllipse()
return
}
func inEllipse (point image.Point, bounds image.Rectangle) bool {
point = point.Sub(bounds.Min)
x := (float64(point.X) + 0.5) / float64(bounds.Dx()) - 0.5
y := (float64(point.Y) + 0.5) / float64(bounds.Dy()) - 0.5
return math.Hypot(x, y) <= 0.5
}
+112
View File
@@ -0,0 +1,112 @@
package shapes
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// ColorLine draws a line from one point to another with the specified weight
// and color.
func ColorLine (
destination canvas.Canvas,
color color.RGBA,
weight int,
min image.Point,
max image.Point,
) (
updatedRegion image.Rectangle,
) {
updatedRegion = image.Rectangle { Min: min, Max: max }.Canon()
updatedRegion.Max.X ++
updatedRegion.Max.Y ++
data, stride := destination.Buffer()
bounds := destination.Bounds()
context := linePlottingContext {
plottingContext: plottingContext {
dstData: data,
dstStride: stride,
color: color,
weight: weight,
bounds: bounds,
},
min: min,
max: max,
}
if abs(max.Y - min.Y) < abs(max.X - min.X) {
if max.X < min.X { context.swap() }
context.lineLow()
} else {
if max.Y < min.Y { context.swap() }
context.lineHigh()
}
return
}
type linePlottingContext struct {
plottingContext
min image.Point
max image.Point
}
func (context *linePlottingContext) swap () {
temp := context.max
context.max = context.min
context.min = temp
}
func (context linePlottingContext) lineLow () {
deltaX := context.max.X - context.min.X
deltaY := context.max.Y - context.min.Y
yi := 1
if deltaY < 0 {
yi = -1
deltaY *= -1
}
D := (2 * deltaY) - deltaX
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)
point.Y += yi
} else {
D += 2 * deltaY
}
}
}
func (context linePlottingContext) lineHigh () {
deltaX := context.max.X - context.min.X
deltaY := context.max.Y - context.min.Y
xi := 1
if deltaX < 0 {
xi = -1
deltaX *= -1
}
D := (2 * deltaX) - deltaY
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
D += 2 * (deltaX - deltaY)
} else {
D += 2 * deltaX
}
}
}
func abs (n int) int {
if n < 0 { n *= -1}
return n
}
+47
View File
@@ -0,0 +1,47 @@
package shapes
import "image"
import "image/color"
// FIXME? drawing a ton of overlapping squares might be a bit wasteful.
type plottingContext struct {
dstData []color.RGBA
dstStride int
srcData []color.RGBA
srcStride int
color color.RGBA
weight int
offset image.Point
bounds image.Rectangle
}
func (context plottingContext) square (center image.Point) (square image.Rectangle) {
return image.Rect(0, 0, context.weight, context.weight).
Sub(image.Pt(context.weight / 2, context.weight / 2)).
Add(center).
Intersect(context.bounds)
}
func (context plottingContext) plotColor (center image.Point) {
square := context.square(center)
for y := square.Min.Y; y < square.Max.Y; y ++ {
for x := square.Min.X; x < square.Max.X; x ++ {
context.dstData[x + y * context.dstStride] = context.color
}}
}
func (context plottingContext) plotSource (center image.Point) {
square := context.square(center)
for y := square.Min.Y; y < square.Max.Y; y ++ {
for x := square.Min.X; x < square.Max.X; x ++ {
// we offset srcIndex here because we have already applied the
// offset to the square, and we need to reverse that to get the
// proper source coordinates.
srcIndex :=
x + context.offset.X +
(y + context.offset.Y) * context.dstStride
dstIndex := x + y * context.dstStride
context.dstData[dstIndex] = context.srcData [srcIndex]
}}
}
+116
View File
@@ -0,0 +1,116 @@
package shapes
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
// TODO: return updatedRegion for all routines in this package
func FillRectangle (
destination canvas.Canvas,
source canvas.Canvas,
) (
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
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 ++ {
offsetPoint := point.Add(offset)
dstIndex := point.X + point.Y * dstStride
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
dstData[dstIndex] = srcData[srcIndex]
}}
return
}
func StrokeRectangle (
destination canvas.Canvas,
source canvas.Canvas,
weight int,
) {
bounds := destination.Bounds()
insetBounds := bounds.Inset(weight)
if insetBounds.Empty() {
FillRectangle(destination, source)
return
}
FillRectangleShatter(destination, source, insetBounds)
}
// FillRectangleShatter is like FillRectangle, but it does not draw in areas
// specified in "rocks".
func FillRectangleShatter (
destination canvas.Canvas,
source canvas.Canvas,
rocks ...image.Rectangle,
) {
tiles := shatter.Shatter(destination.Bounds(), rocks...)
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
for _, tile := range tiles {
FillRectangle (
canvas.Cut(destination, tile),
canvas.Cut(source, tile.Add(offset)))
}
}
// FillColorRectangle fills a rectangle within the destination canvas with a
// solid color.
func FillColorRectangle (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
bounds = bounds.Canon().Intersect(destination.Bounds())
if bounds.Empty() { return }
updatedRegion = bounds
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
dstData[x + y * dstStride] = color
}}
return
}
// FillColorRectangleShatter is like FillColorRectangle, but it does not draw in
// areas specified in "rocks".
func FillColorRectangleShatter (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
rocks ...image.Rectangle,
) {
tiles := shatter.Shatter(bounds, rocks...)
for _, tile := range tiles {
FillColorRectangle(destination, color, tile)
}
}
// StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset
// outline of the given rectangle instead.
func StrokeColorRectangle (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
weight int,
) {
insetBounds := bounds.Inset(weight)
if insetBounds.Empty() {
FillColorRectangle(destination, color, bounds)
return
}
FillColorRectangleShatter(destination, color, bounds, insetBounds)
}
-43
View File
@@ -1,43 +0,0 @@
package artist
import "image/color"
// Orientation specifies an eight-way pattern orientation.
type Orientation int
const (
OrientationVertical Orientation = iota
OrientationDiagonalRight
OrientationHorizontal
OrientationDiagonalLeft
)
// Split is a pattern that is divided in half between two sub-patterns.
type Split struct {
First Pattern
Second Pattern
Orientation
}
// AtWhen satisfies the Pattern interface.
func (pattern Split) AtWhen (x, y, width, height int) (c color.RGBA) {
var first bool
switch pattern.Orientation {
case OrientationVertical:
first = x < width / 2
case OrientationDiagonalRight:
first = float64(x) / float64(width) +
float64(y) / float64(height) < 1
case OrientationHorizontal:
first = y < height / 2
case OrientationDiagonalLeft:
first = float64(width - x) / float64(width) +
float64(y) / float64(height) < 1
}
if first {
return pattern.First.AtWhen(x, y, width, height)
} else {
return pattern.Second.AtWhen(x, y, width, height)
}
}
-37
View File
@@ -1,37 +0,0 @@
package artist
import "image/color"
// Striped is a pattern that produces stripes of two alternating colors.
type Striped struct {
First Stroke
Second Stroke
Orientation
}
// AtWhen satisfies the Pattern interface.
func (pattern Striped) AtWhen (x, y, width, height int) (c color.RGBA) {
position := 0
switch pattern.Orientation {
case OrientationVertical:
position = x
case OrientationDiagonalRight:
position = x + y
case OrientationHorizontal:
position = y
case OrientationDiagonalLeft:
position = x - y
}
phase := pattern.First.Weight + pattern.Second.Weight
position %= phase
if position < 0 {
position += phase
}
if position < pattern.First.Weight {
return pattern.First.AtWhen(x, y, width, height)
} else {
return pattern.Second.AtWhen(x, y, width, height)
}
}
-356
View File
@@ -1,356 +0,0 @@
package artist
// import "fmt"
import "image"
import "unicode"
import "image/draw"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/sashakoshka/tomo"
type characterLayout struct {
x int
character rune
}
type wordLayout struct {
position image.Point
width int
spaceAfter int
breaksAfter int
text []characterLayout
whitespace []characterLayout
}
// Align specifies a text alignment method.
type Align int
const (
// AlignLeft aligns the start of each line to the beginning point
// of each dot.
AlignLeft Align = iota
AlignRight
AlignCenter
AlignJustify
)
// TextDrawer is a struct that is capable of efficient rendering of wrapped
// text, and calculating text bounds. It avoids doing redundant work
// automatically.
type TextDrawer struct {
runes []rune
face font.Face
width int
height int
align Align
wrap bool
cut bool
layout []wordLayout
layoutClean bool
layoutBounds image.Rectangle
}
// SetText sets the text of the text drawer.
func (drawer *TextDrawer) SetText (runes []rune) {
// if drawer.runes == runes { return }
drawer.runes = runes
drawer.layoutClean = false
}
// SetFace sets the font face of the text drawer.
func (drawer *TextDrawer) SetFace (face font.Face) {
if drawer.face == face { return }
drawer.face = face
drawer.layoutClean = false
}
// SetMaxWidth sets a maximum width for the text drawer, and recalculates the
// layout if needed. If zero is given, there will be no width limit and the text
// will not wrap.
func (drawer *TextDrawer) SetMaxWidth (width int) {
if drawer.width == width { return }
drawer.width = width
drawer.wrap = width != 0
drawer.layoutClean = false
}
// SetMaxHeight sets a maximum height for the text drawer. Lines that are
// entirely below this height will not be drawn, and lines that are on the cusp
// of this maximum height will be clipped at the point that they cross it.
func (drawer *TextDrawer) SetMaxHeight (height int) {
if drawer.height == height { return }
drawer.height = height
drawer.cut = height != 0
drawer.layoutClean = false
}
// SetAlignment specifies how the drawer should align its text. For this to have
// an effect, a maximum width must have been set.
func (drawer *TextDrawer) SetAlignment (align Align) {
if drawer.align == align { return }
drawer.align = align
drawer.layoutClean = false
}
// Draw draws the drawer's text onto the specified canvas at the given offset.
func (drawer *TextDrawer) Draw (
destination tomo.Canvas,
source Pattern,
offset image.Point,
) (
updatedRegion image.Rectangle,
) {
wrappedSource := WrappedPattern {
Pattern: source,
Width: 0,
Height: 0, // TODO: choose a better width and height
}
if !drawer.layoutClean { drawer.recalculate() }
// TODO: reimplement a version of draw mask that takes in a pattern and
// only draws to a tomo.Canvas.
for _, word := range drawer.layout {
for _, character := range word.text {
destinationRectangle,
mask, maskPoint, _, ok := drawer.face.Glyph (
fixed.P (
offset.X + word.position.X + character.x,
offset.Y + word.position.Y),
character.character)
if !ok { continue }
// FIXME: clip destination rectangle if we are on the cusp of
// the maximum height.
draw.DrawMask (
destination,
destinationRectangle,
wrappedSource, image.Point { },
mask, maskPoint,
draw.Over)
updatedRegion = updatedRegion.Union(destinationRectangle)
}}
return
}
// LayoutBounds returns a semantic bounding box for text to be used to determine
// an offset for drawing. If a maximum width or height has been set, those will
// be used as the width and height of the bounds respectively. The origin point
// (0, 0) of the returned bounds will be equivalent to the baseline at the start
// of the first line. As such, the minimum of the bounds will be negative.
func (drawer *TextDrawer) LayoutBounds () (bounds image.Rectangle) {
if !drawer.layoutClean { drawer.recalculate() }
bounds = drawer.layoutBounds
return
}
// Em returns the width of an emspace.
func (drawer *TextDrawer) Em () (width fixed.Int26_6) {
if drawer.face == nil { return }
width, _ = drawer.face.GlyphAdvance('M')
return
}
// LineHeight returns the height of one line.
func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) {
if drawer.face == nil { return }
metrics := drawer.face.Metrics()
height = metrics.Height
return
}
// ReccomendedHeightFor returns the reccomended max height if the text were to
// have its maximum width set to the given width. This does not alter the
// drawer's state.
func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) {
if !drawer.layoutClean { drawer.recalculate() }
metrics := drawer.face.Metrics()
dot := fixed.Point26_6 { 0, metrics.Height }
for _, word := range drawer.layout {
if word.width + dot.X.Round() > width {
dot.Y += metrics.Height
dot.X = 0
}
dot.X += fixed.I(word.width + word.spaceAfter)
if word.breaksAfter > 0 {
dot.Y += fixed.I(word.breaksAfter).Mul(metrics.Height)
dot.X = 0
}
}
return dot.Y.Round()
}
// PositionOf returns the position of the character at the specified index
// relative to the baseline.
func (drawer *TextDrawer) PositionOf (index int) (position image.Point) {
if !drawer.layoutClean { drawer.recalculate() }
index ++
for _, word := range drawer.layout {
position = word.position
for _, character := range word.text {
index --
position.X = word.position.X + character.x
if index < 1 { return }
}
for _, character := range word.whitespace {
index --
position.X = word.position.X + character.x
if index < 1 { return }
}
}
return
}
// Length returns the amount of runes in the drawer's text.
func (drawer *TextDrawer) Length () (length int) {
return len(drawer.runes)
}
func (drawer *TextDrawer) recalculate () {
drawer.layoutClean = true
drawer.layout = nil
drawer.layoutBounds = image.Rectangle { }
if drawer.runes == nil { return }
if drawer.face == nil { return }
metrics := drawer.face.Metrics()
dot := fixed.Point26_6 { 0, 0 }
index := 0
horizontalExtent := 0
currentCharacterX := fixed.Int26_6(0)
previousCharacter := rune(-1)
for index < len(drawer.runes) {
word := wordLayout { }
word.position.X = dot.X.Round()
word.position.Y = dot.Y.Round()
// process a word
currentCharacterX = 0
wordWidth := fixed.Int26_6(0)
for index < len(drawer.runes) && !unicode.IsSpace(drawer.runes[index]) {
character := drawer.runes[index]
_, advance, ok := drawer.face.GlyphBounds(character)
index ++
if !ok { continue }
word.text = append(word.text, characterLayout {
x: currentCharacterX.Round(),
character: character,
})
dot.X += advance
wordWidth += advance
currentCharacterX += advance
if dot.X.Round () > horizontalExtent {
horizontalExtent = dot.X.Round()
}
if previousCharacter >= 0 {
dot.X += drawer.face.Kern (
previousCharacter,
character)
}
previousCharacter = character
}
word.width = wordWidth.Round()
// detect if the word that was just processed goes out of
// bounds, and if it does, wrap it
if drawer.wrap &&
word.width + word.position.X > drawer.width &&
word.position.X > 0 {
word.position.Y += metrics.Height.Round()
word.position.X = 0
dot.Y += metrics.Height
dot.X = wordWidth
}
// process whitespace, going onto a new line if there is a
// newline character
spaceWidth := fixed.Int26_6(0)
for index < len(drawer.runes) && unicode.IsSpace(drawer.runes[index]) {
character := drawer.runes[index]
_, advance, ok := drawer.face.GlyphBounds(character)
index ++
if !ok { continue }
word.whitespace = append(word.whitespace, characterLayout {
x: currentCharacterX.Round(),
character: character,
})
spaceWidth += advance
currentCharacterX += advance
if character == '\n' {
dot.Y += metrics.Height
dot.X = 0
word.breaksAfter ++
break
} else {
dot.X += advance
if previousCharacter >= 0 {
dot.X += drawer.face.Kern (
previousCharacter,
character)
}
}
previousCharacter = character
}
word.spaceAfter = spaceWidth.Round()
// add the word to the layout
drawer.layout = append(drawer.layout, word)
// if there is a set maximum height, and we have crossed it,
// stop processing more words. and remove any words that have
// also crossed the line.
if
drawer.cut &&
(dot.Y - metrics.Ascent - metrics.Descent).Round() >
drawer.height {
for
index := len(drawer.layout) - 1;
index >= 0; index -- {
if drawer.layout[index].position.Y < dot.Y.Round() {
break
}
drawer.layout = drawer.layout[:index]
}
break
}
}
// add a little null to the last character
if len(drawer.layout) > 0 {
lastWord := &drawer.layout[len(drawer.layout) - 1]
lastWord.whitespace = append (
lastWord.whitespace,
characterLayout {
x: currentCharacterX.Round(),
})
}
if drawer.wrap {
drawer.layoutBounds.Max.X = drawer.width
} else {
drawer.layoutBounds.Max.X = horizontalExtent
}
if drawer.cut {
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
drawer.layoutBounds.Max.Y = drawer.height - metrics.Ascent.Round()
} else {
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
drawer.layoutBounds.Max.Y = dot.Y.Round() + metrics.Descent.Round()
}
// TODO:
// for each line, calculate the bounds as if the words are left aligned,
// and then at the end of the process go through each line and re-align
// everything. this will make the process far simpler.
}
-43
View File
@@ -1,43 +0,0 @@
package artist
import "image"
import "image/color"
// Texture is a struct that allows an image to be converted into a tiling
// texture pattern.
type Texture struct {
data []color.RGBA
width, height int
}
// NewTexture converts an image into a texture.
func NewTexture (source image.Image) (texture Texture) {
bounds := source.Bounds()
texture.width = bounds.Dx()
texture.height = bounds.Dy()
texture.data = make([]color.RGBA, texture.width * texture.height)
index := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
r, g, b, a := source.At(x, y).RGBA()
texture.data[index] = color.RGBA {
uint8(r >> 8),
uint8(g >> 8),
uint8(b >> 8),
uint8(a >> 8),
}
index ++
}}
return
}
// AtWhen returns the color at the specified x and y coordinates, wrapped to the
// image's width. the width and height are ignored.
func (texture Texture) AtWhen (x, y, width, height int) (pixel color.RGBA) {
x %= texture.width
y %= texture.height
if x < 0 { x += texture.width }
if y < 0 { y += texture.height }
return texture.data[x + y * texture.width]
}
-55
View File
@@ -1,55 +0,0 @@
package artist
import "image"
import "image/color"
// Uniform is an infinite-sized pattern of uniform color. It implements the
// Pattern, color.Color, color.Model, and image.Image interfaces.
type Uniform color.RGBA
// NewUniform returns a new Uniform image of the given color.
func NewUniform (c color.Color) (uniform Uniform) {
r, g, b, a := c.RGBA()
uniform.R = uint8(r >> 8)
uniform.G = uint8(g >> 8)
uniform.B = uint8(b >> 8)
uniform.A = uint8(a >> 8)
return
}
// ColorModel satisfies the image.Image interface.
func (uniform Uniform) ColorModel () (model color.Model) {
return uniform
}
// Convert satisfies the color.Model interface.
func (uniform Uniform) Convert (in color.Color) (c color.Color) {
return color.RGBA(uniform)
}
// Bounds satisfies the image.Image interface.
func (uniform Uniform) Bounds () (rectangle image.Rectangle) {
rectangle.Min = image.Point { -1e9, -1e9 }
rectangle.Max = image.Point { 1e9, 1e9 }
return
}
// At satisfies the image.Image interface.
func (uniform Uniform) At (x, y int) (c color.Color) {
return color.RGBA(uniform)
}
// AtWhen satisfies the Pattern interface.
func (uniform Uniform) AtWhen (x, y, width, height int) (c color.RGBA) {
return color.RGBA(uniform)
}
// RGBA satisfies the color.Color interface.
func (uniform Uniform) RGBA () (r, g, b, a uint32) {
return color.RGBA(uniform).RGBA()
}
// Opaque scans the entire image and reports whether it is fully opaque.
func (uniform Uniform) Opaque () (opaque bool) {
return uniform.A == 0xFF
}
-27
View File
@@ -1,27 +0,0 @@
package artist
import "image"
import "image/color"
// WrappedPattern is a pattern that is able to behave like an image.Image.
type WrappedPattern struct {
Pattern
Width, Height int
}
// At satisfies the image.Image interface.
func (pattern WrappedPattern) At (x, y int) (c color.Color) {
return pattern.Pattern.AtWhen(x, y, pattern.Width, pattern.Height)
}
// Bounds satisfies the image.Image interface.
func (pattern WrappedPattern) Bounds () (rectangle image.Rectangle) {
rectangle.Min = image.Point { -1e9, -1e9 }
rectangle.Max = image.Point { 1e9, 1e9 }
return
}
// ColorModel satisfies the image.Image interface.
func (pattern WrappedPattern) ColorModel () (model color.Model) {
return color.RGBAModel
}
+13 -3
View File
@@ -1,6 +1,10 @@
package tomo package tomo
import "errors" 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"
// Backend represents a connection to a display server, or something similar. // Backend represents a connection to a display server, or something similar.
// It is capable of managing an event loop, and creating windows. // It is capable of managing an event loop, and creating windows.
@@ -19,13 +23,19 @@ type Backend interface {
// NewWindow creates a new window with the specified width and height, // NewWindow creates a new window with the specified width and height,
// and returns a struct representing it that fulfills the Window // and returns a struct representing it that fulfills the Window
// interface. // interface.
NewWindow (width, height int) (window Window, err error) NewWindow (width, height int) (window elements.Window, err error)
// Copy puts data into the clipboard. // Copy puts data into the clipboard.
Copy (Data) Copy (data.Data)
// Paste returns the data currently in the clipboard. // Paste returns the data currently in the clipboard.
Paste (accept []Mime) (Data) Paste (accept []data.Mime) (data.Data)
// SetTheme sets the theme of all open windows.
SetTheme (theme.Theme)
// SetConfig sets the configuration of all open windows.
SetConfig (config.Config)
} }
// BackendFactory represents a function capable of constructing a backend // BackendFactory represents a function capable of constructing a backend
+87 -87
View File
@@ -3,64 +3,64 @@ package x
import "unicode" import "unicode"
import "github.com/jezek/xgb/xproto" import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/keybind" import "github.com/jezek/xgbutil/keybind"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input"
// when making changes to this file, look at keysymdef.h and // when making changes to this file, look at keysymdef.h and
// https://tronche.com/gui/x/xlib/input/keyboard-encoding.html // https://tronche.com/gui/x/xlib/input/keyboard-encoding.html
var buttonCodeTable = map[xproto.Keysym] tomo.Key { var buttonCodeTable = map[xproto.Keysym] input.Key {
0xFFFFFF: tomo.KeyNone, 0xFFFFFF: input.KeyNone,
0xFF63: tomo.KeyInsert, 0xFF63: input.KeyInsert,
0xFF67: tomo.KeyMenu, 0xFF67: input.KeyMenu,
0xFF61: tomo.KeyPrintScreen, 0xFF61: input.KeyPrintScreen,
0xFF6B: tomo.KeyPause, 0xFF6B: input.KeyPause,
0xFFE5: tomo.KeyCapsLock, 0xFFE5: input.KeyCapsLock,
0xFF14: tomo.KeyScrollLock, 0xFF14: input.KeyScrollLock,
0xFF7F: tomo.KeyNumLock, 0xFF7F: input.KeyNumLock,
0xFF08: tomo.KeyBackspace, 0xFF08: input.KeyBackspace,
0xFF09: tomo.KeyTab, 0xFF09: input.KeyTab,
0xFE20: tomo.KeyTab, 0xFE20: input.KeyTab,
0xFF0D: tomo.KeyEnter, 0xFF0D: input.KeyEnter,
0xFF1B: tomo.KeyEscape, 0xFF1B: input.KeyEscape,
0xFF52: tomo.KeyUp, 0xFF52: input.KeyUp,
0xFF54: tomo.KeyDown, 0xFF54: input.KeyDown,
0xFF51: tomo.KeyLeft, 0xFF51: input.KeyLeft,
0xFF53: tomo.KeyRight, 0xFF53: input.KeyRight,
0xFF55: tomo.KeyPageUp, 0xFF55: input.KeyPageUp,
0xFF56: tomo.KeyPageDown, 0xFF56: input.KeyPageDown,
0xFF50: tomo.KeyHome, 0xFF50: input.KeyHome,
0xFF57: tomo.KeyEnd, 0xFF57: input.KeyEnd,
0xFFE1: tomo.KeyLeftShift, 0xFFE1: input.KeyLeftShift,
0xFFE2: tomo.KeyRightShift, 0xFFE2: input.KeyRightShift,
0xFFE3: tomo.KeyLeftControl, 0xFFE3: input.KeyLeftControl,
0xFFE4: tomo.KeyRightControl, 0xFFE4: input.KeyRightControl,
0xFFE7: tomo.KeyLeftMeta, 0xFFE7: input.KeyLeftMeta,
0xFFE8: tomo.KeyRightMeta, 0xFFE8: input.KeyRightMeta,
0xFFE9: tomo.KeyLeftAlt, 0xFFE9: input.KeyLeftAlt,
0xFFEA: tomo.KeyRightAlt, 0xFFEA: input.KeyRightAlt,
0xFFEB: tomo.KeyLeftSuper, 0xFFEB: input.KeyLeftSuper,
0xFFEC: tomo.KeyRightSuper, 0xFFEC: input.KeyRightSuper,
0xFFED: tomo.KeyLeftHyper, 0xFFED: input.KeyLeftHyper,
0xFFEE: tomo.KeyRightHyper, 0xFFEE: input.KeyRightHyper,
0xFFFF: tomo.KeyDelete, 0xFFFF: input.KeyDelete,
0xFFBE: tomo.KeyF1, 0xFFBE: input.KeyF1,
0xFFBF: tomo.KeyF2, 0xFFBF: input.KeyF2,
0xFFC0: tomo.KeyF3, 0xFFC0: input.KeyF3,
0xFFC1: tomo.KeyF4, 0xFFC1: input.KeyF4,
0xFFC2: tomo.KeyF5, 0xFFC2: input.KeyF5,
0xFFC3: tomo.KeyF6, 0xFFC3: input.KeyF6,
0xFFC4: tomo.KeyF7, 0xFFC4: input.KeyF7,
0xFFC5: tomo.KeyF8, 0xFFC5: input.KeyF8,
0xFFC6: tomo.KeyF9, 0xFFC6: input.KeyF9,
0xFFC7: tomo.KeyF10, 0xFFC7: input.KeyF10,
0xFFC8: tomo.KeyF11, 0xFFC8: input.KeyF11,
0xFFC9: tomo.KeyF12, 0xFFC9: input.KeyF12,
// TODO: send this whenever a compose key, dead key, etc is pressed, // TODO: send this whenever a compose key, dead key, etc is pressed,
// and then send the resulting character while witholding the key // and then send the resulting character while witholding the key
@@ -68,46 +68,46 @@ var buttonCodeTable = map[xproto.Keysym] tomo.Key {
// concerned, a magical key with the final character was pressed and the // concerned, a magical key with the final character was pressed and the
// KeyDead key is just so that the program might provide some visual // KeyDead key is just so that the program might provide some visual
// feedback to the user while input is being waited for. // feedback to the user while input is being waited for.
0xFF20: tomo.KeyDead, 0xFF20: input.KeyDead,
} }
var keypadCodeTable = map[xproto.Keysym] tomo.Key { var keypadCodeTable = map[xproto.Keysym] input.Key {
0xff80: tomo.Key(' '), 0xff80: input.Key(' '),
0xff89: tomo.KeyTab, 0xff89: input.KeyTab,
0xff8d: tomo.KeyEnter, 0xff8d: input.KeyEnter,
0xff91: tomo.KeyF1, 0xff91: input.KeyF1,
0xff92: tomo.KeyF2, 0xff92: input.KeyF2,
0xff93: tomo.KeyF3, 0xff93: input.KeyF3,
0xff94: tomo.KeyF4, 0xff94: input.KeyF4,
0xff95: tomo.KeyHome, 0xff95: input.KeyHome,
0xff96: tomo.KeyLeft, 0xff96: input.KeyLeft,
0xff97: tomo.KeyUp, 0xff97: input.KeyUp,
0xff98: tomo.KeyRight, 0xff98: input.KeyRight,
0xff99: tomo.KeyDown, 0xff99: input.KeyDown,
0xff9a: tomo.KeyPageUp, 0xff9a: input.KeyPageUp,
0xff9b: tomo.KeyPageDown, 0xff9b: input.KeyPageDown,
0xff9c: tomo.KeyEnd, 0xff9c: input.KeyEnd,
0xff9d: tomo.KeyHome, 0xff9d: input.KeyHome,
0xff9e: tomo.KeyInsert, 0xff9e: input.KeyInsert,
0xff9f: tomo.KeyDelete, 0xff9f: input.KeyDelete,
0xffbd: tomo.Key('='), 0xffbd: input.Key('='),
0xffaa: tomo.Key('*'), 0xffaa: input.Key('*'),
0xffab: tomo.Key('+'), 0xffab: input.Key('+'),
0xffac: tomo.Key(','), 0xffac: input.Key(','),
0xffad: tomo.Key('-'), 0xffad: input.Key('-'),
0xffae: tomo.Key('.'), 0xffae: input.Key('.'),
0xffaf: tomo.Key('/'), 0xffaf: input.Key('/'),
0xffb0: tomo.Key('0'), 0xffb0: input.Key('0'),
0xffb1: tomo.Key('1'), 0xffb1: input.Key('1'),
0xffb2: tomo.Key('2'), 0xffb2: input.Key('2'),
0xffb3: tomo.Key('3'), 0xffb3: input.Key('3'),
0xffb4: tomo.Key('4'), 0xffb4: input.Key('4'),
0xffb5: tomo.Key('5'), 0xffb5: input.Key('5'),
0xffb6: tomo.Key('6'), 0xffb6: input.Key('6'),
0xffb7: tomo.Key('7'), 0xffb7: input.Key('7'),
0xffb8: tomo.Key('8'), 0xffb8: input.Key('8'),
0xffb9: tomo.Key('9'), 0xffb9: input.Key('9'),
} }
// initializeKeymapInformation grabs keyboard mapping information from the X // initializeKeymapInformation grabs keyboard mapping information from the X
@@ -168,7 +168,7 @@ func (backend *Backend) keycodeToKey (
keycode xproto.Keycode, keycode xproto.Keycode,
state uint16, state uint16,
) ( ) (
button tomo.Key, button input.Key,
numberPad bool, numberPad bool,
) { ) {
// PARAGRAPH 3 // PARAGRAPH 3
@@ -359,7 +359,7 @@ func (backend *Backend) keycodeToKey (
if numberPad { return } if numberPad { return }
// otherwise, use the rune // otherwise, use the rune
button = tomo.Key(selectedRune) button = input.Key(selectedRune)
return return
} }
+17 -16
View File
@@ -1,6 +1,7 @@
package x package x
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "github.com/jezek/xgbutil" import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto" import "github.com/jezek/xgb/xproto"
@@ -85,7 +86,7 @@ func (window *Window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (fo
untypedEvent := nextEvents[0] untypedEvent := nextEvents[0]
if untypedEvent.Err == nil { if untypedEvent.Err == nil {
typedEvent, ok := typedEvent, ok :=
untypedEvent.Event.(xproto.ConfigureNotifyEvent) untypedEvent.Event.(xproto.ExposeEvent)
if ok && typedEvent.Window == event.Window { if ok && typedEvent.Window == event.Window {
return true return true
@@ -98,9 +99,9 @@ func (window *Window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (fo
func (window *Window) modifiersFromState ( func (window *Window) modifiersFromState (
state uint16, state uint16,
) ( ) (
modifiers tomo.Modifiers, modifiers input.Modifiers,
) { ) {
return tomo.Modifiers { return input.Modifiers {
Shift: Shift:
(state & xproto.ModMaskShift) > 0 || (state & xproto.ModMaskShift) > 0 ||
(state & window.backend.modifierMasks.shiftLock) > 0, (state & window.backend.modifierMasks.shiftLock) > 0,
@@ -123,18 +124,18 @@ func (window *Window) handleKeyPress (
modifiers := window.modifiersFromState(keyEvent.State) modifiers := window.modifiersFromState(keyEvent.State)
modifiers.NumberPad = numberPad modifiers.NumberPad = numberPad
if key == tomo.KeyTab && modifiers.Alt { if key == input.KeyTab && modifiers.Alt {
if child, ok := window.child.(tomo.Focusable); ok { if child, ok := window.child.(elements.Focusable); ok {
direction := tomo.KeynavDirectionForward direction := input.KeynavDirectionForward
if modifiers.Shift { if modifiers.Shift {
direction = tomo.KeynavDirectionBackward direction = input.KeynavDirectionBackward
} }
if !child.HandleFocus(direction) { if !child.HandleFocus(direction) {
child.HandleUnfocus() child.HandleUnfocus()
} }
} }
} else if child, ok := window.child.(tomo.KeyboardTarget); ok { } else if child, ok := window.child.(elements.KeyboardTarget); ok {
child.HandleKeyDown(key, modifiers) child.HandleKeyDown(key, modifiers)
} }
} }
@@ -153,7 +154,7 @@ func (window *Window) handleKeyRelease (
untypedEvent := nextEvents[0] untypedEvent := nextEvents[0]
if untypedEvent.Err == nil { if untypedEvent.Err == nil {
typedEvent, ok := typedEvent, ok :=
untypedEvent.Event.(xproto.KeyReleaseEvent) untypedEvent.Event.(xproto.KeyPressEvent)
if ok && typedEvent.Detail == keyEvent.Detail && if ok && typedEvent.Detail == keyEvent.Detail &&
typedEvent.Event == keyEvent.Event && typedEvent.Event == keyEvent.Event &&
@@ -168,7 +169,7 @@ func (window *Window) handleKeyRelease (
modifiers := window.modifiersFromState(keyEvent.State) modifiers := window.modifiersFromState(keyEvent.State)
modifiers.NumberPad = numberPad modifiers.NumberPad = numberPad
if child, ok := window.child.(tomo.KeyboardTarget); ok { if child, ok := window.child.(elements.KeyboardTarget); ok {
child.HandleKeyUp(key, modifiers) child.HandleKeyUp(key, modifiers)
} }
} }
@@ -179,7 +180,7 @@ func (window *Window) handleButtonPress (
) { ) {
if window.child == nil { return } if window.child == nil { return }
if child, ok := window.child.(tomo.MouseTarget); ok { if child, ok := window.child.(elements.MouseTarget); ok {
buttonEvent := *event.ButtonPressEvent buttonEvent := *event.ButtonPressEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
sum := scrollSum { } sum := scrollSum { }
@@ -193,7 +194,7 @@ func (window *Window) handleButtonPress (
child.HandleMouseDown ( child.HandleMouseDown (
int(buttonEvent.EventX), int(buttonEvent.EventX),
int(buttonEvent.EventY), int(buttonEvent.EventY),
tomo.Button(buttonEvent.Detail)) input.Button(buttonEvent.Detail))
} }
} }
@@ -205,13 +206,13 @@ func (window *Window) handleButtonRelease (
) { ) {
if window.child == nil { return } if window.child == nil { return }
if child, ok := window.child.(tomo.MouseTarget); ok { if child, ok := window.child.(elements.MouseTarget); ok {
buttonEvent := *event.ButtonReleaseEvent buttonEvent := *event.ButtonReleaseEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
child.HandleMouseUp ( child.HandleMouseUp (
int(buttonEvent.EventX), int(buttonEvent.EventX),
int(buttonEvent.EventY), int(buttonEvent.EventY),
tomo.Button(buttonEvent.Detail)) input.Button(buttonEvent.Detail))
} }
} }
@@ -221,7 +222,7 @@ func (window *Window) handleMotionNotify (
) { ) {
if window.child == nil { return } if window.child == nil { return }
if child, ok := window.child.(tomo.MouseTarget); ok { if child, ok := window.child.(elements.MouseTarget); ok {
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent) motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
child.HandleMouseMove ( child.HandleMouseMove (
int(motionEvent.EventX), int(motionEvent.EventX),
+73 -28
View File
@@ -7,17 +7,24 @@ import "github.com/jezek/xgbutil/icccm"
import "github.com/jezek/xgbutil/xevent" import "github.com/jezek/xgbutil/xevent"
import "github.com/jezek/xgbutil/xwindow" import "github.com/jezek/xgbutil/xwindow"
import "github.com/jezek/xgbutil/xgraphics" import "github.com/jezek/xgbutil/xgraphics"
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/canvas"
import "git.tebibyte.media/sashakoshka/tomo/elements"
type Window struct { type Window struct {
backend *Backend backend *Backend
xWindow *xwindow.Window xWindow *xwindow.Window
xCanvas *xgraphics.Image xCanvas *xgraphics.Image
canvas tomo.BasicCanvas canvas canvas.BasicCanvas
child tomo.Element child elements.Element
onClose func () onClose func ()
skipChildDrawCallback bool skipChildDrawCallback bool
theme theme.Theme
config config.Config
metrics struct { metrics struct {
width int width int
height int height int
@@ -27,7 +34,7 @@ type Window struct {
func (backend *Backend) NewWindow ( func (backend *Backend) NewWindow (
width, height int, width, height int,
) ( ) (
output tomo.Window, output elements.Window,
err error, err error,
) { ) {
if backend == nil { panic("nil backend") } if backend == nil { panic("nil backend") }
@@ -68,6 +75,9 @@ func (backend *Backend) NewWindow (
xevent.MotionNotifyFun(window.handleMotionNotify). xevent.MotionNotifyFun(window.handleMotionNotify).
Connect(backend.connection, window.xWindow.Id) Connect(backend.connection, window.xWindow.Id)
window.SetTheme(backend.theme)
window.SetConfig(backend.config)
window.metrics.width = width window.metrics.width = width
window.metrics.height = height window.metrics.height = height
window.childMinimumSizeChangeCallback(8, 8) window.childMinimumSizeChangeCallback(8, 8)
@@ -79,16 +89,16 @@ func (backend *Backend) NewWindow (
return return
} }
func (window *Window) Adopt (child tomo.Element) { func (window *Window) Adopt (child elements.Element) {
// disown previous child // disown previous child
if window.child != nil { if window.child != nil {
window.child.OnDamage(nil) window.child.OnDamage(nil)
window.child.OnMinimumSizeChange(nil) window.child.OnMinimumSizeChange(nil)
} }
if previousChild, ok := window.child.(tomo.Flexible); ok { if previousChild, ok := window.child.(elements.Flexible); ok {
previousChild.OnFlexibleHeightChange(nil) previousChild.OnFlexibleHeightChange(nil)
} }
if previousChild, ok := window.child.(tomo.Focusable); ok { if previousChild, ok := window.child.(elements.Focusable); ok {
previousChild.OnFocusRequest(nil) previousChild.OnFocusRequest(nil)
previousChild.OnFocusMotionRequest(nil) previousChild.OnFocusMotionRequest(nil)
if previousChild.Focused() { if previousChild.Focused() {
@@ -98,10 +108,16 @@ func (window *Window) Adopt (child tomo.Element) {
// adopt new child // adopt new child
window.child = child window.child = child
if newChild, ok := child.(tomo.Flexible); ok { 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) newChild.OnFlexibleHeightChange(window.resizeChildToFit)
} }
if newChild, ok := child.(tomo.Focusable); ok { if newChild, ok := child.(elements.Focusable); ok {
newChild.OnFocusRequest(window.childSelectionRequestCallback) newChild.OnFocusRequest(window.childSelectionRequestCallback)
} }
if child != nil { if child != nil {
@@ -110,13 +126,14 @@ func (window *Window) Adopt (child tomo.Element) {
window.childMinimumSizeChangeCallback ( window.childMinimumSizeChangeCallback (
child.MinimumSize()) child.MinimumSize())
}) })
if !window.childMinimumSizeChangeCallback(child.MinimumSize()) {
window.resizeChildToFit() window.resizeChildToFit()
window.childMinimumSizeChangeCallback(child.MinimumSize())
window.redrawChildEntirely() window.redrawChildEntirely()
} }
}
} }
func (window *Window) Child () (child tomo.Element) { func (window *Window) Child () (child elements.Element) {
child = window.child child = window.child
return return
} }
@@ -184,9 +201,8 @@ func (window *Window) Hide () {
} }
func (window *Window) Close () { func (window *Window) Close () {
delete(window.backend.windows, window.xWindow.Id)
if window.onClose != nil { window.onClose() } if window.onClose != nil { window.onClose() }
xevent.Detach(window.xWindow.X, window.xWindow.Id) delete(window.backend.windows, window.xWindow.Id)
window.xWindow.Destroy() window.xWindow.Destroy()
} }
@@ -194,10 +210,34 @@ func (window *Window) OnClose (callback func ()) {
window.onClose = callback 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 () { func (window *Window) reallocateCanvas () {
window.canvas = tomo.NewBasicCanvas ( window.canvas.Reallocate(window.metrics.width, window.metrics.height)
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 { if window.xCanvas != nil {
window.xCanvas.Destroy() window.xCanvas.Destroy()
} }
@@ -205,19 +245,21 @@ func (window *Window) reallocateCanvas () {
window.backend.connection, window.backend.connection,
image.Rect ( image.Rect (
0, 0, 0, 0,
window.metrics.width, (newWidth / 64) * 64 + 64,
window.metrics.height)) (newHeight / 64) * 64 + 64))
window.xCanvas.CreatePixmap() window.xCanvas.CreatePixmap()
}
} }
func (window *Window) redrawChildEntirely () { func (window *Window) redrawChildEntirely () {
window.pushRegion(window.paste(window.child)) window.pushRegion(window.paste(window.canvas))
} }
func (window *Window) resizeChildToFit () { func (window *Window) resizeChildToFit () {
window.skipChildDrawCallback = true window.skipChildDrawCallback = true
if child, ok := window.child.(tomo.Flexible); ok { if child, ok := window.child.(elements.Flexible); ok {
minimumHeight := child.FlexibleHeightFor(window.metrics.width) minimumHeight := child.FlexibleHeightFor(window.metrics.width)
minimumWidth, _ := child.MinimumSize() minimumWidth, _ := child.MinimumSize()
@@ -240,12 +282,12 @@ func (window *Window) resizeChildToFit () {
window.skipChildDrawCallback = false window.skipChildDrawCallback = false
} }
func (window *Window) childDrawCallback (region tomo.Canvas) { func (window *Window) childDrawCallback (region canvas.Canvas) {
if window.skipChildDrawCallback { return } if window.skipChildDrawCallback { return }
window.pushRegion(window.paste(region)) window.pushRegion(window.paste(region))
} }
func (window *Window) paste (canvas tomo.Canvas) (updatedRegion image.Rectangle) { func (window *Window) paste (canvas canvas.Canvas) (updatedRegion image.Rectangle) {
data, stride := canvas.Buffer() data, stride := canvas.Buffer()
bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds()) bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds())
for x := bounds.Min.X; x < bounds.Max.X; x ++ { for x := bounds.Min.X; x < bounds.Max.X; x ++ {
@@ -261,7 +303,7 @@ func (window *Window) paste (canvas tomo.Canvas) (updatedRegion image.Rectangle)
return bounds return bounds
} }
func (window *Window) childMinimumSizeChangeCallback (width, height int) { func (window *Window) childMinimumSizeChangeCallback (width, height int) (resized bool) {
icccm.WmNormalHintsSet ( icccm.WmNormalHintsSet (
window.backend.connection, window.backend.connection,
window.xWindow.Id, window.xWindow.Id,
@@ -277,22 +319,25 @@ func (window *Window) childMinimumSizeChangeCallback (width, height int) {
if newWidth != window.metrics.width || if newWidth != window.metrics.width ||
newHeight != window.metrics.height { newHeight != window.metrics.height {
window.xWindow.Resize(newWidth, newHeight) window.xWindow.Resize(newWidth, newHeight)
return true
} }
return false
} }
func (window *Window) childSelectionRequestCallback () (granted bool) { func (window *Window) childSelectionRequestCallback () (granted bool) {
if child, ok := window.child.(tomo.Focusable); ok { if _, ok := window.child.(elements.Focusable); ok {
child.HandleFocus(tomo.KeynavDirectionNeutral)
}
return true return true
}
return false
} }
func (window *Window) childSelectionMotionRequestCallback ( func (window *Window) childSelectionMotionRequestCallback (
direction tomo.KeynavDirection, direction input.KeynavDirection,
) ( ) (
granted bool, granted bool,
) { ) {
if child, ok := window.child.(tomo.Focusable); ok { if child, ok := window.child.(elements.Focusable); ok {
if !child.HandleFocus(direction) { if !child.HandleFocus(direction) {
child.HandleUnfocus() child.HandleUnfocus()
} }
+39 -2
View File
@@ -1,6 +1,9 @@
package x package x
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "github.com/jezek/xgbutil" import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto" import "github.com/jezek/xgb/xproto"
@@ -24,7 +27,12 @@ type Backend struct {
hyper uint16 hyper uint16
} }
theme theme.Theme
config config.Config
windows map[xproto.Window] *Window windows map[xproto.Window] *Window
open bool
} }
// NewBackend instantiates an X backend. // NewBackend instantiates an X backend.
@@ -32,6 +40,9 @@ func NewBackend () (output tomo.Backend, err error) {
backend := &Backend { backend := &Backend {
windows: map[xproto.Window] *Window { }, windows: map[xproto.Window] *Window { },
doChannel: make(chan func (), 0), doChannel: make(chan func (), 0),
theme: theme.Default { },
config: config.Default { },
open: true,
} }
// connect to X // connect to X
@@ -65,7 +76,14 @@ func (backend *Backend) Run () (err error) {
// Stop gracefully closes the connection and stops the event loop. // Stop gracefully closes the connection and stops the event loop.
func (backend *Backend) Stop () { func (backend *Backend) Stop () {
backend.assert() backend.assert()
if !backend.open { return }
backend.open = false
toClose := []*Window { }
for _, window := range backend.windows { for _, window := range backend.windows {
toClose = append(toClose, window)
}
for _, window := range toClose {
window.Close() window.Close()
} }
xevent.Quit(backend.connection) xevent.Quit(backend.connection)
@@ -81,19 +99,38 @@ func (backend *Backend) Do (callback func ()) {
// Copy puts data into the clipboard. This method is not yet implemented and // Copy puts data into the clipboard. This method is not yet implemented and
// will do nothing! // will do nothing!
func (backend *Backend) Copy (data tomo.Data) { func (backend *Backend) Copy (data data.Data) {
backend.assert() backend.assert()
// TODO // TODO
} }
// Paste returns the data currently in the clipboard. This method may // Paste returns the data currently in the clipboard. This method may
// return nil. This method is not yet implemented and will do nothing! // return nil. This method is not yet implemented and will do nothing!
func (backend *Backend) Paste (accept []tomo.Mime) (data tomo.Data) { func (backend *Backend) Paste (accept []data.Mime) (data data.Data) {
backend.assert() backend.assert()
// TODO // TODO
return 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 () { func (backend *Backend) assert () {
if backend == nil { panic("nil backend") } if backend == nil { panic("nil backend") }
} }
+40 -1
View File
@@ -1,9 +1,15 @@
package tomo package canvas
import "image" import "image"
import "image/draw" import "image/draw"
import "image/color" import "image/color"
// Image represents an immutable canvas.
type Image interface {
image.Image
RGBAAt (x, y int) color.RGBA
}
// Canvas is like draw.Image but is also able to return a raw pixel buffer for // Canvas is like draw.Image but is also able to return a raw pixel buffer for
// more efficient drawing. This interface can be easily satisfied using a // more efficient drawing. This interface can be easily satisfied using a
// BasicCanvas struct. // BasicCanvas struct.
@@ -28,6 +34,21 @@ func NewBasicCanvas (width, height int) (canvas BasicCanvas) {
return return
} }
// FromImage creates a new BasicCanvas from an image.Image.
func FromImage (img image.Image) (canvas BasicCanvas) {
bounds := img.Bounds()
canvas = NewBasicCanvas(bounds.Dx(), bounds.Dy())
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 ++ {
canvasPoint := point.Sub(bounds.Min)
canvas.Set (
canvasPoint.X, canvasPoint.Y,
img.At(point.X, point.Y))
}}
return
}
// you know what it do // you know what it do
func (canvas BasicCanvas) Bounds () (bounds image.Rectangle) { func (canvas BasicCanvas) Bounds () (bounds image.Rectangle) {
return canvas.rect return canvas.rect
@@ -61,6 +82,24 @@ func (canvas BasicCanvas) Buffer () (data []color.RGBA, stride int) {
return canvas.pix, canvas.stride return canvas.pix, canvas.stride
} }
// Reallocate efficiently reallocates the canvas. The data within will be
// garbage. This method will do nothing if this is a cut image.
func (canvas *BasicCanvas) Reallocate (width, height int) {
if canvas.rect.Min != (image.Point { }) { return }
previousLen := len(canvas.pix)
newLen := width * height
bigger := newLen > previousLen
smaller := newLen < previousLen / 2
if bigger || smaller {
canvas.pix = make (
[]color.RGBA,
((height * width) / 4096) * 4096 + 4096)
}
canvas.stride = width
canvas.rect = image.Rect(0, 0, width, height)
}
// Cut returns a sub-canvas of a given canvas. // Cut returns a sub-canvas of a given canvas.
func Cut (canvas Canvas, bounds image.Rectangle) (reduced BasicCanvas) { func Cut (canvas Canvas, bounds image.Rectangle) (reduced BasicCanvas) {
// println(canvas.Bounds().String(), bounds.String()) // println(canvas.Bounds().String(), bounds.String())
+4
View File
@@ -0,0 +1,4 @@
// 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
+62
View File
@@ -0,0 +1,62 @@
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
}
+9
View File
@@ -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 Config) {
// TODO
return Default { }
}
+1 -1
View File
@@ -1,4 +1,4 @@
package tomo package data
import "io" import "io"
+82
View File
@@ -0,0 +1,82 @@
package dirs
import "os"
import "strings"
import "path/filepath"
var homeDirectory string
var configHome string
var configDirs []string
var dataHome string
var dataDirs []string
var cacheHome string
func init () {
var err error
homeDirectory, err = os.UserHomeDir()
if err != nil {
panic("could not get user home directory: " + err.Error())
}
configHome = os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
configHome = filepath.Join(homeDirectory, "/.config/")
}
configDirsString := os.Getenv("XDG_CONFIG_DIRS")
if configDirsString == "" {
configDirsString = "/etc/xdg/"
}
configDirs = append(strings.Split(configDirsString, ":"), configHome)
dataHome = os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
dataHome = filepath.Join(homeDirectory, "/.local/share/")
}
dataDirsString := os.Getenv("XDG_CONFIG_DIRS")
if dataDirsString == "" {
dataDirsString = "/usr/local/share/:/usr/share/"
}
configDirs = append(strings.Split(configDirsString, ":"), configHome)
cacheHome = os.Getenv("XDG_CACHE_HOME")
if cacheHome == "" {
cacheHome = filepath.Join(homeDirectory, "/.cache/")
}
}
// ConfigHome returns the path to the directory where user configuration files
// should be stored.
func ConfigHome (name string) (home string) {
return filepath.Join(configHome, name)
}
// ConfigDirs returns all paths where configuration files might exist.
func ConfigDirs (name string) (dirs []string) {
dirs = make([]string, len(configDirs))
for index, dir := range configDirs {
dirs[index] = filepath.Join(dir, name)
}
return
}
// DataHome returns the path to the directory where user data should be stored.
func DataHome (name string) (home string) {
return filepath.Join(dataHome, name)
}
// DataDirs returns all paths where data files might exist.
func DataDirs (name string) (dirs []string) {
dirs = make([]string, len(dataDirs))
for index, dir := range dataDirs {
dirs[index] = filepath.Join(dir, name)
}
return
}
// CacheHome returns the path to the directory where user cache files should be
// stored.
func CacheHome (name string) (home string) {
return filepath.Join(cacheHome, name)
}
+105 -60
View File
@@ -1,92 +1,79 @@
package basic package basicElements
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo" // import "runtime/debug"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme" 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"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var buttonCase = theme.C("basic", "button")
// Button is a clickable button. // Button is a clickable button.
type Button struct { type Button struct {
*core.Core *core.Core
*core.FocusableCore *core.FocusableCore
core core.CoreControl core core.CoreControl
focusableControl core.FocusableCoreControl focusableControl core.FocusableCoreControl
drawer artist.TextDrawer drawer textdraw.Drawer
pressed bool pressed bool
text string text string
config config.Wrapped
theme theme.Wrapped
onClick func () onClick func ()
} }
// NewButton creates a new button with the specified label text. // NewButton creates a new button with the specified label text.
func NewButton (text string) (element *Button) { func NewButton (text string) (element *Button) {
element = &Button { } element = &Button { }
element.Core, element.core = core.NewCore(element.draw) element.theme.Case = theme.C("basic", "button")
element.Core, element.core = core.NewCore(element.drawAll)
element.FocusableCore, element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () { element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () { element.drawAndPush(true)
element.draw()
element.core.DamageAll()
}
}) })
element.drawer.SetFace(theme.FontFaceRegular())
element.SetText(text) element.SetText(text)
return return
} }
func (element *Button) HandleMouseDown (x, y int, button tomo.Button) { func (element *Button) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return } if !element.Enabled() { return }
if !element.Focused() { element.Focus() } if !element.Focused() { element.Focus() }
if button != tomo.ButtonLeft { return } if button != input.ButtonLeft { return }
element.pressed = true element.pressed = true
if element.core.HasImage() { element.drawAndPush(true)
element.draw()
element.core.DamageAll()
}
} }
func (element *Button) HandleMouseUp (x, y int, button tomo.Button) { func (element *Button) HandleMouseUp (x, y int, button input.Button) {
if button != tomo.ButtonLeft { return } if button != input.ButtonLeft { return }
element.pressed = false element.pressed = false
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
within := image.Point { x, y }. within := image.Point { x, y }.
In(element.Bounds()) In(element.Bounds())
if element.Enabled() && within && element.onClick != nil {
if !element.Enabled() { return }
if within && element.onClick != nil {
element.onClick() element.onClick()
} }
element.drawAndPush(true)
} }
func (element *Button) HandleMouseMove (x, y int) { } func (element *Button) HandleMouseMove (x, y int) { }
func (element *Button) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *Button) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Button) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) { func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return } if !element.Enabled() { return }
if key == tomo.KeyEnter { if key == input.KeyEnter {
element.pressed = true element.pressed = true
if element.core.HasImage() { element.drawAndPush(true)
element.draw()
element.core.DamageAll()
}
} }
} }
func (element *Button) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
if key == tomo.KeyEnter && element.pressed { if key == input.KeyEnter && element.pressed {
element.pressed = false element.pressed = false
if element.core.HasImage() { element.drawAndPush(true)
element.draw()
element.core.DamageAll()
}
if !element.Enabled() { return } if !element.Enabled() { return }
if element.onClick != nil { if element.onClick != nil {
element.onClick() element.onClick()
@@ -110,44 +97,102 @@ func (element *Button) SetText (text string) {
element.text = text element.text = text
element.drawer.SetText([]rune(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() textBounds := element.drawer.LayoutBounds()
_, inset := theme.ButtonPattern(theme.PatternState { Case: buttonCase }) padding := element.theme.Padding(theme.PatternButton)
minimumSize := inset.Inverse().Apply(textBounds).Inset(-theme.Padding()) minimumSize := padding.Inverse().Apply(textBounds)
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
}
func (element *Button) drawAndPush (partial bool) {
if element.core.HasImage () { if element.core.HasImage () {
element.draw() if partial {
element.core.DamageRegion (append (
element.drawBackground(true),
element.drawText(true))...)
} else {
element.drawAll()
element.core.DamageAll() element.core.DamageAll()
} }
}
} }
func (element *Button) draw () { func (element *Button) state () theme.State {
bounds := element.Bounds() return theme.State {
pattern, inset := theme.ButtonPattern(theme.PatternState {
Case: buttonCase,
Disabled: !element.Enabled(), Disabled: !element.Enabled(),
Focused: element.Focused(), Focused: element.Focused(),
Pressed: element.pressed, Pressed: element.pressed,
}) }
}
artist.FillRectangle(element, pattern, bounds) 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
innerBounds := inset.Apply(bounds) 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() textBounds := element.drawer.LayoutBounds()
offset := image.Point { offset := image.Point {
X: innerBounds.Min.X + (innerBounds.Dx() - textBounds.Dx()) / 2, X: bounds.Min.X + (bounds.Dx() - textBounds.Dx()) / 2,
Y: innerBounds.Min.Y + (innerBounds.Dy() - textBounds.Dy()) / 2, Y: bounds.Min.Y + (bounds.Dy() - textBounds.Dy()) / 2,
} }
// account for the fact that the bounding rectangle will be shifted over
// due to the bounds origin being at the baseline of the first line
offset.Y -= textBounds.Min.Y offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X offset.X -= textBounds.Min.X
region := textBounds.Union(textBounds.Add(sink)).Add(offset)
foreground, _ := theme.ForegroundPattern (theme.PatternState { if element.pressed {
Case: buttonCase, offset = offset.Add(sink)
Disabled: !element.Enabled(), }
})
element.drawer.Draw(element, foreground, offset) 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)
} }
+64 -42
View File
@@ -1,45 +1,43 @@
package basic package basicElements
import "image" 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/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var checkboxCase = theme.C("basic", "checkbox")
// Checkbox is a toggle-able checkbox with a label. // Checkbox is a toggle-able checkbox with a label.
type Checkbox struct { type Checkbox struct {
*core.Core *core.Core
*core.FocusableCore *core.FocusableCore
core core.CoreControl core core.CoreControl
focusableControl core.FocusableCoreControl focusableControl core.FocusableCoreControl
drawer artist.TextDrawer drawer textdraw.Drawer
pressed bool pressed bool
checked bool checked bool
text string text string
config config.Wrapped
theme theme.Wrapped
onToggle func () onToggle func ()
} }
// NewCheckbox creates a new cbeckbox with the specified label text. // NewCheckbox creates a new cbeckbox with the specified label text.
func NewCheckbox (text string, checked bool) (element *Checkbox) { func NewCheckbox (text string, checked bool) (element *Checkbox) {
element = &Checkbox { checked: checked } element = &Checkbox { checked: checked }
element.theme.Case = theme.C("basic", "checkbox")
element.Core, element.core = core.NewCore(element.draw) element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore, element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () { element.focusableControl = core.NewFocusableCore(element.redo)
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.drawer.SetFace(theme.FontFaceRegular())
element.SetText(text) element.SetText(text)
return return
} }
func (element *Checkbox) HandleMouseDown (x, y int, button tomo.Button) { func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return } if !element.Enabled() { return }
element.Focus() element.Focus()
element.pressed = true element.pressed = true
@@ -49,8 +47,8 @@ func (element *Checkbox) HandleMouseDown (x, y int, button tomo.Button) {
} }
} }
func (element *Checkbox) HandleMouseUp (x, y int, button tomo.Button) { func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) {
if button != tomo.ButtonLeft || !element.pressed { return } if button != input.ButtonLeft || !element.pressed { return }
element.pressed = false element.pressed = false
within := image.Point { x, y }. within := image.Point { x, y }.
@@ -71,8 +69,8 @@ func (element *Checkbox) HandleMouseUp (x, y int, button tomo.Button) {
func (element *Checkbox) HandleMouseMove (x, y int) { } func (element *Checkbox) HandleMouseMove (x, y int) { }
func (element *Checkbox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *Checkbox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Checkbox) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) { func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == tomo.KeyEnter { if key == input.KeyEnter {
element.pressed = true element.pressed = true
if element.core.HasImage() { if element.core.HasImage() {
element.draw() element.draw()
@@ -81,8 +79,8 @@ func (element *Checkbox) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers)
} }
} }
func (element *Checkbox) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) { func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == tomo.KeyEnter && element.pressed { if key == input.KeyEnter && element.pressed {
element.pressed = false element.pressed = false
element.checked = !element.checked element.checked = !element.checked
if element.core.HasImage() { if element.core.HasImage() {
@@ -116,16 +114,46 @@ func (element *Checkbox) SetText (text string) {
element.text = text element.text = text
element.drawer.SetText([]rune(text)) element.drawer.SetText([]rune(text))
textBounds := element.drawer.LayoutBounds() element.updateMinimumSize()
if text == "" { 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()) element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
} else { } else {
margin := element.theme.Margin(theme.PatternBackground)
element.core.SetMinimumSize ( element.core.SetMinimumSize (
textBounds.Dy() + theme.Padding() + textBounds.Dx(), textBounds.Dy() + margin.X + textBounds.Dx(),
textBounds.Dy()) textBounds.Dy())
} }
}
func (element *Checkbox) redo () {
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.DamageAll() element.core.DamageAll()
@@ -136,35 +164,29 @@ func (element *Checkbox) draw () {
bounds := element.Bounds() bounds := element.Bounds()
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState { state := theme.State {
Case: checkboxCase,
})
artist.FillRectangle(element, backgroundPattern, bounds)
pattern, inset := theme.ButtonPattern(theme.PatternState {
Case: checkboxCase,
Disabled: !element.Enabled(), Disabled: !element.Enabled(),
Focused: element.Focused(), Focused: element.Focused(),
Pressed: element.pressed, Pressed: element.pressed,
}) On: element.checked,
artist.FillRectangle(element, pattern, boxBounds) }
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() textBounds := element.drawer.LayoutBounds()
margin := element.theme.Margin(theme.PatternBackground)
offset := bounds.Min.Add(image.Point { offset := bounds.Min.Add(image.Point {
X: bounds.Dy() + theme.Padding(), X: bounds.Dy() + margin.X,
}) })
offset.Y -= textBounds.Min.Y offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X offset.X -= textBounds.Min.X
foreground, _ := theme.ForegroundPattern (theme.PatternState { foreground := element.theme.Color(theme.ColorForeground, state)
Case: checkboxCase, element.drawer.Draw(element.core, foreground, offset)
Disabled: !element.Enabled(),
})
element.drawer.Draw(element, foreground, offset)
if element.checked {
checkBounds := inset.Apply(boxBounds).Inset(2)
artist.FillRectangle(element, foreground, checkBounds)
}
} }
+134 -73
View File
@@ -1,42 +1,48 @@
package basic package basicElements
import "image" 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/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/artist"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var containerCase = theme.C("basic", "container")
// Container is an element capable of containg other elements, and arranging // Container is an element capable of containg other elements, and arranging
// them in a layout. // them in a layout.
type Container struct { type Container struct {
*core.Core *core.Core
core core.CoreControl core core.CoreControl
layout tomo.Layout layout layouts.Layout
children []tomo.LayoutEntry children []layouts.LayoutEntry
drags [10]tomo.MouseTarget drags [10]elements.MouseTarget
warping bool warping bool
focused bool focused bool
focusable bool focusable bool
flexible bool flexible bool
config config.Wrapped
theme theme.Wrapped
onFocusRequest func () (granted bool) onFocusRequest func () (granted bool)
onFocusMotionRequest func (tomo.KeynavDirection) (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool)
onFlexibleHeightChange func () onFlexibleHeightChange func ()
} }
// NewContainer creates a new container. // NewContainer creates a new container.
func NewContainer (layout tomo.Layout) (element *Container) { func NewContainer (layout layouts.Layout) (element *Container) {
element = &Container { } element = &Container { }
element.theme.Case = theme.C("basic", "container")
element.Core, element.core = core.NewCore(element.redoAll) element.Core, element.core = core.NewCore(element.redoAll)
element.SetLayout(layout) element.SetLayout(layout)
return return
} }
// SetLayout sets the layout of this container. // SetLayout sets the layout of this container.
func (element *Container) SetLayout (layout tomo.Layout) { func (element *Container) SetLayout (layout layouts.Layout) {
element.layout = layout element.layout = layout
if element.core.HasImage() { if element.core.HasImage() {
element.redoAll() element.redoAll()
@@ -47,28 +53,40 @@ func (element *Container) SetLayout (layout tomo.Layout) {
// Adopt adds a new child element to the container. If expand is set to true, // 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 // the element will expand (instead of contract to its minimum size), in
// whatever way is defined by the current layout. // whatever way is defined by the current layout.
func (element *Container) Adopt (child tomo.Element, expand bool) { func (element *Container) Adopt (child elements.Element, expand bool) {
// set event handlers // set event handlers
child.OnDamage (func (region tomo.Canvas) { 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()) element.core.DamageRegion(region.Bounds())
}) })
child.OnMinimumSizeChange(element.updateMinimumSize) child.OnMinimumSizeChange (func () {
if child0, ok := child.(tomo.Flexible); ok { // 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) child0.OnFlexibleHeightChange(element.updateMinimumSize)
} }
if child0, ok := child.(tomo.Focusable); ok { if child0, ok := child.(elements.Focusable); ok {
child0.OnFocusRequest (func () (granted bool) { child0.OnFocusRequest (func () (granted bool) {
return element.childFocusRequestCallback(child0) return element.childFocusRequestCallback(child0)
}) })
child0.OnFocusMotionRequest ( child0.OnFocusMotionRequest (
func (direction tomo.KeynavDirection) (granted bool) { func (direction input.KeynavDirection) (granted bool) {
if element.onFocusMotionRequest == nil { return } if element.onFocusMotionRequest == nil { return }
return element.onFocusMotionRequest(direction) return element.onFocusMotionRequest(direction)
}) })
} }
// add child // add child
element.children = append (element.children, tomo.LayoutEntry { element.children = append (element.children, layouts.LayoutEntry {
Element: child, Element: child,
Expand: expand, Expand: expand,
}) })
@@ -106,7 +124,7 @@ func (element *Container) Warp (callback func ()) {
// Disown removes the given child from the container if it is contained within // Disown removes the given child from the container if it is contained within
// it. // it.
func (element *Container) Disown (child tomo.Element) { func (element *Container) Disown (child elements.Element) {
for index, entry := range element.children { for index, entry := range element.children {
if entry.Element == child { if entry.Element == child {
element.clearChildEventHandlers(entry.Element) element.clearChildEventHandlers(entry.Element)
@@ -125,18 +143,18 @@ func (element *Container) Disown (child tomo.Element) {
} }
} }
func (element *Container) clearChildEventHandlers (child tomo.Element) { func (element *Container) clearChildEventHandlers (child elements.Element) {
child.DrawTo(nil) child.DrawTo(nil)
child.OnDamage(nil) child.OnDamage(nil)
child.OnMinimumSizeChange(nil) child.OnMinimumSizeChange(nil)
if child0, ok := child.(tomo.Focusable); ok { if child0, ok := child.(elements.Focusable); ok {
child0.OnFocusRequest(nil) child0.OnFocusRequest(nil)
child0.OnFocusMotionRequest(nil) child0.OnFocusMotionRequest(nil)
if child0.Focused() { if child0.Focused() {
child0.HandleUnfocus() child0.HandleUnfocus()
} }
} }
if child0, ok := child.(tomo.Flexible); ok { if child0, ok := child.(elements.Flexible); ok {
child0.OnFlexibleHeightChange(nil) child0.OnFlexibleHeightChange(nil)
} }
} }
@@ -154,8 +172,8 @@ func (element *Container) DisownAll () {
} }
// Children returns a slice containing this element's children. // Children returns a slice containing this element's children.
func (element *Container) Children () (children []tomo.Element) { func (element *Container) Children () (children []elements.Element) {
children = make([]tomo.Element, len(element.children)) children = make([]elements.Element, len(element.children))
for index, entry := range element.children { for index, entry := range element.children {
children[index] = entry.Element children[index] = entry.Element
} }
@@ -169,14 +187,14 @@ func (element *Container) CountChildren () (count int) {
// Child returns the child at the specified index. If the index is out of // Child returns the child at the specified index. If the index is out of
// bounds, this method will return nil. // bounds, this method will return nil.
func (element *Container) Child (index int) (child tomo.Element) { func (element *Container) Child (index int) (child elements.Element) {
if index < 0 || index > len(element.children) { return } if index < 0 || index > len(element.children) { return }
return element.children[index].Element return element.children[index].Element
} }
// ChildAt returns the child that contains the specified x and y coordinates. If // 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. // there are no children at the coordinates, this method will return nil.
func (element *Container) ChildAt (point image.Point) (child tomo.Element) { func (element *Container) ChildAt (point image.Point) (child elements.Element) {
for _, entry := range element.children { for _, entry := range element.children {
if point.In(entry.Bounds) { if point.In(entry.Bounds) {
child = entry.Element child = entry.Element
@@ -185,7 +203,7 @@ func (element *Container) ChildAt (point image.Point) (child tomo.Element) {
return return
} }
func (element *Container) childPosition (child tomo.Element) (position image.Point) { func (element *Container) childPosition (child elements.Element) (position image.Point) {
for _, entry := range element.children { for _, entry := range element.children {
if entry.Element == child { if entry.Element == child {
position = entry.Bounds.Min position = entry.Bounds.Min
@@ -197,30 +215,62 @@ func (element *Container) childPosition (child tomo.Element) (position image.Poi
} }
func (element *Container) redoAll () { func (element *Container) redoAll () {
if !element.core.HasImage() { return }
// do a layout // do a layout
element.recalculate() element.doLayout()
// draw a background // draw a background
bounds := element.Bounds() rocks := make([]image.Rectangle, len(element.children))
pattern, _ := theme.BackgroundPattern (theme.PatternState { for index, entry := range element.children {
Case: containerCase, rocks[index] = entry.Bounds
}) }
artist.FillRectangle(element, pattern, 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 // cut our canvas up and give peices to child elements
for _, entry := range element.children { for _, entry := range element.children {
entry.DrawTo(tomo.Cut(element, entry.Bounds)) entry.DrawTo(canvas.Cut(element.core, entry.Bounds))
} }
} }
func (element *Container) HandleMouseDown (x, y int, button tomo.Button) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget) // 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 } if !handlesMouse { return }
element.drags[button] = child element.drags[button] = child
child.HandleMouseDown(x, y, button) child.HandleMouseDown(x, y, button)
} }
func (element *Container) HandleMouseUp (x, y int, button tomo.Button) { func (element *Container) HandleMouseUp (x, y int, button input.Button) {
child := element.drags[button] child := element.drags[button]
if child == nil { return } if child == nil { return }
element.drags[button] = nil element.drags[button] = nil
@@ -235,14 +285,14 @@ func (element *Container) HandleMouseMove (x, y int) {
} }
func (element *Container) HandleMouseScroll (x, y int, deltaX, deltaY float64) { func (element *Container) HandleMouseScroll (x, y int, deltaX, deltaY float64) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget) child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget)
if !handlesMouse { return } if !handlesMouse { return }
child.HandleMouseScroll(x, y, deltaX, deltaY) child.HandleMouseScroll(x, y, deltaX, deltaY)
} }
func (element *Container) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) { func (element *Container) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
element.forFocused (func (child tomo.Focusable) bool { element.forFocused (func (child elements.Focusable) bool {
child0, handlesKeyboard := child.(tomo.KeyboardTarget) child0, handlesKeyboard := child.(elements.KeyboardTarget)
if handlesKeyboard { if handlesKeyboard {
child0.HandleKeyDown(key, modifiers) child0.HandleKeyDown(key, modifiers)
} }
@@ -250,9 +300,9 @@ func (element *Container) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers)
}) })
} }
func (element *Container) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) { func (element *Container) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
element.forFocused (func (child tomo.Focusable) bool { element.forFocused (func (child elements.Focusable) bool {
child0, handlesKeyboard := child.(tomo.KeyboardTarget) child0, handlesKeyboard := child.(elements.KeyboardTarget)
if handlesKeyboard { if handlesKeyboard {
child0.HandleKeyUp(key, modifiers) child0.HandleKeyUp(key, modifiers)
} }
@@ -261,7 +311,11 @@ func (element *Container) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
} }
func (element *Container) FlexibleHeightFor (width int) (height int) { func (element *Container) FlexibleHeightFor (width int) (height int) {
return element.layout.FlexibleHeightFor(element.children, width) 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 ()) { func (element *Container) OnFlexibleHeightChange (callback func ()) {
@@ -278,7 +332,7 @@ func (element *Container) Focus () {
} }
} }
func (element *Container) HandleFocus (direction tomo.KeynavDirection) (ok bool) { func (element *Container) HandleFocus (direction input.KeynavDirection) (ok bool) {
if !element.focusable { return false } if !element.focusable { return false }
direction = direction.Canon() direction = direction.Canon()
@@ -288,12 +342,12 @@ func (element *Container) HandleFocus (direction tomo.KeynavDirection) (ok bool)
// the first or last focusable element depending on the // the first or last focusable element depending on the
// direction. // direction.
switch direction { switch direction {
case tomo.KeynavDirectionNeutral, tomo.KeynavDirectionForward: case input.KeynavDirectionNeutral, input.KeynavDirectionForward:
// if we recieve a neutral or forward direction, focus // if we recieve a neutral or forward direction, focus
// the first focusable element. // the first focusable element.
return element.focusFirstFocusableElement(direction) return element.focusFirstFocusableElement(direction)
case tomo.KeynavDirectionBackward: case input.KeynavDirectionBackward:
// if we recieve a backward direction, focus the last // if we recieve a backward direction, focus the last
// focusable element. // focusable element.
return element.focusLastFocusableElement(direction) return element.focusLastFocusableElement(direction)
@@ -302,7 +356,7 @@ func (element *Container) HandleFocus (direction tomo.KeynavDirection) (ok bool)
// an element is currently focused, so we need to move the // an element is currently focused, so we need to move the
// focus in the specified direction // focus in the specified direction
firstFocusedChild := firstFocusedChild :=
element.children[firstFocused].Element.(tomo.Focusable) element.children[firstFocused].Element.(elements.Focusable)
// before we move the focus, the currently focused child // before we move the focus, the currently focused child
// may also be able to move its focus. if the child is able // may also be able to move its focus. if the child is able
@@ -319,7 +373,7 @@ func (element *Container) HandleFocus (direction tomo.KeynavDirection) (ok bool)
child, focusable := child, focusable :=
element.children[index]. element.children[index].
Element.(tomo.Focusable) Element.(elements.Focusable)
if focusable && child.HandleFocus(direction) { if focusable && child.HandleFocus(direction) {
// we have found one, so we now actually move // we have found one, so we now actually move
// the focus. // the focus.
@@ -334,11 +388,11 @@ func (element *Container) HandleFocus (direction tomo.KeynavDirection) (ok bool)
} }
func (element *Container) focusFirstFocusableElement ( func (element *Container) focusFirstFocusableElement (
direction tomo.KeynavDirection, direction input.KeynavDirection,
) ( ) (
ok bool, ok bool,
) { ) {
element.forFocusable (func (child tomo.Focusable) bool { element.forFocusable (func (child elements.Focusable) bool {
if child.HandleFocus(direction) { if child.HandleFocus(direction) {
element.focused = true element.focused = true
ok = true ok = true
@@ -350,11 +404,11 @@ func (element *Container) focusFirstFocusableElement (
} }
func (element *Container) focusLastFocusableElement ( func (element *Container) focusLastFocusableElement (
direction tomo.KeynavDirection, direction input.KeynavDirection,
) ( ) (
ok bool, ok bool,
) { ) {
element.forFocusableBackward (func (child tomo.Focusable) bool { element.forFocusableBackward (func (child elements.Focusable) bool {
if child.HandleFocus(direction) { if child.HandleFocus(direction) {
element.focused = true element.focused = true
ok = true ok = true
@@ -367,7 +421,7 @@ func (element *Container) focusLastFocusableElement (
func (element *Container) HandleUnfocus () { func (element *Container) HandleUnfocus () {
element.focused = false element.focused = false
element.forFocused (func (child tomo.Focusable) bool { element.forFocused (func (child elements.Focusable) bool {
child.HandleUnfocus() child.HandleUnfocus()
return true return true
}) })
@@ -378,41 +432,41 @@ func (element *Container) OnFocusRequest (callback func () (granted bool)) {
} }
func (element *Container) OnFocusMotionRequest ( func (element *Container) OnFocusMotionRequest (
callback func (direction tomo.KeynavDirection) (granted bool), callback func (direction input.KeynavDirection) (granted bool),
) { ) {
element.onFocusMotionRequest = callback element.onFocusMotionRequest = callback
} }
func (element *Container) forFocused (callback func (child tomo.Focusable) bool) { func (element *Container) forFocused (callback func (child elements.Focusable) bool) {
for _, entry := range element.children { for _, entry := range element.children {
child, focusable := entry.Element.(tomo.Focusable) child, focusable := entry.Element.(elements.Focusable)
if focusable && child.Focused() { if focusable && child.Focused() {
if !callback(child) { break } if !callback(child) { break }
} }
} }
} }
func (element *Container) forFocusable (callback func (child tomo.Focusable) bool) { func (element *Container) forFocusable (callback func (child elements.Focusable) bool) {
for _, entry := range element.children { for _, entry := range element.children {
child, focusable := entry.Element.(tomo.Focusable) child, focusable := entry.Element.(elements.Focusable)
if focusable { if focusable {
if !callback(child) { break } if !callback(child) { break }
} }
} }
} }
func (element *Container) forFlexible (callback func (child tomo.Flexible) bool) { func (element *Container) forFlexible (callback func (child elements.Flexible) bool) {
for _, entry := range element.children { for _, entry := range element.children {
child, flexible := entry.Element.(tomo.Flexible) child, flexible := entry.Element.(elements.Flexible)
if flexible { if flexible {
if !callback(child) { break } if !callback(child) { break }
} }
} }
} }
func (element *Container) forFocusableBackward (callback func (child tomo.Focusable) bool) { func (element *Container) forFocusableBackward (callback func (child elements.Focusable) bool) {
for index := len(element.children) - 1; index >= 0; index -- { for index := len(element.children) - 1; index >= 0; index -- {
child, focusable := element.children[index].Element.(tomo.Focusable) child, focusable := element.children[index].Element.(elements.Focusable)
if focusable { if focusable {
if !callback(child) { break } if !callback(child) { break }
} }
@@ -421,7 +475,7 @@ func (element *Container) forFocusableBackward (callback func (child tomo.Focusa
func (element *Container) firstFocused () (index int) { func (element *Container) firstFocused () (index int) {
for currentIndex, entry := range element.children { for currentIndex, entry := range element.children {
child, focusable := entry.Element.(tomo.Focusable) child, focusable := entry.Element.(elements.Focusable)
if focusable && child.Focused() { if focusable && child.Focused() {
return currentIndex return currentIndex
} }
@@ -431,12 +485,12 @@ func (element *Container) firstFocused () (index int) {
func (element *Container) reflectChildProperties () { func (element *Container) reflectChildProperties () {
element.focusable = false element.focusable = false
element.forFocusable (func (tomo.Focusable) bool { element.forFocusable (func (elements.Focusable) bool {
element.focusable = true element.focusable = true
return false return false
}) })
element.flexible = false element.flexible = false
element.forFlexible (func (tomo.Flexible) bool { element.forFlexible (func (elements.Flexible) bool {
element.flexible = true element.flexible = true
return false return false
}) })
@@ -446,16 +500,16 @@ func (element *Container) reflectChildProperties () {
} }
func (element *Container) childFocusRequestCallback ( func (element *Container) childFocusRequestCallback (
child tomo.Focusable, child elements.Focusable,
) ( ) (
granted bool, granted bool,
) { ) {
if element.onFocusRequest != nil && element.onFocusRequest() { if element.onFocusRequest != nil && element.onFocusRequest() {
element.forFocused (func (child tomo.Focusable) bool { element.focused = true
element.forFocused (func (child elements.Focusable) bool {
child.HandleUnfocus() child.HandleUnfocus()
return true return true
}) })
child.HandleFocus(tomo.KeynavDirectionNeutral)
return true return true
} else { } else {
return false return false
@@ -463,13 +517,20 @@ func (element *Container) childFocusRequestCallback (
} }
func (element *Container) updateMinimumSize () { func (element *Container) updateMinimumSize () {
width, height := element.layout.MinimumSize(element.children) 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 { if element.flexible {
height = element.layout.FlexibleHeightFor(element.children, width) height = element.layout.FlexibleHeightFor (
element.children,
margin.X, width)
} }
element.core.SetMinimumSize(width, height) element.core.SetMinimumSize(width, height)
} }
func (element *Container) recalculate () { func (element *Container) doLayout () {
element.layout.Arrange(element.children, element.Bounds()) margin := element.theme.Margin(theme.PatternBackground)
// TODO: have layouts take in x and y margins
element.layout.Arrange (
element.children, margin.X, element.Bounds())
} }
+3
View File
@@ -0,0 +1,3 @@
// Package basicElements provides standard elements that are commonly used in
// GUI applications.
package basicElements
+25
View File
@@ -0,0 +1,25 @@
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())
}
+61 -16
View File
@@ -1,11 +1,10 @@
package basic package basicElements
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var labelCase = theme.C("basic", "label")
// Label is a simple text box. // Label is a simple text box.
type Label struct { type Label struct {
*core.Core *core.Core
@@ -13,7 +12,10 @@ type Label struct {
wrap bool wrap bool
text string text string
drawer artist.TextDrawer drawer textdraw.Drawer
config config.Wrapped
theme theme.Wrapped
onFlexibleHeightChange func () onFlexibleHeightChange func ()
} }
@@ -22,14 +24,28 @@ type Label struct {
// wrapped. // wrapped.
func NewLabel (text string, wrap bool) (element *Label) { func NewLabel (text string, wrap bool) (element *Label) {
element = &Label { } element = &Label { }
element.theme.Case = theme.C("basic", "label")
element.Core, element.core = core.NewCore(element.handleResize) element.Core, element.core = core.NewCore(element.handleResize)
face := theme.FontFaceRegular()
element.drawer.SetFace(face)
element.SetWrap(wrap) element.SetWrap(wrap)
element.SetText(text) element.SetText(text)
return 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 () { func (element *Label) handleResize () {
bounds := element.Bounds() bounds := element.Bounds()
if element.wrap { if element.wrap {
@@ -90,10 +106,39 @@ func (element *Label) SetWrap (wrap bool) {
} }
} }
// 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 () { func (element *Label) updateMinimumSize () {
if element.wrap { if element.wrap {
em := element.drawer.Em().Round() em := element.drawer.Em().Round()
if em < 1 { em = theme.Padding() } if em < 1 {
em = element.theme.Padding(theme.PatternBackground)[0]
}
element.core.SetMinimumSize ( element.core.SetMinimumSize (
em, element.drawer.LineHeight().Round()) em, element.drawer.LineHeight().Round())
if element.onFlexibleHeightChange != nil { if element.onFlexibleHeightChange != nil {
@@ -108,15 +153,15 @@ func (element *Label) updateMinimumSize () {
func (element *Label) draw () { func (element *Label) draw () {
bounds := element.Bounds() bounds := element.Bounds()
pattern, _ := theme.BackgroundPattern(theme.PatternState { pattern := element.theme.Pattern (
Case: labelCase, theme.PatternBackground,
}) theme.State { })
artist.FillRectangle(element, pattern, bounds) pattern.Draw(element.core, bounds)
textBounds := element.drawer.LayoutBounds() textBounds := element.drawer.LayoutBounds()
foreground, _ := theme.ForegroundPattern (theme.PatternState { foreground := element.theme.Color (
Case: labelCase, theme.ColorForeground,
}) theme.State { })
element.drawer.Draw (element, foreground, bounds.Min.Sub(textBounds.Min)) element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min))
} }
+51
View File
@@ -0,0 +1,51 @@
package basicElements
// Numeric is a type constraint representing a number.
type Numeric interface {
~float32 | ~float64 |
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
// 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
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
}
element = &LerpSlider[T] {
Slider: NewSlider(0, vertical),
min: min,
max: max,
}
element.SetValue(value)
return
}
// SetValue sets the slider's value.
func (element *LerpSlider[T]) SetValue (value T) {
value -= element.min
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())) +
element.min
}
// Range returns the difference between the slider's maximum and minimum values.
func (element *LerpSlider[T]) Range () T {
return element.max - element.min
}
+113 -44
View File
@@ -1,14 +1,14 @@
package basic package basicElements
import "fmt" import "fmt"
import "image" 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/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/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var listCase = theme.C("basic", "list")
// List is an element that contains several objects that a user can select. // List is an element that contains several objects that a user can select.
type List struct { type List struct {
*core.Core *core.Core
@@ -26,6 +26,9 @@ type List struct {
scroll int scroll int
entries []ListEntry entries []ListEntry
config config.Wrapped
theme theme.Wrapped
onScrollBoundsChange func () onScrollBoundsChange func ()
onNoEntrySelected func () onNoEntrySelected func ()
} }
@@ -33,6 +36,7 @@ type List struct {
// NewList creates a new list element with the specified entries. // NewList creates a new list element with the specified entries.
func NewList (entries ...ListEntry) (element *List) { func NewList (entries ...ListEntry) (element *List) {
element = &List { selectedEntry: -1 } element = &List { selectedEntry: -1 }
element.theme.Case = theme.C("basic", "list")
element.Core, element.core = core.NewCore(element.handleResize) element.Core, element.core = core.NewCore(element.handleResize)
element.FocusableCore, element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () { element.focusableControl = core.NewFocusableCore (func () {
@@ -56,27 +60,81 @@ func (element *List) handleResize () {
element.entries[index] = element.resizeEntryToFit(entry) element.entries[index] = element.resizeEntryToFit(entry)
} }
if element.scroll > element.maxScrollHeight() {
element.scroll = element.maxScrollHeight()
}
element.draw() element.draw()
if element.onScrollBoundsChange != nil { if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange() 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 // 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. // 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 // 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 // via scrolling. If an entry's width goes beyond the forced size, its text will
// be truncated so that it fits. // be truncated so that it fits.
func (element *List) Collapse (width, height int) { func (element *List) Collapse (width, height int) {
if
element.forcedMinimumWidth == width &&
element.forcedMinimumHeight == height {
return
}
element.forcedMinimumWidth = width element.forcedMinimumWidth = width
element.forcedMinimumHeight = height element.forcedMinimumHeight = height
element.updateMinimumSize() element.updateMinimumSize()
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
element.redo()
} }
func (element *List) HandleMouseDown (x, y int, button tomo.Button) { func (element *List) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return } if !element.Enabled() { return }
if !element.Focused() { element.Focus() } if !element.Focused() { element.Focus() }
if button != tomo.ButtonLeft { return } if button != input.ButtonLeft { return }
element.pressed = true element.pressed = true
if element.selectUnderMouse(x, y) && element.core.HasImage() { if element.selectUnderMouse(x, y) && element.core.HasImage() {
element.draw() element.draw()
@@ -84,8 +142,8 @@ func (element *List) HandleMouseDown (x, y int, button tomo.Button) {
} }
} }
func (element *List) HandleMouseUp (x, y int, button tomo.Button) { func (element *List) HandleMouseUp (x, y int, button input.Button) {
if button != tomo.ButtonLeft { return } if button != input.ButtonLeft { return }
element.pressed = false element.pressed = false
} }
@@ -100,18 +158,18 @@ func (element *List) HandleMouseMove (x, y int) {
func (element *List) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *List) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *List) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) { func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return } if !element.Enabled() { return }
altered := false altered := false
switch key { switch key {
case tomo.KeyLeft, tomo.KeyUp: case input.KeyLeft, input.KeyUp:
altered = element.changeSelectionBy(-1) altered = element.changeSelectionBy(-1)
case tomo.KeyRight, tomo.KeyDown: case input.KeyRight, input.KeyDown:
altered = element.changeSelectionBy(1) altered = element.changeSelectionBy(1)
case tomo.KeyEscape: case input.KeyEscape:
altered = element.selectEntry(-1) altered = element.selectEntry(-1)
} }
@@ -121,7 +179,7 @@ func (element *List) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
} }
} }
func (element *List) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { } func (element *List) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
// ScrollContentBounds returns the full content size of the element. // ScrollContentBounds returns the full content size of the element.
func (element *List) ScrollContentBounds () (bounds image.Rectangle) { func (element *List) ScrollContentBounds () (bounds image.Rectangle) {
@@ -163,10 +221,8 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) {
} }
func (element *List) scrollViewportHeight () (height int) { func (element *List) scrollViewportHeight () (height int) {
_, inset := theme.ListPattern(theme.PatternState { padding := element.theme.Padding(theme.PatternSunken)
Case: listCase, return element.Bounds().Dy() - padding[0] - padding[2]
})
return element.Bounds().Dy() - inset[0] - inset[2]
} }
func (element *List) maxScrollHeight () (height int) { func (element *List) maxScrollHeight () (height int) {
@@ -196,7 +252,9 @@ func (element *List) CountEntries () (count int) {
// Append adds an entry to the end of the list. // Append adds an entry to the end of the list.
func (element *List) Append (entry ListEntry) { func (element *List) Append (entry ListEntry) {
// append // append
entry.Collapse(element.forcedMinimumWidth) entry = element.resizeEntryToFit(entry)
entry.SetTheme(element.theme.Theme)
entry.SetConfig(element.config)
element.entries = append(element.entries, entry) element.entries = append(element.entries, entry)
// recalculate, redraw, notify // recalculate, redraw, notify
@@ -229,7 +287,7 @@ func (element *List) Insert (index int, entry ListEntry) {
element.entries = append ( element.entries = append (
element.entries[:index + 1], element.entries[:index + 1],
element.entries[index:]...) element.entries[index:]...)
entry.Collapse(element.forcedMinimumWidth) entry = element.resizeEntryToFit(entry)
element.entries[index] = entry element.entries[index] = entry
// recalculate, redraw, notify // recalculate, redraw, notify
@@ -274,7 +332,7 @@ func (element *List) Replace (index int, entry ListEntry) {
} }
// replace // replace
entry.Collapse(element.forcedMinimumWidth) entry = element.resizeEntryToFit(entry)
element.entries[index] = entry element.entries[index] = entry
// redraw // redraw
@@ -288,9 +346,17 @@ func (element *List) Replace (index int, entry ListEntry) {
} }
} }
// 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) { func (element *List) selectUnderMouse (x, y int) (updated bool) {
_, inset := theme.ListPattern(theme.PatternState { }) padding := element.theme.Padding(theme.PatternSunken)
bounds := inset.Apply(element.Bounds()) bounds := padding.Apply(element.Bounds())
mousePoint := image.Pt(x, y) mousePoint := image.Pt(x, y)
dot := image.Pt ( dot := image.Pt (
bounds.Min.X, bounds.Min.X,
@@ -331,10 +397,9 @@ func (element *List) changeSelectionBy (delta int) (updated bool) {
} }
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
_, inset := theme.ListPattern(theme.PatternState { bounds := element.Bounds()
Case: listCase, padding := element.theme.Padding(theme.PatternSunken)
}) entry.Resize(padding.Apply(bounds).Dx())
entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1])
return entry return entry
} }
@@ -349,7 +414,7 @@ func (element *List) updateMinimumSize () {
if minimumWidth == 0 { if minimumWidth == 0 {
for _, entry := range element.entries { for _, entry := range element.entries {
entryWidth := entry.Bounds().Dx() entryWidth := entry.MinimumWidth()
if entryWidth > minimumWidth { if entryWidth > minimumWidth {
minimumWidth = entryWidth minimumWidth = entryWidth
} }
@@ -360,37 +425,41 @@ func (element *List) updateMinimumSize () {
minimumHeight = element.contentHeight minimumHeight = element.contentHeight
} }
_, inset := theme.ListPattern(theme.PatternState { padding := element.theme.Padding(theme.PatternSunken)
Case: listCase, minimumHeight += padding[0] + padding[2]
})
minimumHeight += inset[0] + inset[2]
element.core.SetMinimumSize(minimumWidth, minimumHeight) element.core.SetMinimumSize(minimumWidth, minimumHeight)
} }
func (element *List) draw () { func (element *List) draw () {
bounds := element.Bounds() bounds := element.Bounds()
padding := element.theme.Padding(theme.PatternSunken)
pattern, inset := theme.ListPattern(theme.PatternState { innerBounds := padding.Apply(bounds)
Case: listCase, state := theme.State {
Disabled: !element.Enabled(), Disabled: !element.Enabled(),
Focused: element.Focused(), Focused: element.Focused(),
})
artist.FillRectangle(element, pattern, bounds)
bounds = inset.Apply(bounds)
dot := image.Point {
bounds.Min.X,
bounds.Min.Y - element.scroll,
} }
innerCanvas := tomo.Cut(element, bounds)
dot := image.Point {
innerBounds.Min.X,
innerBounds.Min.Y - element.scroll,
}
innerCanvas := canvas.Cut(element.core, innerBounds)
for index, entry := range element.entries { for index, entry := range element.entries {
entryPosition := dot entryPosition := dot
dot.Y += entry.Bounds().Dy() dot.Y += entry.Bounds().Dy()
if dot.Y < bounds.Min.Y { continue } if dot.Y < innerBounds.Min.Y { continue }
if entryPosition.Y > bounds.Max.Y { break } if entryPosition.Y > innerBounds.Max.Y { break }
entry.Draw ( entry.Draw (
innerCanvas, entryPosition, innerCanvas, entryPosition,
element.Focused(), element.selectedEntry == index) 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)
} }
+49 -40
View File
@@ -1,19 +1,23 @@
package basic package basicElements
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme" 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/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
var listEntryCase = theme.C("basic", "listEntry")
// ListEntry is an item that can be added to a list. // ListEntry is an item that can be added to a list.
type ListEntry struct { type ListEntry struct {
drawer artist.TextDrawer drawer textdraw.Drawer
bounds image.Rectangle bounds image.Rectangle
textPoint image.Point
text string text string
forcedMinimumWidth int width int
minimumWidth int
config config.Wrapped
theme theme.Wrapped
onSelect func () onSelect func ()
} }
@@ -22,62 +26,58 @@ func NewListEntry (text string, onSelect func ()) (entry ListEntry) {
text: text, text: text,
onSelect: onSelect, onSelect: onSelect,
} }
entry.theme.Case = theme.C("basic", "listEntry")
entry.drawer.SetText([]rune(text)) entry.drawer.SetText([]rune(text))
entry.drawer.SetFace(theme.FontFaceRegular())
entry.updateBounds() entry.updateBounds()
return return
} }
func (entry *ListEntry) Collapse (width int) { func (entry *ListEntry) SetTheme (new theme.Theme) {
if entry.forcedMinimumWidth == width { return } if new == entry.theme.Theme { return }
entry.forcedMinimumWidth = width entry.theme.Theme = new
entry.drawer.SetFace (entry.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal))
entry.updateBounds() entry.updateBounds()
} }
func (entry *ListEntry) SetConfig (new config.Config) {
if new == entry.config.Config { return }
entry.config.Config = new
}
func (entry *ListEntry) updateBounds () { func (entry *ListEntry) updateBounds () {
entry.bounds = image.Rectangle { } padding := entry.theme.Padding(theme.PatternRaised)
entry.bounds.Max.Y = entry.drawer.LineHeight().Round() entry.bounds = padding.Inverse().Apply(entry.drawer.LayoutBounds())
if entry.forcedMinimumWidth > 0 { entry.bounds = entry.bounds.Sub(entry.bounds.Min)
entry.bounds.Max.X = entry.forcedMinimumWidth entry.minimumWidth = entry.bounds.Dx()
} else { entry.bounds.Max.X = entry.width
entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx()
}
_, inset := theme.ItemPattern(theme.PatternState {
})
entry.bounds.Max.Y += inset[0] + inset[2]
entry.textPoint =
image.Pt(inset[3], inset[0]).
Sub(entry.drawer.LayoutBounds().Min)
} }
func (entry *ListEntry) Draw ( func (entry *ListEntry) Draw (
destination tomo.Canvas, destination canvas.Canvas,
offset image.Point, offset image.Point,
focused bool, focused bool,
on bool, on bool,
) ( ) (
updatedRegion image.Rectangle, updatedRegion image.Rectangle,
) { ) {
pattern, _ := theme.ItemPattern(theme.PatternState { state := theme.State {
Case: listEntryCase,
Focused: focused, Focused: focused,
On: on, On: on,
}) }
artist.FillRectangle (
destination, pattern := entry.theme.Pattern(theme.PatternRaised, state)
pattern, padding := entry.theme.Padding(theme.PatternRaised)
entry.Bounds().Add(offset)) bounds := entry.Bounds().Add(offset)
foreground, _ := theme.ForegroundPattern (theme.PatternState { artist.DrawBounds(destination, pattern, bounds)
Case: listEntryCase,
Focused: focused, foreground := entry.theme.Color (theme.ColorForeground, state)
On: on,
})
return entry.drawer.Draw ( return entry.drawer.Draw (
destination, destination,
foreground, foreground,
offset.Add(entry.textPoint)) offset.Add(image.Pt(padding[artist.SideLeft], padding[artist.SideTop])).
Sub(entry.drawer.LayoutBounds().Min))
} }
func (entry *ListEntry) RunSelect () { func (entry *ListEntry) RunSelect () {
@@ -89,3 +89,12 @@ func (entry *ListEntry) RunSelect () {
func (entry *ListEntry) Bounds () (bounds image.Rectangle) { func (entry *ListEntry) Bounds () (bounds image.Rectangle) {
return entry.bounds return entry.bounds
} }
func (entry *ListEntry) Resize (width int) {
entry.width = width
entry.updateBounds()
}
func (entry *ListEntry) MinimumWidth () (width int) {
return entry.minimumWidth
}
+43 -7
View File
@@ -1,7 +1,8 @@
package basic package basicElements
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/theme" 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"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
@@ -10,14 +11,17 @@ type ProgressBar struct {
*core.Core *core.Core
core core.CoreControl core core.CoreControl
progress float64 progress float64
config config.Wrapped
theme theme.Wrapped
} }
// NewProgressBar creates a new progress bar displaying the given progress // NewProgressBar creates a new progress bar displaying the given progress
// level. // level.
func NewProgressBar (progress float64) (element *ProgressBar) { func NewProgressBar (progress float64) (element *ProgressBar) {
element = &ProgressBar { progress: progress } element = &ProgressBar { progress: progress }
element.theme.Case = theme.C("basic", "progressBar")
element.Core, element.core = core.NewCore(element.draw) element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(theme.Padding() * 2, theme.Padding() * 2)
return return
} }
@@ -31,16 +35,48 @@ func (element *ProgressBar) SetProgress (progress float64) {
} }
} }
// 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 () { func (element *ProgressBar) draw () {
bounds := element.Bounds() bounds := element.Bounds()
pattern, inset := theme.SunkenPattern(theme.PatternState { }) pattern := element.theme.Pattern(theme.PatternSunken, theme.State { })
artist.FillRectangle(element, pattern, bounds) padding := element.theme.Padding(theme.PatternSunken)
bounds = inset.Apply(bounds) pattern.Draw(element.core, bounds)
bounds = padding.Apply(bounds)
meterBounds := image.Rect ( meterBounds := image.Rect (
bounds.Min.X, bounds.Min.Y, bounds.Min.X, bounds.Min.Y,
bounds.Min.X + int(float64(bounds.Dx()) * element.progress), bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y) bounds.Max.Y)
accent, _ := theme.AccentPattern(theme.PatternState { }) mercury := element.theme.Pattern(theme.PatternMercury, theme.State { })
artist.FillRectangle(element, accent, meterBounds) artist.DrawBounds(element.core, mercury, meterBounds)
} }
+145 -90
View File
@@ -1,15 +1,14 @@
package basic package basicElements
import "image" 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/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/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var scrollContainerCase = theme.C("basic", "scrollContainer")
var scrollBarHorizontalCase = theme.C("basic", "scrollBarHorizontal")
var scrollBarVerticalCase = theme.C("basic", "scrollBarVertical")
// ScrollContainer is a container that is capable of holding a scrollable // ScrollContainer is a container that is capable of holding a scrollable
// element. // element.
type ScrollContainer struct { type ScrollContainer struct {
@@ -17,10 +16,11 @@ type ScrollContainer struct {
core core.CoreControl core core.CoreControl
focused bool focused bool
child tomo.Scrollable child elements.Scrollable
childWidth, childHeight int childWidth, childHeight int
horizontal struct { horizontal struct {
theme theme.Wrapped
exists bool exists bool
enabled bool enabled bool
dragging bool dragging bool
@@ -31,6 +31,7 @@ type ScrollContainer struct {
} }
vertical struct { vertical struct {
theme theme.Wrapped
exists bool exists bool
enabled bool enabled bool
dragging bool dragging bool
@@ -40,16 +41,22 @@ type ScrollContainer struct {
bar image.Rectangle bar image.Rectangle
} }
config config.Wrapped
theme theme.Wrapped
onFocusRequest func () (granted bool) onFocusRequest func () (granted bool)
onFocusMotionRequest func (tomo.KeynavDirection) (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool)
} }
// NewScrollContainer creates a new scroll container with the specified scroll // NewScrollContainer creates a new scroll container with the specified scroll
// bars. // bars.
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) { func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
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.Core, element.core = core.NewCore(element.handleResize)
element.updateMinimumSize()
element.horizontal.exists = horizontal element.horizontal.exists = horizontal
element.vertical.exists = vertical element.vertical.exists = vertical
return return
@@ -64,7 +71,7 @@ func (element *ScrollContainer) handleResize () {
// Adopt adds a scrollable element to the scroll container. The container can // 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 // only contain one scrollable element at a time, and when a new one is adopted
// it replaces the last one. // it replaces the last one.
func (element *ScrollContainer) Adopt (child tomo.Scrollable) { func (element *ScrollContainer) Adopt (child elements.Scrollable) {
// disown previous child if it exists // disown previous child if it exists
if element.child != nil { if element.child != nil {
element.clearChildEventHandlers(child) element.clearChildEventHandlers(child)
@@ -73,18 +80,22 @@ func (element *ScrollContainer) Adopt (child tomo.Scrollable) {
// adopt new child // adopt new child
element.child = child element.child = child
if child != nil { 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.OnDamage(element.childDamageCallback)
child.OnMinimumSizeChange(element.updateMinimumSize) child.OnMinimumSizeChange(element.updateMinimumSize)
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback) child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
if newChild, ok := child.(tomo.Focusable); ok { if newChild, ok := child.(elements.Focusable); ok {
newChild.OnFocusRequest ( newChild.OnFocusRequest (
element.childFocusRequestCallback) element.childFocusRequestCallback)
newChild.OnFocusMotionRequest ( newChild.OnFocusMotionRequest (
element.childFocusMotionRequestCallback) element.childFocusMotionRequestCallback)
} }
// TODO: somehow inform the core that we do not in fact want to
// redraw the element.
element.updateMinimumSize() element.updateMinimumSize()
element.horizontal.enabled, element.horizontal.enabled,
@@ -96,19 +107,48 @@ func (element *ScrollContainer) Adopt (child tomo.Scrollable) {
} }
} }
func (element *ScrollContainer) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) { // SetTheme sets the element's theme.
if child, ok := element.child.(tomo.KeyboardTarget); ok { 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) child.HandleKeyDown(key, modifiers)
} }
} }
func (element *ScrollContainer) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) { func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if child, ok := element.child.(tomo.KeyboardTarget); ok { if child, ok := element.child.(elements.KeyboardTarget); ok {
child.HandleKeyUp(key, modifiers) child.HandleKeyUp(key, modifiers)
} }
} }
func (element *ScrollContainer) HandleMouseDown (x, y int, button tomo.Button) { func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) {
velocity := element.config.ScrollVelocity()
point := image.Pt(x, y) point := image.Pt(x, y)
if point.In(element.horizontal.bar) { if point.In(element.horizontal.bar) {
element.horizontal.dragging = true element.horizontal.dragging = true
@@ -118,12 +158,26 @@ func (element *ScrollContainer) HandleMouseDown (x, y int, button tomo.Button) {
element.dragHorizontalBar(point) element.dragHorizontalBar(point)
} else if point.In(element.horizontal.gutter) { } else if point.In(element.horizontal.gutter) {
// FIXME: x backend and scroll container should pull these switch button {
// values from the same place 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 { if x > element.horizontal.bar.Min.X {
element.scrollChildBy(16, 0) element.scrollChildBy(viewport, 0)
} else { } else {
element.scrollChildBy(-16, 0) 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) { } else if point.In(element.vertical.bar) {
@@ -134,18 +188,34 @@ func (element *ScrollContainer) HandleMouseDown (x, y int, button tomo.Button) {
element.dragVerticalBar(point) element.dragVerticalBar(point)
} else if point.In(element.vertical.gutter) { } 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 { if y > element.vertical.bar.Min.Y {
element.scrollChildBy(0, 16) element.scrollChildBy(0, viewport)
} else { } else {
element.scrollChildBy(0, -16) 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.(tomo.MouseTarget); ok { } else if child, ok := element.child.(elements.MouseTarget); ok {
child.HandleMouseDown(x, y, button) child.HandleMouseDown(x, y, button)
} }
} }
func (element *ScrollContainer) HandleMouseUp (x, y int, button tomo.Button) { func (element *ScrollContainer) HandleMouseUp (x, y int, button input.Button) {
if element.horizontal.dragging { if element.horizontal.dragging {
element.horizontal.dragging = false element.horizontal.dragging = false
element.drawHorizontalBar() element.drawHorizontalBar()
@@ -156,7 +226,7 @@ func (element *ScrollContainer) HandleMouseUp (x, y int, button tomo.Button) {
element.drawVerticalBar() element.drawVerticalBar()
element.core.DamageRegion(element.vertical.bar) element.core.DamageRegion(element.vertical.bar)
} else if child, ok := element.child.(tomo.MouseTarget); ok { } else if child, ok := element.child.(elements.MouseTarget); ok {
child.HandleMouseUp(x, y, button) child.HandleMouseUp(x, y, button)
} }
} }
@@ -168,7 +238,7 @@ func (element *ScrollContainer) HandleMouseMove (x, y int) {
} else if element.vertical.dragging { } else if element.vertical.dragging {
element.dragVerticalBar(image.Pt(x, y)) element.dragVerticalBar(image.Pt(x, y))
} else if child, ok := element.child.(tomo.MouseTarget); ok { } else if child, ok := element.child.(elements.MouseTarget); ok {
child.HandleMouseMove(x, y) child.HandleMouseMove(x, y)
} }
} }
@@ -194,18 +264,20 @@ func (element *ScrollContainer) Focused () (focused bool) {
func (element *ScrollContainer) Focus () { func (element *ScrollContainer) Focus () {
if element.onFocusRequest != nil { if element.onFocusRequest != nil {
element.onFocusRequest() if element.onFocusRequest() {
element.focused = true
}
} }
} }
func (element *ScrollContainer) HandleFocus ( func (element *ScrollContainer) HandleFocus (
direction tomo.KeynavDirection, direction input.KeynavDirection,
) ( ) (
accepted bool, accepted bool,
) { ) {
if child, ok := element.child.(tomo.Focusable); ok { if child, ok := element.child.(elements.Focusable); ok {
element.focused = true element.focused = child.HandleFocus(direction)
return child.HandleFocus(direction) return element.focused
} else { } else {
element.focused = false element.focused = false
return false return false
@@ -213,7 +285,7 @@ func (element *ScrollContainer) HandleFocus (
} }
func (element *ScrollContainer) HandleUnfocus () { func (element *ScrollContainer) HandleUnfocus () {
if child, ok := element.child.(tomo.Focusable); ok { if child, ok := element.child.(elements.Focusable); ok {
child.HandleUnfocus() child.HandleUnfocus()
} }
element.focused = false element.focused = false
@@ -224,28 +296,26 @@ func (element *ScrollContainer) OnFocusRequest (callback func () (granted bool))
} }
func (element *ScrollContainer) OnFocusMotionRequest ( func (element *ScrollContainer) OnFocusMotionRequest (
callback func (direction tomo.KeynavDirection) (granted bool), callback func (direction input.KeynavDirection) (granted bool),
) { ) {
element.onFocusMotionRequest = callback element.onFocusMotionRequest = callback
} }
func (element *ScrollContainer) childDamageCallback (region tomo.Canvas) { func (element *ScrollContainer) childDamageCallback (region canvas.Canvas) {
element.core.DamageRegion(artist.Paste(element, region, image.Point { })) element.core.DamageRegion(region.Bounds())
} }
func (element *ScrollContainer) childFocusRequestCallback () (granted bool) { func (element *ScrollContainer) childFocusRequestCallback () (granted bool) {
child, ok := element.child.(tomo.Focusable) if element.onFocusRequest != nil {
if !ok { return false } element.focused = element.onFocusRequest()
if element.onFocusRequest != nil && element.onFocusRequest() { return element.focused
child.HandleFocus(tomo.KeynavDirectionNeutral)
return true
} else { } else {
return false return false
} }
} }
func (element *ScrollContainer) childFocusMotionRequestCallback ( func (element *ScrollContainer) childFocusMotionRequestCallback (
direction tomo.KeynavDirection, direction input.KeynavDirection,
) ( ) (
granted bool, granted bool,
) { ) {
@@ -253,19 +323,19 @@ func (element *ScrollContainer) childFocusMotionRequestCallback (
return element.onFocusMotionRequest(direction) return element.onFocusMotionRequest(direction)
} }
func (element *ScrollContainer) clearChildEventHandlers (child tomo.Scrollable) { func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollable) {
child.DrawTo(nil) child.DrawTo(nil)
child.OnDamage(nil) child.OnDamage(nil)
child.OnMinimumSizeChange(nil) child.OnMinimumSizeChange(nil)
child.OnScrollBoundsChange(nil) child.OnScrollBoundsChange(nil)
if child0, ok := child.(tomo.Focusable); ok { if child0, ok := child.(elements.Focusable); ok {
child0.OnFocusRequest(nil) child0.OnFocusRequest(nil)
child0.OnFocusMotionRequest(nil) child0.OnFocusMotionRequest(nil)
if child0.Focused() { if child0.Focused() {
child0.HandleUnfocus() child0.HandleUnfocus()
} }
} }
if child0, ok := child.(tomo.Flexible); ok { if child0, ok := child.(elements.Flexible); ok {
child0.OnFlexibleHeightChange(nil) child0.OnFlexibleHeightChange(nil)
} }
} }
@@ -275,26 +345,23 @@ func (element *ScrollContainer) resizeChildToFit () {
0, 0, 0, 0,
element.childWidth, element.childWidth,
element.childHeight).Add(element.Bounds().Min) element.childHeight).Add(element.Bounds().Min)
element.child.DrawTo(tomo.Cut(element, childBounds)) element.child.DrawTo(canvas.Cut(element.core, childBounds))
} }
func (element *ScrollContainer) recalculate () { func (element *ScrollContainer) recalculate () {
_, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState {
Case: scrollBarHorizontalCase,
})
_, gutterInsetVertical := theme.GutterPattern(theme.PatternState {
Case: scrollBarHorizontalCase,
})
horizontal := &element.horizontal horizontal := &element.horizontal
vertical := &element.vertical vertical := &element.vertical
gutterInsetHorizontal := horizontal.theme.Padding(theme.PatternGutter)
gutterInsetVertical := vertical.theme.Padding(theme.PatternGutter)
bounds := element.Bounds() bounds := element.Bounds()
thicknessHorizontal := thicknessHorizontal :=
theme.HandleWidth() + element.config.HandleWidth() +
gutterInsetHorizontal[3] + gutterInsetHorizontal[3] +
gutterInsetHorizontal[1] gutterInsetHorizontal[1]
thicknessVertical := thicknessVertical :=
theme.HandleWidth() + element.config.HandleWidth() +
gutterInsetVertical[3] + gutterInsetVertical[3] +
gutterInsetVertical[1] gutterInsetVertical[1]
@@ -370,12 +437,10 @@ func (element *ScrollContainer) recalculate () {
} }
func (element *ScrollContainer) draw () { func (element *ScrollContainer) draw () {
artist.Paste(element, element.child, image.Point { }) deadPattern := element.theme.Pattern (
deadPattern, _ := theme.DeadPattern(theme.PatternState { theme.PatternDead, theme.State { })
Case: scrollContainerCase, artist.DrawBounds (
}) element.core, deadPattern,
artist.FillRectangle (
element, deadPattern,
image.Rect ( image.Rect (
element.vertical.gutter.Min.X, element.vertical.gutter.Min.X,
element.horizontal.gutter.Min.Y, element.horizontal.gutter.Min.Y,
@@ -386,33 +451,27 @@ func (element *ScrollContainer) draw () {
} }
func (element *ScrollContainer) drawHorizontalBar () { func (element *ScrollContainer) drawHorizontalBar () {
gutterPattern, _ := theme.GutterPattern (theme.PatternState { state := theme.State {
Case: scrollBarHorizontalCase,
Disabled: !element.horizontal.enabled,
})
artist.FillRectangle(element, gutterPattern, element.horizontal.gutter)
handlePattern, _ := theme.HandlePattern (theme.PatternState {
Case: scrollBarHorizontalCase,
Disabled: !element.horizontal.enabled, Disabled: !element.horizontal.enabled,
Pressed: element.horizontal.dragging, Pressed: element.horizontal.dragging,
}) }
artist.FillRectangle(element, handlePattern, element.horizontal.bar) 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 () { func (element *ScrollContainer) drawVerticalBar () {
gutterPattern, _ := theme.GutterPattern (theme.PatternState { state := theme.State {
Case: scrollBarVerticalCase,
Disabled: !element.vertical.enabled,
})
artist.FillRectangle(element, gutterPattern, element.vertical.gutter)
handlePattern, _ := theme.HandlePattern (theme.PatternState {
Case: scrollBarVerticalCase,
Disabled: !element.vertical.enabled, Disabled: !element.vertical.enabled,
Pressed: element.vertical.dragging, Pressed: element.vertical.dragging,
}) }
artist.FillRectangle(element, handlePattern, element.vertical.bar) 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) { func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) {
@@ -434,19 +493,15 @@ func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) {
} }
func (element *ScrollContainer) updateMinimumSize () { func (element *ScrollContainer) updateMinimumSize () {
_, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState { gutterInsetHorizontal := element.horizontal.theme.Padding(theme.PatternGutter)
Case: scrollBarHorizontalCase, gutterInsetVertical := element.vertical.theme.Padding(theme.PatternGutter)
})
_, gutterInsetVertical := theme.GutterPattern(theme.PatternState {
Case: scrollBarHorizontalCase,
})
thicknessHorizontal := thicknessHorizontal :=
theme.HandleWidth() + element.config.HandleWidth() +
gutterInsetHorizontal[3] + gutterInsetHorizontal[3] +
gutterInsetHorizontal[1] gutterInsetHorizontal[1]
thicknessVertical := thicknessVertical :=
theme.HandleWidth() + element.config.HandleWidth() +
gutterInsetVertical[3] + gutterInsetVertical[3] +
gutterInsetVertical[1] gutterInsetVertical[1]
+206
View File
@@ -0,0 +1,206 @@
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)
}
+48 -15
View File
@@ -1,16 +1,17 @@
package basic package basicElements
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var spacerCase = theme.C("basic", "spacer")
// Spacer can be used to put space between two elements.. // Spacer can be used to put space between two elements..
type Spacer struct { type Spacer struct {
*core.Core *core.Core
core core.CoreControl core core.CoreControl
line bool line bool
config config.Wrapped
theme theme.Wrapped
} }
// NewSpacer creates a new spacer. If line is set to true, the spacer will be // NewSpacer creates a new spacer. If line is set to true, the spacer will be
@@ -18,8 +19,9 @@ type Spacer struct {
// will appear as a line. // will appear as a line.
func NewSpacer (line bool) (element *Spacer) { func NewSpacer (line bool) (element *Spacer) {
element = &Spacer { line: line } element = &Spacer { line: line }
element.theme.Case = theme.C("basic", "spacer")
element.Core, element.core = core.NewCore(element.draw) element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(1, 1) element.updateMinimumSize()
return return
} }
@@ -27,26 +29,57 @@ func NewSpacer (line bool) (element *Spacer) {
func (element *Spacer) SetLine (line bool) { func (element *Spacer) SetLine (line bool) {
if element.line == line { return } if element.line == line { return }
element.line = line element.line = line
element.updateMinimumSize()
if element.core.HasImage() { if element.core.HasImage() {
element.draw() element.draw()
element.core.DamageAll() 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 () { func (element *Spacer) draw () {
bounds := element.Bounds() bounds := element.Bounds()
if element.line { if element.line {
pattern, _ := theme.ForegroundPattern(theme.PatternState { pattern := element.theme.Pattern (
Case: spacerCase, theme.PatternLine,
Disabled: true, theme.State { })
}) pattern.Draw(element.core, bounds)
artist.FillRectangle(element, pattern, bounds)
} else { } else {
pattern, _ := theme.BackgroundPattern(theme.PatternState { pattern := element.theme.Pattern (
Case: spacerCase, theme.PatternBackground,
Disabled: true, theme.State { })
}) pattern.Draw(element.core, bounds)
artist.FillRectangle(element, pattern, bounds)
} }
} }
+72 -60
View File
@@ -1,13 +1,13 @@
package basic package basicElements
import "image" 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/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var switchCase = theme.C("basic", "switch")
// Switch is a toggle-able on/off switch with an optional label. It is // Switch is a toggle-able on/off switch with an optional label. It is
// functionally identical to Checkbox, but plays a different semantic role. // functionally identical to Checkbox, but plays a different semantic role.
type Switch struct { type Switch struct {
@@ -15,44 +15,42 @@ type Switch struct {
*core.FocusableCore *core.FocusableCore
core core.CoreControl core core.CoreControl
focusableControl core.FocusableCoreControl focusableControl core.FocusableCoreControl
drawer artist.TextDrawer drawer textdraw.Drawer
pressed bool pressed bool
checked bool checked bool
text string text string
config config.Wrapped
theme theme.Wrapped
onToggle func () onToggle func ()
} }
// NewSwitch creates a new switch with the specified label text. // NewSwitch creates a new switch with the specified label text.
func NewSwitch (text string, on bool) (element *Switch) { func NewSwitch (text string, on bool) (element *Switch) {
element = &Switch { checked: on, text: text } element = &Switch {
checked: on,
text: text,
}
element.theme.Case = theme.C("basic", "switch")
element.Core, element.core = core.NewCore(element.draw) element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore, element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () { element.focusableControl = core.NewFocusableCore(element.redo)
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.drawer.SetFace(theme.FontFaceRegular())
element.drawer.SetText([]rune(text)) element.drawer.SetText([]rune(text))
element.calculateMinimumSize() element.updateMinimumSize()
return return
} }
func (element *Switch) HandleMouseDown (x, y int, button tomo.Button) { func (element *Switch) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return } if !element.Enabled() { return }
element.Focus() element.Focus()
element.pressed = true element.pressed = true
if element.core.HasImage() { element.redo()
element.draw()
element.core.DamageAll()
}
} }
func (element *Switch) HandleMouseUp (x, y int, button tomo.Button) { func (element *Switch) HandleMouseUp (x, y int, button input.Button) {
if button != tomo.ButtonLeft || !element.pressed { return } if button != input.ButtonLeft || !element.pressed { return }
element.pressed = false element.pressed = false
within := image.Point { x, y }. within := image.Point { x, y }.
@@ -73,24 +71,18 @@ func (element *Switch) HandleMouseUp (x, y int, button tomo.Button) {
func (element *Switch) HandleMouseMove (x, y int) { } func (element *Switch) HandleMouseMove (x, y int) { }
func (element *Switch) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *Switch) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Switch) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) { func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == tomo.KeyEnter { if key == input.KeyEnter {
element.pressed = true element.pressed = true
if element.core.HasImage() { element.redo()
element.draw()
element.core.DamageAll()
}
} }
} }
func (element *Switch) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) { func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == tomo.KeyEnter && element.pressed { if key == input.KeyEnter && element.pressed {
element.pressed = false element.pressed = false
element.checked = !element.checked element.checked = !element.checked
if element.core.HasImage() { element.redo()
element.draw()
element.core.DamageAll()
}
if element.onToggle != nil { if element.onToggle != nil {
element.onToggle() element.onToggle()
} }
@@ -118,15 +110,37 @@ func (element *Switch) SetText (text string) {
element.text = text element.text = text
element.drawer.SetText([]rune(text)) element.drawer.SetText([]rune(text))
element.calculateMinimumSize() element.updateMinimumSize()
element.redo()
}
// 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))
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 () { if element.core.HasImage () {
element.draw() element.draw()
element.core.DamageAll() element.core.DamageAll()
} }
} }
func (element *Switch) calculateMinimumSize () { func (element *Switch) updateMinimumSize () {
textBounds := element.drawer.LayoutBounds() textBounds := element.drawer.LayoutBounds()
lineHeight := element.drawer.LineHeight().Round() lineHeight := element.drawer.LineHeight().Round()
@@ -134,7 +148,9 @@ func (element *Switch) calculateMinimumSize () {
element.core.SetMinimumSize(lineHeight * 2, lineHeight) element.core.SetMinimumSize(lineHeight * 2, lineHeight)
} else { } else {
element.core.SetMinimumSize ( element.core.SetMinimumSize (
lineHeight * 2 + theme.Padding() + textBounds.Dx(), lineHeight * 2 +
element.theme.Margin(theme.PatternBackground).X +
textBounds.Dx(),
lineHeight) lineHeight)
} }
} }
@@ -143,10 +159,15 @@ func (element *Switch) draw () {
bounds := element.Bounds() bounds := element.Bounds()
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min) gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState {
Case: switchCase, state := theme.State {
}) Disabled: !element.Enabled(),
artist.FillRectangle (element, backgroundPattern, bounds) Focused: element.Focused(),
Pressed: element.pressed,
}
backgroundPattern := element.theme.Pattern (
theme.PatternBackground, state)
backgroundPattern.Draw(element.core, bounds)
if element.checked { if element.checked {
handleBounds.Min.X += bounds.Dy() handleBounds.Min.X += bounds.Dy()
@@ -162,33 +183,24 @@ func (element *Switch) draw () {
} }
} }
gutterPattern, _ := theme.GutterPattern(theme.PatternState { gutterPattern := element.theme.Pattern (
Case: switchCase, theme.PatternGutter, state)
Disabled: !element.Enabled(), artist.DrawBounds(element.core, gutterPattern, gutterBounds)
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element, gutterPattern, gutterBounds)
handlePattern, _ := theme.HandlePattern(theme.PatternState { handlePattern := element.theme.Pattern (
Case: switchCase, theme.PatternHandle, state)
Disabled: !element.Enabled(), artist.DrawBounds(element.core, handlePattern, handleBounds)
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element, handlePattern, handleBounds)
textBounds := element.drawer.LayoutBounds() textBounds := element.drawer.LayoutBounds()
offset := bounds.Min.Add(image.Point { offset := bounds.Min.Add(image.Point {
X: bounds.Dy() * 2 + theme.Padding(), X: bounds.Dy() * 2 +
element.theme.Margin(theme.PatternBackground).X,
}) })
offset.Y -= textBounds.Min.Y offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X offset.X -= textBounds.Min.X
foreground, _ := theme.ForegroundPattern (theme.PatternState { foreground := element.theme.Color (
Case: switchCase, theme.ColorForeground, state)
Disabled: !element.Enabled(), element.drawer.Draw(element.core, foreground, offset)
})
element.drawer.Draw(element, foreground, offset)
} }
+181 -97
View File
@@ -1,14 +1,17 @@
package basic package basicElements
import "image" 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/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/textmanip" 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" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var textBoxCase = theme.C("basic", "textBox")
// TextBox is a single-line text input. // TextBox is a single-line text input.
type TextBox struct { type TextBox struct {
*core.Core *core.Core
@@ -16,15 +19,19 @@ type TextBox struct {
core core.CoreControl core core.CoreControl
focusableControl core.FocusableCoreControl focusableControl core.FocusableCoreControl
cursor int dragging bool
dot textmanip.Dot
scroll int scroll int
placeholder string placeholder string
text []rune text []rune
placeholderDrawer artist.TextDrawer placeholderDrawer textdraw.Drawer
valueDrawer artist.TextDrawer valueDrawer textdraw.Drawer
onKeyDown func (key tomo.Key, modifiers tomo.Modifiers) (handled bool) config config.Wrapped
theme theme.Wrapped
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
onChange func () onChange func ()
onScrollBoundsChange func () onScrollBoundsChange func ()
} }
@@ -34,6 +41,7 @@ type TextBox struct {
// text. // text.
func NewTextBox (placeholder, value string) (element *TextBox) { func NewTextBox (placeholder, value string) (element *TextBox) {
element = &TextBox { } element = &TextBox { }
element.theme.Case = theme.C("basic", "textBox")
element.Core, element.core = core.NewCore(element.handleResize) element.Core, element.core = core.NewCore(element.handleResize)
element.FocusableCore, element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () { element.focusableControl = core.NewFocusableCore (func () {
@@ -42,8 +50,6 @@ func NewTextBox (placeholder, value string) (element *TextBox) {
element.core.DamageAll() element.core.DamageAll()
} }
}) })
element.placeholderDrawer.SetFace(theme.FontFaceRegular())
element.valueDrawer.SetFace(theme.FontFaceRegular())
element.placeholder = placeholder element.placeholder = placeholder
element.placeholderDrawer.SetText([]rune(placeholder)) element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize() element.updateMinimumSize()
@@ -59,56 +65,108 @@ func (element *TextBox) handleResize () {
} }
} }
func (element *TextBox) HandleMouseDown (x, y int, button tomo.Button) { func (element *TextBox) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return } if !element.Enabled() { return }
if !element.Focused() { element.Focus() } 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) HandleMouseUp (x, y int, button tomo.Button) { }
func (element *TextBox) HandleMouseMove (x, y int) { }
func (element *TextBox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *TextBox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *TextBox) HandleKeyDown(key tomo.Key, modifiers tomo.Modifiers) { func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) {
if element.onKeyDown != nil && element.onKeyDown(key, modifiers) { if element.onKeyDown != nil && element.onKeyDown(key, modifiers) {
return return
} }
// TODO: text selection with shift
scrollMemory := element.scroll scrollMemory := element.scroll
altered := true altered := true
textChanged := false textChanged := false
switch { switch {
case key == tomo.KeyBackspace: case key == input.KeyBackspace:
if len(element.text) < 1 { break } if len(element.text) < 1 { break }
element.text, element.cursor = textmanip.Backspace ( element.text, element.dot = textmanip.Backspace (
element.text, element.text,
element.cursor, element.dot,
modifiers.Control) modifiers.Control)
textChanged = true textChanged = true
case key == tomo.KeyDelete: case key == input.KeyDelete:
if len(element.text) < 1 { break } if len(element.text) < 1 { break }
element.text, element.cursor = textmanip.Delete ( element.text, element.dot = textmanip.Delete (
element.text, element.text,
element.cursor, element.dot,
modifiers.Control) modifiers.Control)
textChanged = true textChanged = true
case key == tomo.KeyLeft: case key == input.KeyLeft:
element.cursor = textmanip.MoveLeft ( if modifiers.Shift {
element.dot = textmanip.SelectLeft (
element.text, element.text,
element.cursor, element.dot,
modifiers.Control) modifiers.Control)
} else {
element.dot = textmanip.MoveLeft (
element.text,
element.dot,
modifiers.Control)
}
case key == tomo.KeyRight: case key == input.KeyRight:
element.cursor = textmanip.MoveRight ( if modifiers.Shift {
element.dot = textmanip.SelectRight (
element.text, element.text,
element.cursor, element.dot,
modifiers.Control) modifiers.Control)
} else {
element.dot = textmanip.MoveRight (
element.text,
element.dot,
modifiers.Control)
}
case key.Printable(): case key.Printable():
element.text, element.cursor = textmanip.Type ( element.text, element.dot = textmanip.Type (
element.text, element.text,
element.cursor, element.dot,
rune(key)) rune(key))
textChanged = true textChanged = true
@@ -130,13 +188,12 @@ func (element *TextBox) HandleKeyDown(key tomo.Key, modifiers tomo.Modifiers) {
element.onScrollBoundsChange() element.onScrollBoundsChange()
} }
if altered && element.core.HasImage () { if altered {
element.draw() element.redo()
element.core.DamageAll()
} }
} }
func (element *TextBox) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { } func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
func (element *TextBox) SetPlaceholder (placeholder string) { func (element *TextBox) SetPlaceholder (placeholder string) {
if element.placeholder == placeholder { return } if element.placeholder == placeholder { return }
@@ -145,10 +202,7 @@ func (element *TextBox) SetPlaceholder (placeholder string) {
element.placeholderDrawer.SetText([]rune(placeholder)) element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize() element.updateMinimumSize()
if element.core.HasImage () { element.redo()
element.draw()
element.core.DamageAll()
}
} }
func (element *TextBox) SetValue (text string) { func (element *TextBox) SetValue (text string) {
@@ -157,15 +211,11 @@ func (element *TextBox) SetValue (text string) {
element.text = []rune(text) element.text = []rune(text)
element.runOnChange() element.runOnChange()
element.valueDrawer.SetText(element.text) element.valueDrawer.SetText(element.text)
if element.cursor > element.valueDrawer.Length() { if element.dot.End > element.valueDrawer.Length() {
element.cursor = element.valueDrawer.Length() element.dot = textmanip.EmptyDot(element.valueDrawer.Length())
} }
element.scrollToCursor() element.scrollToCursor()
element.redo()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
} }
func (element *TextBox) Value () (value string) { func (element *TextBox) Value () (value string) {
@@ -177,7 +227,7 @@ func (element *TextBox) Filled () (filled bool) {
} }
func (element *TextBox) OnKeyDown ( func (element *TextBox) OnKeyDown (
callback func (key tomo.Key, modifiers tomo.Modifiers) (handled bool), callback func (key input.Key, modifiers input.Modifiers) (handled bool),
) { ) {
element.onKeyDown = callback element.onKeyDown = callback
} }
@@ -203,7 +253,8 @@ func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
} }
func (element *TextBox) scrollViewportWidth () (width int) { func (element *TextBox) scrollViewportWidth () (width int) {
return element.Bounds().Inset(theme.Padding()).Dx() padding := element.theme.Padding(theme.PatternInput)
return padding.Apply(element.Bounds()).Dx()
} }
// ScrollTo scrolls the viewport to the specified point relative to // ScrollTo scrolls the viewport to the specified point relative to
@@ -218,10 +269,7 @@ func (element *TextBox) ScrollTo (position image.Point) {
maxPosition := contentBounds.Max.X - element.scrollViewportWidth() maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
if element.scroll > maxPosition { element.scroll = maxPosition } if element.scroll > maxPosition { element.scroll = maxPosition }
if element.core.HasImage () { element.redo()
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil { if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange() element.onScrollBoundsChange()
} }
@@ -236,18 +284,6 @@ func (element *TextBox) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback element.onScrollBoundsChange = callback
} }
func (element *TextBox) updateMinimumSize () {
textBounds := element.placeholderDrawer.LayoutBounds()
_, inset := theme.InputPattern(theme.PatternState {
Case: textBoxCase,
})
element.core.SetMinimumSize (
textBounds.Dx() +
theme.Padding() * 2 + inset[3] + inset[1],
element.placeholderDrawer.LineHeight().Round() +
theme.Padding() * 2 + inset[0] + inset[2])
}
func (element *TextBox) runOnChange () { func (element *TextBox) runOnChange () {
if element.onChange != nil { if element.onChange != nil {
element.onChange() element.onChange()
@@ -257,10 +293,12 @@ func (element *TextBox) runOnChange () {
func (element *TextBox) scrollToCursor () { func (element *TextBox) scrollToCursor () {
if !element.core.HasImage() { return } if !element.core.HasImage() { return }
bounds := element.Bounds().Inset(theme.Padding()) padding := element.theme.Padding(theme.PatternInput)
bounds := padding.Apply(element.Bounds())
bounds = bounds.Sub(bounds.Min) bounds = bounds.Sub(bounds.Min)
bounds.Max.X -= element.valueDrawer.Em().Round() bounds.Max.X -= element.valueDrawer.Em().Round()
cursorPosition := element.valueDrawer.PositionOf(element.cursor) cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
cursorPosition.X -= element.scroll cursorPosition.X -= element.scroll
maxX := bounds.Max.X maxX := bounds.Max.X
minX := maxX minX := maxX
@@ -272,57 +310,104 @@ func (element *TextBox) scrollToCursor () {
} }
} }
// 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 () { func (element *TextBox) draw () {
bounds := element.Bounds() bounds := element.Bounds()
// FIXME: take index into account state := theme.State {
pattern, inset := theme.InputPattern(theme.PatternState {
Case: textBoxCase,
Disabled: !element.Enabled(), Disabled: !element.Enabled(),
Focused: element.Focused(), Focused: element.Focused(),
}) }
artist.FillRectangle(element, pattern, bounds) 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)
if len(element.text) == 0 && !element.Focused() { 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 // draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds() textBounds := element.placeholderDrawer.LayoutBounds()
offset := bounds.Min.Add (image.Point { foreground := element.theme.Color (
X: theme.Padding() + inset[3], theme.ColorForeground,
Y: theme.Padding() + inset[0], theme.State { Disabled: true })
})
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Case: textBoxCase,
Disabled: true,
})
element.placeholderDrawer.Draw ( element.placeholderDrawer.Draw (
element, innerCanvas,
foreground, foreground,
offset.Sub(textBounds.Min)) offset.Sub(textBounds.Min))
} else { } else {
// draw input value // draw input value
textBounds := element.valueDrawer.LayoutBounds() textBounds := element.valueDrawer.LayoutBounds()
offset := bounds.Min.Add (image.Point { foreground := element.theme.Color(theme.ColorForeground, state)
X: theme.Padding() + inset[3] - element.scroll,
Y: theme.Padding() + inset[0],
})
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Case: textBoxCase,
Disabled: !element.Enabled(),
})
element.valueDrawer.Draw ( element.valueDrawer.Draw (
element, innerCanvas,
foreground, foreground,
offset.Sub(textBounds.Min)) offset.Sub(textBounds.Min))
}
if element.Focused() { if element.Focused() && element.dot.Empty() {
// cursor // draw cursor
cursorPosition := element.valueDrawer.PositionOf ( foreground := element.theme.Color(theme.ColorForeground, state)
element.cursor) cursorPosition := fixedutil.RoundPt (
foreground, _ := theme.ForegroundPattern(theme.PatternState { element.valueDrawer.PositionAt(element.dot.End))
Case: textBoxCase, shapes.ColorLine (
}) innerCanvas,
artist.Line (
element,
foreground, 1, foreground, 1,
cursorPosition.Add(offset), cursorPosition.Add(offset),
image.Pt ( image.Pt (
@@ -330,5 +415,4 @@ func (element *TextBox) draw () {
cursorPosition.Y + element.valueDrawer. cursorPosition.Y + element.valueDrawer.
LineHeight().Round()).Add(offset)) LineHeight().Round()).Add(offset))
} }
}
} }
+51 -33
View File
@@ -2,12 +2,12 @@ package core
import "image" import "image"
import "image/color" import "image/color"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/canvas"
// Core is a struct that implements some core functionality common to most // Core is a struct that implements some core functionality common to most
// widgets. It is meant to be embedded directly into a struct. // widgets. It is meant to be embedded directly into a struct.
type Core struct { type Core struct {
canvas tomo.Canvas canvas canvas.Canvas
metrics struct { metrics struct {
minimumWidth int minimumWidth int
@@ -16,45 +16,30 @@ type Core struct {
drawSizeChange func () drawSizeChange func ()
onMinimumSizeChange func () onMinimumSizeChange func ()
onDamage func (region tomo.Canvas) onDamage func (region canvas.Canvas)
} }
// NewCore creates a new element core and its corresponding control. // NewCore creates a new element core and its corresponding control.
func NewCore (drawSizeChange func ()) (core *Core, control CoreControl) { func NewCore (
core = &Core { drawSizeChange: drawSizeChange } drawSizeChange func (),
) (
core *Core,
control CoreControl,
) {
core = &Core {
drawSizeChange: drawSizeChange,
}
control = CoreControl { core: core } control = CoreControl { core: core }
return return
} }
// ColorModel fulfills the draw.Image interface. // Bounds fulfills the tomo.Element interface. This should not need to be
func (core *Core) ColorModel () (model color.Model) { // overridden.
return color.RGBAModel
}
// ColorModel fulfills the draw.Image interface.
func (core *Core) At (x, y int) (pixel color.Color) {
if core.canvas == nil { return }
return core.canvas.At(x, y)
}
// ColorModel fulfills the draw.Image interface.
func (core *Core) Bounds () (bounds image.Rectangle) { func (core *Core) Bounds () (bounds image.Rectangle) {
if core.canvas == nil { return } if core.canvas == nil { return }
return core.canvas.Bounds() return core.canvas.Bounds()
} }
// ColorModel fulfills the draw.Image interface.
func (core *Core) Set (x, y int, c color.Color) () {
if core.canvas == nil { return }
core.canvas.Set(x, y, c)
}
// Buffer fulfills the tomo.Canvas interface.
func (core *Core) Buffer () (data []color.RGBA, stride int) {
if core.canvas == nil { return }
return core.canvas.Buffer()
}
// MinimumSize fulfils the tomo.Element interface. This should not need to be // MinimumSize fulfils the tomo.Element interface. This should not need to be
// overridden. // overridden.
func (core *Core) MinimumSize () (width, height int) { func (core *Core) MinimumSize () (width, height int) {
@@ -63,7 +48,7 @@ func (core *Core) MinimumSize () (width, height int) {
// DrawTo fulfills the tomo.Element interface. This should not need to be // DrawTo fulfills the tomo.Element interface. This should not need to be
// overridden. // overridden.
func (core *Core) DrawTo (canvas tomo.Canvas) { func (core *Core) DrawTo (canvas canvas.Canvas) {
core.canvas = canvas core.canvas = canvas
if core.drawSizeChange != nil { if core.drawSizeChange != nil {
core.drawSizeChange() core.drawSizeChange()
@@ -72,7 +57,7 @@ func (core *Core) DrawTo (canvas tomo.Canvas) {
// OnDamage fulfils the tomo.Element interface. This should not need to be // OnDamage fulfils the tomo.Element interface. This should not need to be
// overridden. // overridden.
func (core *Core) OnDamage (callback func (region tomo.Canvas)) { func (core *Core) OnDamage (callback func (region canvas.Canvas)) {
core.onDamage = callback core.onDamage = callback
} }
@@ -90,6 +75,35 @@ type CoreControl struct {
core *Core 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 // HasImage returns true if the core has an allocated image buffer, and false if
// it doesn't. // it doesn't.
func (control CoreControl) HasImage () (has bool) { func (control CoreControl) HasImage () (has bool) {
@@ -98,9 +112,13 @@ func (control CoreControl) HasImage () (has bool) {
// DamageRegion pushes the selected region of pixels to the parent element. This // DamageRegion pushes the selected region of pixels to the parent element. This
// does not need to be called when responding to a resize event. // does not need to be called when responding to a resize event.
func (control CoreControl) DamageRegion (bounds image.Rectangle) { func (control CoreControl) DamageRegion (regions ...image.Rectangle) {
if control.core.canvas == nil { return }
if control.core.onDamage != nil { if control.core.onDamage != nil {
control.core.onDamage(tomo.Cut(control.core, bounds)) for _, region := range regions {
control.core.onDamage (
canvas.Cut(control.core.canvas, region))
}
} }
} }
+7
View File
@@ -0,0 +1,7 @@
// 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
+14 -7
View File
@@ -1,6 +1,6 @@
package core package core
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input"
// FocusableCore is a struct that can be embedded into objects to make them // FocusableCore is a struct that can be embedded into objects to make them
// focusable, giving them the default keynav behavior. // focusable, giving them the default keynav behavior.
@@ -9,7 +9,7 @@ type FocusableCore struct {
enabled bool enabled bool
drawFocusChange func () drawFocusChange func ()
onFocusRequest func () (granted bool) onFocusRequest func () (granted bool)
onFocusMotionRequest func(tomo.KeynavDirection) (granted bool) onFocusMotionRequest func(input.KeynavDirection) (granted bool)
} }
// NewFocusableCore creates a new focusability core and its corresponding // NewFocusableCore creates a new focusability core and its corresponding
@@ -37,27 +37,34 @@ func (core *FocusableCore) Focused () (focused bool) {
// Focus focuses this element, if its parent element grants the request. // Focus focuses this element, if its parent element grants the request.
func (core *FocusableCore) Focus () { func (core *FocusableCore) Focus () {
if !core.enabled { return } if !core.enabled || core.focused { return }
if core.onFocusRequest != nil { if core.onFocusRequest != nil {
core.onFocusRequest() if core.onFocusRequest() {
core.focused = true
if core.drawFocusChange != nil {
core.drawFocusChange()
}
}
} }
} }
// HandleFocus causes this element to mark itself as focused, if it can // HandleFocus causes this element to mark itself as focused, if it can
// currently be. Otherwise, it will return false and do nothing. // currently be. Otherwise, it will return false and do nothing.
func (core *FocusableCore) HandleFocus ( func (core *FocusableCore) HandleFocus (
direction tomo.KeynavDirection, direction input.KeynavDirection,
) ( ) (
accepted bool, accepted bool,
) { ) {
direction = direction.Canon() direction = direction.Canon()
if !core.enabled { return false } if !core.enabled { return false }
if core.focused && direction != tomo.KeynavDirectionNeutral { if core.focused && direction != input.KeynavDirectionNeutral {
return false return false
} }
if core.focused == false {
core.focused = true core.focused = true
if core.drawFocusChange != nil { core.drawFocusChange() } if core.drawFocusChange != nil { core.drawFocusChange() }
}
return true return true
} }
@@ -80,7 +87,7 @@ func (core *FocusableCore) OnFocusRequest (callback func () (granted bool)) {
// should return true if the request was granted, and false if it was // should return true if the request was granted, and false if it was
// not. // not.
func (core *FocusableCore) OnFocusMotionRequest ( func (core *FocusableCore) OnFocusMotionRequest (
callback func (direction tomo.KeynavDirection) (granted bool), callback func (direction input.KeynavDirection) (granted bool),
) { ) {
core.onFocusMotionRequest = callback core.onFocusMotionRequest = callback
} }
+6
View File
@@ -0,0 +1,6 @@
// 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
+58 -42
View File
@@ -1,13 +1,25 @@
package tomo package elements
import "image" 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. // Element represents a basic on-screen object.
type Element interface { type Element interface {
// Element must implement the Canvas interface. Elements should start // Bounds reports the element's bounding box. This must reflect the
// out with a completely blank buffer, and only allocate memory and draw // bounding box of the last canvas given to the element by DrawTo.
// on it for the first time when sent an EventResize event. Bounds () (bounds image.Rectangle)
Canvas
// 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 // MinimumSize specifies the minimum amount of pixels this element's
// width and height may be set to. If the element is given a resize // width and height may be set to. If the element is given a resize
@@ -15,47 +27,19 @@ type Element interface {
// instead of the offending dimension(s). // instead of the offending dimension(s).
MinimumSize () (width, height int) MinimumSize () (width, height int)
// 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)
// 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))
// OnMinimumSizeChange sets a function to be called when the element's // OnMinimumSizeChange sets a function to be called when the element's
// minimum size is changed. // minimum size is changed.
OnMinimumSizeChange (callback func ()) OnMinimumSizeChange (callback func ())
} }
// KeynavDirection represents a keyboard navigation direction.
type KeynavDirection int
const (
KeynavDirectionNeutral KeynavDirection = 0
KeynavDirectionBackward KeynavDirection = -1
KeynavDirectionForward KeynavDirection = 1
)
// Canon returns a well-formed direction.
func (direction KeynavDirection) Canon () (canon KeynavDirection) {
if direction > 0 {
return KeynavDirectionForward
} else if direction == 0 {
return KeynavDirectionNeutral
} else {
return KeynavDirectionBackward
}
}
// Focusable represents an element that has keyboard navigation support. This // Focusable represents an element that has keyboard navigation support. This
// includes inputs, buttons, sliders, etc. as well as any elements that have // includes inputs, buttons, sliders, etc. as well as any elements that have
// children (so keyboard navigation events can be propagated downward). // children (so keyboard navigation events can be propagated downward).
type Focusable interface { type Focusable interface {
Element Element
// Focused returns whether or not this element is currently focused. // Focused returns whether or not this element or any of its children
// are currently focused.
Focused () (selected bool) Focused () (selected bool)
// Focus focuses this element, if its parent element grants the // Focus focuses this element, if its parent element grants the
@@ -67,7 +51,7 @@ type Focusable interface {
// selectable children in the given direction, it should return false // selectable children in the given direction, it should return false
// and do nothing. Otherwise, it should select itself and any children // and do nothing. Otherwise, it should select itself and any children
// (if applicable) and return true. // (if applicable) and return true.
HandleFocus (direction KeynavDirection) (accepted bool) HandleFocus (direction input.KeynavDirection) (accepted bool)
// HandleDeselection causes this element to mark itself and all of its // HandleDeselection causes this element to mark itself and all of its
// children as unfocused. // children as unfocused.
@@ -75,7 +59,9 @@ type Focusable interface {
// OnFocusRequest sets a function to be called when this element wants // OnFocusRequest sets a function to be called when this element wants
// its parent element to focus it. Parent elements should return true if // its parent element to focus it. Parent elements should return true if
// the request was granted, and false if it was not. // 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)) OnFocusRequest (func () (granted bool))
// OnFocusMotionRequest sets a function to be called when this // OnFocusMotionRequest sets a function to be called when this
@@ -83,7 +69,7 @@ type Focusable interface {
// front of it, depending on the specified direction. Parent elements // front of it, depending on the specified direction. Parent elements
// should return true if the request was granted, and false if it was // should return true if the request was granted, and false if it was
// not. // not.
OnFocusMotionRequest (func (direction KeynavDirection) (granted bool)) OnFocusMotionRequest (func (direction input.KeynavDirection) (granted bool))
} }
// KeyboardTarget represents an element that can receive keyboard input. // KeyboardTarget represents an element that can receive keyboard input.
@@ -95,11 +81,11 @@ type KeyboardTarget interface {
// every key down event is guaranteed to be paired with exactly one key // 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 // up event. This is the reason a list of modifier keys held down at the
// time of the key press is given. // time of the key press is given.
HandleKeyDown (key Key, modifiers Modifiers) HandleKeyDown (key input.Key, modifiers input.Modifiers)
// HandleKeyUp is called when a key is released while this element has // HandleKeyUp is called when a key is released while this element has
// keyboard focus. // keyboard focus.
HandleKeyUp (key Key, modifiers Modifiers) HandleKeyUp (key input.Key, modifiers input.Modifiers)
} }
// MouseTarget represents an element that can receive mouse events. // MouseTarget represents an element that can receive mouse events.
@@ -111,11 +97,11 @@ type MouseTarget interface {
// HandleMouseDown is called when a mouse button is pressed down on this // HandleMouseDown is called when a mouse button is pressed down on this
// element. // element.
HandleMouseDown (x, y int, button Button) HandleMouseDown (x, y int, button input.Button)
// HandleMouseUp is called when a mouse button is released that was // HandleMouseUp is called when a mouse button is released that was
// originally pressed down on this element. // originally pressed down on this element.
HandleMouseUp (x, y int, button Button) HandleMouseUp (x, y int, button input.Button)
// HandleMouseMove is called when the mouse is moved over this element, // HandleMouseMove is called when the mouse is moved over this element,
// or the mouse is moving while being held down and originally pressed // or the mouse is moving while being held down and originally pressed
@@ -175,3 +161,33 @@ type Scrollable interface {
// ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed. // ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed.
OnScrollBoundsChange (callback func ()) 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)
}
+35 -18
View File
@@ -3,22 +3,26 @@ package fun
import "time" import "time"
import "math" import "math"
import "image" import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var clockCase = theme.C("fun", "clock")
// AnalogClock can display the time of day in an analog format. // AnalogClock can display the time of day in an analog format.
type AnalogClock struct { type AnalogClock struct {
*core.Core *core.Core
core core.CoreControl core core.CoreControl
time time.Time time time.Time
config config.Wrapped
theme theme.Wrapped
} }
// NewAnalogClock creates a new analog clock that displays the specified time. // NewAnalogClock creates a new analog clock that displays the specified time.
func NewAnalogClock (newTime time.Time) (element *AnalogClock) { func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
element = &AnalogClock { } element = &AnalogClock { }
element.theme.Case = theme.C("fun", "clock")
element.Core, element.core = core.NewCore(element.draw) element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(64, 64) element.core.SetMinimumSize(64, 64)
return return
@@ -28,6 +32,24 @@ func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
func (element *AnalogClock) SetTime (newTime time.Time) { func (element *AnalogClock) SetTime (newTime time.Time) {
if newTime == element.time { return } if newTime == element.time { return }
element.time = newTime element.time = newTime
element.redo()
}
// 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()
}
// 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() { if element.core.HasImage() {
element.draw() element.draw()
element.core.DamageAll() element.core.DamageAll()
@@ -37,19 +59,15 @@ func (element *AnalogClock) SetTime (newTime time.Time) {
func (element *AnalogClock) draw () { func (element *AnalogClock) draw () {
bounds := element.Bounds() bounds := element.Bounds()
pattern, inset := theme.SunkenPattern(theme.PatternState { state := theme.State { }
Case: clockCase, pattern := element.theme.Pattern(theme.PatternSunken, state)
}) padding := element.theme.Padding(theme.PatternSunken)
artist.FillRectangle(element, pattern, bounds) pattern.Draw(element.core, bounds)
bounds = inset.Apply(bounds) bounds = padding.Apply(bounds)
foreground, _ := theme.ForegroundPattern(theme.PatternState { foreground := element.theme.Color(theme.ColorForeground, state)
Case: clockCase, accent := element.theme.Color(theme.ColorAccent, state)
})
accent, _ := theme.AccentPattern(theme.PatternState {
Case: clockCase,
})
for hour := 0; hour < 12; hour ++ { for hour := 0; hour < 12; hour ++ {
element.radialLine ( element.radialLine (
@@ -71,12 +89,12 @@ func (element *AnalogClock) FlexibleHeightFor (width int) (height int) {
return width return width
} }
// OnFlexibleHeightChange sets a function to be calle dwhen the parameters // OnFlexibleHeightChange sets a function to be called when the parameters
// affecting the clock's flexible height change. // affecting the clock's flexible height change.
func (element *AnalogClock) OnFlexibleHeightChange (func ()) { } func (element *AnalogClock) OnFlexibleHeightChange (func ()) { }
func (element *AnalogClock) radialLine ( func (element *AnalogClock) radialLine (
source artist.Pattern, source color.RGBA,
inner float64, inner float64,
outer float64, outer float64,
radian float64, radian float64,
@@ -90,6 +108,5 @@ func (element *AnalogClock) radialLine (
max := element.Bounds().Min.Add(image.Pt ( max := element.Bounds().Min.Add(image.Pt (
int(math.Cos(radian) * outer * width + width), int(math.Cos(radian) * outer * width + width),
int(math.Sin(radian) * outer * height + height))) int(math.Sin(radian) * outer * height + height)))
// println(min.String(), max.String()) shapes.ColorLine(element.core, source, 1, min, max)
artist.Line(element, source, 1, min, max)
} }
+3
View File
@@ -0,0 +1,3 @@
// Package fun provides "fun" elements that have few actual use cases, but serve
// as good demos of what Tomo is capable of.
package fun
+4
View File
@@ -0,0 +1,4 @@
// Package music provides types relating to music theory and the math behind it.
// It is used in the fun.Piano element, and in the piano example to generate
// pitches from notes.
package music
+70
View File
@@ -0,0 +1,70 @@
package music
import "math"
var semitone = math.Pow(2, 1.0 / 12.0)
// Tuning is an interface representing a tuning.
type Tuning interface {
// Tune returns the frequency of a given note in Hz.
Tune (Note) float64
}
// EqualTemparment implements twelve-tone equal temparment.
type EqualTemparment struct { A4 float64 }
// Tune returns the EqualTemparment frequency of a given note in Hz.
func (tuning EqualTemparment) Tune (note Note) float64 {
return tuning.A4 * math.Pow(semitone, float64(note - NoteA4))
}
// Octave represents a MIDI octave.
type Octave int
// Note returns the note at the specified scale degree in the chromatic scale.
func (octave Octave) Note (degree int) Note {
return Note(int(octave + 1) * 12 + degree)
}
// Note represents a MIDI note.
type Note int
const (
NoteC0 Note = iota
NoteDb0
NoteD0
NoteEb0
NoteE0
NoteF0
NoteGb0
NoteG0
NoteAb0
NoteA0
NoteBb0
NoteB0
// nice
NoteA4 Note = 69
)
// Octave returns the octave of the note
func (note Note) Octave () Octave {
return Octave(note / 12 - 1)
}
// Degree returns the scale degree of the note in the chromatic scale.
func (note Note) Degree () int {
mod := note % 12
if mod < 0 { mod += 12 }
return int(mod)
}
// IsSharp returns whether or not the note is a sharp.
func (note Note) IsSharp () bool {
degree := note.Degree()
return degree == 1 ||
degree == 3 ||
degree == 6 ||
degree == 8 ||
degree == 10
}
+331
View File
@@ -0,0 +1,331 @@
package fun
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"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
const pianoKeyWidth = 18
type pianoKey struct {
image.Rectangle
music.Note
}
// 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
flatKeys []pianoKey
sharpKeys []pianoKey
contentBounds image.Rectangle
pressed *pianoKey
keynavPressed map[music.Note] bool
onPress func (music.Note)
onRelease func (music.Note)
}
// 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
}
element = &Piano {
low: low,
high: high,
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.updateMinimumSize()
return
}
// OnPress sets a function to be called when a key is pressed.
func (element *Piano) OnPress (callback func (note music.Note)) {
element.onPress = callback
}
// OnRelease sets a function to be called when a key is released.
func (element *Piano) OnRelease (callback func (note music.Note)) {
element.onRelease = callback
}
func (element *Piano) HandleMouseDown (x, y int, button input.Button) {
element.Focus()
if button != input.ButtonLeft { return }
element.pressUnderMouseCursor(image.Pt(x, y))
}
func (element *Piano) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft { return }
if element.onRelease != nil && element.pressed != nil {
element.onRelease((*element.pressed).Note)
}
element.pressed = nil
element.redo()
}
func (element *Piano) HandleMouseMove (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)
for index, key := range element.flatKeys {
if point.In(key.Rectangle) {
newKey = &element.flatKeys[index]
break
}
}
for index, key := range element.sharpKeys {
if point.In(key.Rectangle) {
newKey = &element.sharpKeys[index]
break
}
}
if newKey == nil { return }
if newKey != element.pressed {
// release previous note
if element.pressed != nil && element.onRelease != nil {
element.onRelease((*element.pressed).Note)
}
// press new note
element.pressed = newKey
if element.onPress != nil {
element.onPress((*element.pressed).Note)
}
element.redo()
}
}
var noteForKey = map[input.Key] music.Note {
'a': 46,
'z': 47,
'x': 48,
'd': 49,
'c': 50,
'f': 51,
'v': 52,
'b': 53,
'h': 54,
'n': 55,
'j': 56,
'm': 57,
'k': 58,
',': 59,
'.': 60,
';': 61,
'/': 62,
'\'': 63,
'1': 56,
'q': 57,
'2': 58,
'w': 59,
'e': 60,
'4': 61,
'r': 62,
'5': 63,
't': 64,
'y': 65,
'7': 66,
'u': 67,
'8': 68,
'i': 69,
'9': 70,
'o': 71,
'p': 72,
'-': 73,
'[': 74,
'=': 75,
']': 76,
'\\': 77,
}
func (element *Piano) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
note, exists := noteForKey[key]
if !exists { return }
if !element.keynavPressed[note] {
element.keynavPressed[note] = true
if element.onPress != nil {
element.onPress(note)
}
element.redo()
}
}
func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
note, exists := noteForKey[key]
if !exists { return }
_, pressed := element.keynavPressed[note]
if !pressed { return }
delete(element.keynavPressed, note)
if element.onRelease != nil {
element.onRelease(note)
}
element.redo()
}
// SetTheme sets the element's theme.
func (element *Piano) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
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()
}
func (element *Piano) updateMinimumSize () {
padding := element.theme.Padding(theme.PatternPinboard)
element.core.SetMinimumSize (
pianoKeyWidth * 7 * element.countOctaves() +
padding.Horizontal(),
64 + padding.Vertical())
}
func (element *Piano) countOctaves () int {
return int(element.high - element.low + 1)
}
func (element *Piano) countFlats () int {
return element.countOctaves() * 8
}
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())
dot := bounds.Min
note := element.low.Note(0)
limit := element.high.Note(12)
flatIndex := 0
sharpIndex := 0
for note < limit {
if note.IsSharp() {
element.sharpKeys[sharpIndex].Rectangle = image.Rect (
-(pianoKeyWidth * 3) / 7, 0,
(pianoKeyWidth * 3) / 7,
(bounds.Dy() * 5) / 8).Add(dot)
element.sharpKeys[sharpIndex].Note = note
sharpIndex ++
} else {
element.flatKeys[flatIndex].Rectangle = image.Rect (
0, 0, pianoKeyWidth, bounds.Dy()).Add(dot)
dot.X += pianoKeyWidth
element.flatKeys[flatIndex].Note = note
flatIndex ++
}
note ++
}
element.contentBounds = image.Rectangle {
bounds.Min,
image.Pt(dot.X, bounds.Max.Y),
}
}
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 (
bounds image.Rectangle,
pressed bool,
state theme.State,
) {
state.Pressed = pressed
pattern := element.theme.Theme.Pattern (
theme.PatternButton, state, theme.C("fun", "flatKey"))
artist.DrawBounds(element.core, pattern, bounds)
}
func (element *Piano) drawSharp (
bounds image.Rectangle,
pressed bool,
state theme.State,
) {
state.Pressed = pressed
pattern := element.theme.Theme.Pattern (
theme.PatternButton, state, theme.C("fun", "sharpKey"))
artist.DrawBounds(element.core, pattern, bounds)
}
+126 -253
View File
@@ -4,319 +4,192 @@ import "fmt"
import "time" import "time"
import "image" import "image"
import "image/color" import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/defaultfont" import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
// Artist is an element that displays shapes and patterns drawn by the artist // Artist is an element that displays shapes and patterns drawn by the artist
// package in order to test it. // package in order to test it.
type Artist struct { type Artist struct {
*core.Core *core.Core
core core.CoreControl core core.CoreControl
cellBounds image.Rectangle
} }
// NewArtist creates a new artist test element. // NewArtist creates a new artist test element.
func NewArtist () (element *Artist) { func NewArtist () (element *Artist) {
element = &Artist { } element = &Artist { }
element.Core, element.core = core.NewCore(element.draw) element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(480, 600) element.core.SetMinimumSize(240, 240)
return return
} }
func (element *Artist) draw () { func (element *Artist) draw () {
bounds := element.Bounds() bounds := element.Bounds()
element.cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5 patterns.Uhex(0x000000FF).Draw(element.core, bounds)
element.cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 8
drawStart := time.Now() drawStart := time.Now()
// 0, 0 // 0, 0 - 3, 0
artist.FillRectangle ( for x := 0; x < 4; x ++ {
element, element.colorLines(x + 1, element.cellAt(x, 0).Bounds())
artist.Beveled { }
artist.NewUniform(hex(0xFF0000FF)),
artist.NewUniform(hex(0x0000FFFF)),
},
element.cellAt(0, 0))
// 1, 0
artist.StrokeRectangle (
element,
artist.NewUniform(hex(0x00FF00FF)), 3,
element.cellAt(1, 0))
// 2, 0
artist.FillRectangle (
element,
artist.NewMultiBordered (
artist.Stroke { Pattern: uhex(0xFF0000FF), Weight: 1 },
artist.Stroke { Pattern: uhex(0x888800FF), Weight: 2 },
artist.Stroke { Pattern: uhex(0x00FF00FF), Weight: 3 },
artist.Stroke { Pattern: uhex(0x008888FF), Weight: 4 },
artist.Stroke { Pattern: uhex(0x0000FFFF), Weight: 5 },
),
element.cellAt(2, 0))
// 3, 0
artist.FillRectangle (
element,
artist.Bordered {
Stroke: artist.Stroke { Pattern: uhex(0x0000FFFF), Weight: 5 },
Fill: uhex(0xFF0000FF),
},
element.cellAt(3, 0))
// 4, 0 // 4, 0
artist.FillRectangle ( c40 := element.cellAt(4, 0)
element, shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1)
artist.Padded { shapes.ColorLine (
Stroke: uhex(0xFFFFFFFF), c40, artist.Hex(0xFF0000FF), 1,
Fill: uhex(0x666666FF), c40.Bounds().Min, c40.Bounds().Max)
Sides: []int { 4, 13, 2, 0 },
},
element.cellAt(4, 0))
// 0, 1 - 3, 1 // 0, 1
for x := 0; x < 4; x ++ { c01 := element.cellAt(0, 1)
artist.FillRectangle ( shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1)
element, shapes.FillColorEllipse(element.core, artist.Hex(0x00FF00FF), c01.Bounds())
artist.Striped {
First: artist.Stroke { Pattern: uhex(0xFF8800FF), Weight: 7 },
Second: artist.Stroke { Pattern: uhex(0x0088FFFF), Weight: 2 },
Orientation: artist.Orientation(x),
}, // 1, 1 - 3, 1
element.cellAt(x, 1)) for x := 1; x < 4; x ++ {
c := element.cellAt(x, 1)
shapes.StrokeColorRectangle (
element.core, artist.Hex(0x888888FF),
c.Bounds(), 1)
shapes.StrokeColorEllipse (
element.core,
[]color.RGBA {
artist.Hex(0xFF0000FF),
artist.Hex(0x00FF00FF),
artist.Hex(0xFF00FFFF),
} [x - 1],
c.Bounds(), x)
} }
// 0, 2 - 3, 2 // 4, 1
for x := 0; x < 4; x ++ { c41 := element.cellAt(4, 1)
element.lines(x + 1, element.cellAt(x, 2)) shatterPos := c41.Bounds().Min
rocks := []image.Rectangle {
image.Rect(3, 12, 13, 23).Add(shatterPos),
// image.Rect(30, 10, 40, 23).Add(shatterPos),
image.Rect(55, 40, 70, 49).Add(shatterPos),
image.Rect(30, -10, 40, 43).Add(shatterPos),
image.Rect(80, 30, 90, 45).Add(shatterPos),
} }
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)
}
// 0, 2
c02 := element.cellAt(0, 2)
shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
shapes.FillEllipse(c02, c41)
// 1, 2
c12 := element.cellAt(1, 2)
shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
shapes.StrokeEllipse(c12, c41, 5)
// 2, 2
c22 := element.cellAt(2, 2)
shapes.FillRectangle(c22, c41)
// 3, 2
c32 := element.cellAt(3, 2)
shapes.StrokeRectangle(c32, c41, 5)
// 4, 2
c42 := element.cellAt(4, 2)
// 0, 3 // 0, 3
artist.StrokeRectangle ( c03 := element.cellAt(0, 3)
element,uhex(0x888888FF), 1, patterns.Border {
element.cellAt(0, 3)) Canvas: element.thingy(c42),
artist.FillEllipse(element, uhex(0x00FF00FF), element.cellAt(0, 3)) Inset: artist.Inset { 8, 8, 8, 8 },
}.Draw(c03, c03.Bounds())
// 1, 3 - 3, 3 // 1, 3
for x := 1; x < 4; x ++ { c13 := element.cellAt(1, 3)
artist.StrokeRectangle ( patterns.Border {
element,uhex(0x888888FF), 1, Canvas: element.thingy(c42),
element.cellAt(x, 3)) Inset: artist.Inset { 8, 8, 8, 8 },
artist.StrokeEllipse ( }.Draw(c13, c13.Bounds().Inset(10))
element,
[]artist.Pattern {
uhex(0xFF0000FF),
uhex(0x00FF00FF),
uhex(0xFF00FFFF),
} [x - 1],
x, element.cellAt(x, 3))
}
// 0, 4 - 3, 4
for x := 0; x < 4; x ++ {
artist.FillEllipse (
element,
artist.Split {
First: uhex(0xFF0000FF),
Second: uhex(0x0000FFFF),
Orientation: artist.Orientation(x),
},
element.cellAt(x, 4))
}
// how long did that take to render? // how long did that take to render?
drawTime := time.Since(drawStart) drawTime := time.Since(drawStart)
textDrawer := artist.TextDrawer { } textDrawer := textdraw.Drawer { }
textDrawer.SetFace(defaultfont.FaceRegular) textDrawer.SetFace(defaultfont.FaceRegular)
textDrawer.SetText ([]rune (fmt.Sprintf ( textDrawer.SetText ([]rune (fmt.Sprintf (
"%dms\n%dus", "%dms\n%dus",
drawTime.Milliseconds(), drawTime.Milliseconds(),
drawTime.Microseconds()))) drawTime.Microseconds())))
textDrawer.Draw(element, uhex(0xFFFFFFFF), image.Pt(8, bounds.Max.Y - 24)) textDrawer.Draw (
element.core, artist.Hex(0xFFFFFFFF),
// 0, 5 image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
artist.FillRectangle (
element,
artist.QuadBeveled {
uhex(0x880000FF),
uhex(0x00FF00FF),
uhex(0x0000FFFF),
uhex(0xFF00FFFF),
},
element.cellAt(0, 5))
// 1, 5
artist.FillRectangle (
element,
artist.Checkered {
First: artist.QuadBeveled {
uhex(0x880000FF),
uhex(0x00FF00FF),
uhex(0x0000FFFF),
uhex(0xFF00FFFF),
},
Second: artist.Striped {
First: artist.Stroke { Pattern: uhex(0xFF8800FF), Weight: 1 },
Second: artist.Stroke { Pattern: uhex(0x0088FFFF), Weight: 1 },
Orientation: artist.OrientationVertical,
},
CellWidth: 32,
CellHeight: 16,
},
element.cellAt(1, 5))
// 2, 5
artist.FillRectangle (
element,
artist.Dotted {
Foreground: uhex(0x00FF00FF),
Background: artist.Checkered {
First: uhex(0x444444FF),
Second: uhex(0x888888FF),
CellWidth: 16,
CellHeight: 16,
},
Size: 8,
Spacing: 16,
},
element.cellAt(2, 5))
// 3, 5
artist.FillRectangle (
element,
artist.Tiled {
Pattern: artist.QuadBeveled {
uhex(0x880000FF),
uhex(0x00FF00FF),
uhex(0x0000FFFF),
uhex(0xFF00FFFF),
},
CellWidth: 17,
CellHeight: 23,
},
element.cellAt(3, 5))
// 0, 6 - 3, 6
for x := 0; x < 4; x ++ {
artist.FillRectangle (
element,
artist.Gradient {
First: uhex(0xFF0000FF),
Second: uhex(0x0000FFFF),
Orientation: artist.Orientation(x),
},
element.cellAt(x, 6))
}
// 0, 7
artist.FillEllipse (
element,
artist.EllipticallyBordered {
Fill: artist.Gradient {
First: uhex(0x00FF00FF),
Second: uhex(0x0000FFFF),
Orientation: artist.OrientationVertical,
},
Stroke: artist.Stroke { Pattern: uhex(0x00FF00), Weight: 5 },
},
element.cellAt(0, 7))
// 1, 7
artist.FillRectangle (
element,
artist.Noisy {
Low: uhex(0x000000FF),
High: uhex(0xFFFFFFFF),
Seed: 0,
},
element.cellAt(1, 7),
)
// 2, 7
artist.FillRectangle (
element,
artist.Noisy {
Low: uhex(0x000000FF),
High: artist.Gradient {
First: uhex(0x000000FF),
Second: uhex(0xFFFFFFFF),
Orientation: artist.OrientationVertical,
},
Seed: 0,
},
element.cellAt(2, 7),
)
// 3, 7
artist.FillRectangle (
element,
artist.Noisy {
Low: uhex(0x000000FF),
High: uhex(0xFFFFFFFF),
Seed: 0,
Harsh: true,
},
element.cellAt(3, 7),
)
} }
func (element *Artist) lines (weight int, bounds image.Rectangle) { func (element *Artist) colorLines (weight int, bounds image.Rectangle) {
bounds = bounds.Inset(4) bounds = bounds.Inset(4)
c := uhex(0xFFFFFFFF) c := artist.Hex(0xFFFFFFFF)
artist.Line(element, c, weight, bounds.Min, bounds.Max) shapes.ColorLine(element.core, c, weight, bounds.Min, bounds.Max)
artist.Line ( shapes.ColorLine (
element, c, weight, element.core, c, weight,
image.Pt(bounds.Max.X, bounds.Min.Y), image.Pt(bounds.Max.X, bounds.Min.Y),
image.Pt(bounds.Min.X, bounds.Max.Y)) image.Pt(bounds.Min.X, bounds.Max.Y))
artist.Line ( shapes.ColorLine (
element, c, weight, element.core, c, weight,
image.Pt(bounds.Max.X, bounds.Min.Y + 16), image.Pt(bounds.Max.X, bounds.Min.Y + 16),
image.Pt(bounds.Min.X, bounds.Max.Y - 16)) image.Pt(bounds.Min.X, bounds.Max.Y - 16))
artist.Line ( shapes.ColorLine (
element, c, weight, element.core, c, weight,
image.Pt(bounds.Min.X, bounds.Min.Y + 16), image.Pt(bounds.Min.X, bounds.Min.Y + 16),
image.Pt(bounds.Max.X, bounds.Max.Y - 16)) image.Pt(bounds.Max.X, bounds.Max.Y - 16))
artist.Line ( shapes.ColorLine (
element, c, weight, element.core, c, weight,
image.Pt(bounds.Min.X + 20, bounds.Min.Y), image.Pt(bounds.Min.X + 20, bounds.Min.Y),
image.Pt(bounds.Max.X - 20, bounds.Max.Y)) image.Pt(bounds.Max.X - 20, bounds.Max.Y))
artist.Line ( shapes.ColorLine (
element, c, weight, element.core, c, weight,
image.Pt(bounds.Max.X - 20, bounds.Min.Y), image.Pt(bounds.Max.X - 20, bounds.Min.Y),
image.Pt(bounds.Min.X + 20, bounds.Max.Y)) image.Pt(bounds.Min.X + 20, bounds.Max.Y))
artist.Line ( shapes.ColorLine (
element, c, weight, element.core, c, weight,
image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2), image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2)) image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2))
artist.Line ( shapes.ColorLine (
element, c, weight, element.core, c, weight,
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y), image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y),
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y)) image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y))
} }
func (element *Artist) cellAt (x, y int) (image.Rectangle) { func (element *Artist) cellAt (x, y int) (canvas.Canvas) {
return element.cellBounds.Add (image.Pt ( bounds := element.Bounds()
x * element.cellBounds.Dx(), cellBounds := image.Rectangle { }
y * element.cellBounds.Dy())) 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 (
x * cellBounds.Dx(),
y * cellBounds.Dy())))
} }
func hex (n uint32) (c color.RGBA) { func (element *Artist) thingy (destination canvas.Canvas) (result canvas.Canvas) {
c.A = uint8(n) bounds := destination.Bounds()
c.B = uint8(n >> 8) bounds = image.Rect(0, 0, 32, 32).Add(bounds.Min)
c.G = uint8(n >> 16) shapes.FillColorRectangle(destination, artist.Hex(0x440000FF), bounds)
c.R = uint8(n >> 24) shapes.StrokeColorRectangle(destination, artist.Hex(0xFF0000FF), bounds, 1)
return 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)
func uhex (n uint32) (artist.Pattern) { return canvas.Cut(destination, bounds)
return artist.NewUniform (color.RGBA {
A: uint8(n),
B: uint8(n >> 8),
G: uint8(n >> 16),
R: uint8(n >> 24),
})
} }
+3
View File
@@ -0,0 +1,3 @@
// Package testing provides elements that are used to test different parts of
// Tomo's API.
package testing
+45 -21
View File
@@ -1,10 +1,11 @@
package testing package testing
import "image" import "image"
import "image/color" import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme" 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"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Mouse is an element capable of testing mouse input. When the mouse is clicked // Mouse is an element capable of testing mouse input. When the mouse is clicked
@@ -13,47 +14,70 @@ type Mouse struct {
*core.Core *core.Core
core core.CoreControl core core.CoreControl
drawing bool drawing bool
color artist.Pattern
lastMousePos image.Point lastMousePos image.Point
config config.Config
theme theme.Theme
c theme.Case
} }
// NewMouse creates a new mouse test element. // NewMouse creates a new mouse test element.
func NewMouse () (element *Mouse) { func NewMouse () (element *Mouse) {
element = &Mouse { } element = &Mouse { c: theme.C("testing", "mouse") }
element.Core, element.core = core.NewCore(element.draw) element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(32, 32) element.core.SetMinimumSize(32, 32)
element.color = artist.NewUniform(color.Black)
return return
} }
// SetTheme sets the element's theme.
func (element *Mouse) SetTheme (new theme.Theme) {
element.theme = new
element.redo()
}
// 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 () { func (element *Mouse) draw () {
bounds := element.Bounds() bounds := element.Bounds()
pattern, _ := theme.AccentPattern(theme.PatternState { }) accent := element.theme.Color (
artist.FillRectangle(element, pattern, bounds) theme.ColorAccent,
artist.StrokeRectangle ( theme.State { },
element, element.c)
artist.NewUniform(color.Black), 1, shapes.FillColorRectangle(element.core, accent, bounds)
bounds) shapes.StrokeColorRectangle (
artist.Line ( element.core,
element, artist.NewUniform(color.White), 1, artist.Hex(0x000000FF),
bounds, 1)
shapes.ColorLine (
element.core, artist.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, 1)), bounds.Min.Add(image.Pt(1, 1)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2))) bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
artist.Line ( shapes.ColorLine (
element, artist.NewUniform(color.White), 1, element.core, artist.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)), bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1))) bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
} }
func (element *Mouse) HandleMouseDown (x, y int, button tomo.Button) { func (element *Mouse) HandleMouseDown (x, y int, button input.Button) {
element.drawing = true element.drawing = true
element.lastMousePos = image.Pt(x, y) element.lastMousePos = image.Pt(x, y)
} }
func (element *Mouse) HandleMouseUp (x, y int, button tomo.Button) { func (element *Mouse) HandleMouseUp (x, y int, button input.Button) {
element.drawing = false element.drawing = false
mousePos := image.Pt(x, y) mousePos := image.Pt(x, y)
element.core.DamageRegion (artist.Line ( element.core.DamageRegion (shapes.ColorLine (
element, element.color, 1, element.core, artist.Hex(0x000000FF), 1,
element.lastMousePos, mousePos)) element.lastMousePos, mousePos))
element.lastMousePos = mousePos element.lastMousePos = mousePos
} }
@@ -61,8 +85,8 @@ func (element *Mouse) HandleMouseUp (x, y int, button tomo.Button) {
func (element *Mouse) HandleMouseMove (x, y int) { func (element *Mouse) HandleMouseMove (x, y int) {
if !element.drawing { return } if !element.drawing { return }
mousePos := image.Pt(x, y) mousePos := image.Pt(x, y)
element.core.DamageRegion (artist.Line ( element.core.DamageRegion (shapes.ColorLine (
element, element.color, 1, element.core, artist.Hex(0x000000FF), 1,
element.lastMousePos, mousePos)) element.lastMousePos, mousePos))
element.lastMousePos = mousePos element.lastMousePos = mousePos
} }
+1 -1
View File
@@ -1,4 +1,4 @@
package tomo package elements
import "image" import "image"
+6 -1
View File
@@ -3,15 +3,20 @@ package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing" import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
import _ "net/http/pprof"
import "net/http"
func main () { func main () {
tomo.Run(run) tomo.Run(run)
} }
func run () { func run () {
window, _ := tomo.NewWindow(128, 128) window, _ := tomo.NewWindow(480, 360)
window.SetTitle("Draw Test") window.SetTitle("Draw Test")
window.Adopt(testing.NewArtist()) window.Adopt(testing.NewArtist())
window.OnClose(tomo.Stop) window.OnClose(tomo.Stop)
window.Show() window.Show()
go func () {
http.ListenAndServe("localhost:9090", nil)
} ()
} }
+1 -1
View File
@@ -11,7 +11,7 @@ func main () {
func run () { func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("example button") window.SetTitle("example button")
button := basic.NewButton("hello tomo!") button := basicElements.NewButton("hello tomo!")
button.OnClick (func () { button.OnClick (func () {
// when we set the button's text to something longer, the window // when we set the button's text to something longer, the window
// will automatically resize to accomodate it. // will automatically resize to accomodate it.
+11 -11
View File
@@ -2,7 +2,7 @@ package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -14,22 +14,22 @@ func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Checkboxes") window.SetTitle("Checkboxes")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
container.Adopt (basic.NewLabel ( container.Adopt (basicElements.NewLabel (
"We advise you to not read thPlease listen to me. I am " + "We advise you to not read thPlease listen to me. I am " +
"trapped inside the example code. This is the only way for " + "trapped inside the example code. This is the only way for " +
"me to communicate.", true), true) "me to communicate.", true), true)
container.Adopt(basic.NewSpacer(true), false) container.Adopt(basicElements.NewSpacer(true), false)
container.Adopt(basic.NewCheckbox("Oh god", false), false) container.Adopt(basicElements.NewCheckbox("Oh god", false), false)
container.Adopt(basic.NewCheckbox("Can you hear them", true), false) container.Adopt(basicElements.NewCheckbox("Can you hear them", true), false)
container.Adopt(basic.NewCheckbox("They are in the walls", false), false) container.Adopt(basicElements.NewCheckbox("They are in the walls", false), false)
container.Adopt(basic.NewCheckbox("They are coming for us", false), false) container.Adopt(basicElements.NewCheckbox("They are coming for us", false), false)
disabledCheckbox := basic.NewCheckbox("We are but their helpless prey", false) disabledCheckbox := basicElements.NewCheckbox("We are but their helpless prey", false)
disabledCheckbox.SetEnabled(false) disabledCheckbox.SetEnabled(false)
container.Adopt(disabledCheckbox, false) container.Adopt(disabledCheckbox, false)
vsync := basic.NewCheckbox("Enable vsync", false) vsync := basicElements.NewCheckbox("Enable vsync", false)
vsync.OnToggle (func () { vsync.OnToggle (func () {
if vsync.Value() { if vsync.Value() {
popups.NewDialog ( popups.NewDialog (
@@ -39,7 +39,7 @@ func run () {
} }
}) })
container.Adopt(vsync, false) container.Adopt(vsync, false)
button := basic.NewButton("What") button := basicElements.NewButton("What")
button.OnClick(tomo.Stop) button.OnClick(tomo.Stop)
container.Adopt(button, false) container.Adopt(button, false)
button.Focus() button.Focus()
+5 -5
View File
@@ -1,7 +1,7 @@
package main package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -13,14 +13,14 @@ func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("dialog") window.SetTitle("dialog")
container := basic.NewContainer(layouts.Dialog { true, true }) container := basicElements.NewContainer(basicLayouts.Dialog { true, true })
window.Adopt(container) window.Adopt(container)
container.Adopt(basic.NewLabel("you will explode", true), true) container.Adopt(basicElements.NewLabel("you will explode", true), true)
cancel := basic.NewButton("Cancel") cancel := basicElements.NewButton("Cancel")
cancel.SetEnabled(false) cancel.SetEnabled(false)
container.Adopt(cancel, false) container.Adopt(cancel, false)
okButton := basic.NewButton("OK") okButton := basicElements.NewButton("OK")
container.Adopt(okButton, false) container.Adopt(okButton, false)
okButton.Focus() okButton.Focus()
+17 -17
View File
@@ -2,7 +2,7 @@ package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/flow" import "git.tebibyte.media/sashakoshka/tomo/flow"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -13,21 +13,21 @@ func main () {
func run () { func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("adventure") window.SetTitle("adventure")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
var world flow.Flow var world flow.Flow
world.Transition = container.DisownAll world.Transition = container.DisownAll
world.Stages = map [string] func () { world.Stages = map [string] func () {
"start": func () { "start": func () {
label := basic.NewLabel ( label := basicElements.NewLabel (
"you are standing next to a river.", true) "you are standing next to a river.", true)
button0 := basic.NewButton("go in the river") button0 := basicElements.NewButton("go in the river")
button0.OnClick(world.SwitchFunc("wet")) button0.OnClick(world.SwitchFunc("wet"))
button1 := basic.NewButton("walk along the river") button1 := basicElements.NewButton("walk along the river")
button1.OnClick(world.SwitchFunc("house")) button1.OnClick(world.SwitchFunc("house"))
button2 := basic.NewButton("turn around") button2 := basicElements.NewButton("turn around")
button2.OnClick(world.SwitchFunc("bear")) button2.OnClick(world.SwitchFunc("bear"))
container.Warp ( func () { container.Warp ( func () {
@@ -39,13 +39,13 @@ func run () {
}) })
}, },
"wet": func () { "wet": func () {
label := basic.NewLabel ( label := basicElements.NewLabel (
"you get completely soaked.\n" + "you get completely soaked.\n" +
"you die of hypothermia.", true) "you die of hypothermia.", true)
button0 := basic.NewButton("try again") button0 := basicElements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start")) button0.OnClick(world.SwitchFunc("start"))
button1 := basic.NewButton("exit") button1 := basicElements.NewButton("exit")
button1.OnClick(tomo.Stop) button1.OnClick(tomo.Stop)
container.Warp (func () { container.Warp (func () {
@@ -56,13 +56,13 @@ func run () {
}) })
}, },
"house": func () { "house": func () {
label := basic.NewLabel ( label := basicElements.NewLabel (
"you are standing in front of a delapidated " + "you are standing in front of a delapidated " +
"house.", true) "house.", true)
button1 := basic.NewButton("go inside") button1 := basicElements.NewButton("go inside")
button1.OnClick(world.SwitchFunc("inside")) button1.OnClick(world.SwitchFunc("inside"))
button0 := basic.NewButton("turn back") button0 := basicElements.NewButton("turn back")
button0.OnClick(world.SwitchFunc("start")) button0.OnClick(world.SwitchFunc("start"))
container.Warp (func () { container.Warp (func () {
@@ -73,14 +73,14 @@ func run () {
}) })
}, },
"inside": func () { "inside": func () {
label := basic.NewLabel ( label := basicElements.NewLabel (
"you are standing inside of the house.\n" + "you are standing inside of the house.\n" +
"it is dark, but rays of light stream " + "it is dark, but rays of light stream " +
"through the window.\n" + "through the window.\n" +
"there is nothing particularly interesting " + "there is nothing particularly interesting " +
"here.", true) "here.", true)
button0 := basic.NewButton("go back outside") button0 := basicElements.NewButton("go back outside")
button0.OnClick(world.SwitchFunc("house")) button0.OnClick(world.SwitchFunc("house"))
container.Warp (func () { container.Warp (func () {
@@ -90,13 +90,13 @@ func run () {
}) })
}, },
"bear": func () { "bear": func () {
label := basic.NewLabel ( label := basicElements.NewLabel (
"you come face to face with a bear.\n" + "you come face to face with a bear.\n" +
"it eats you (it was hungry).", true) "it eats you (it was hungry).", true)
button0 := basic.NewButton("try again") button0 := basicElements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start")) button0.OnClick(world.SwitchFunc("start"))
button1 := basic.NewButton("exit") button1 := basicElements.NewButton("exit")
button1.OnClick(tomo.Stop) button1.OnClick(tomo.Stop)
container.Warp (func () { container.Warp (func () {
+5 -5
View File
@@ -3,7 +3,7 @@ package main
import "os" import "os"
import "time" import "time"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun" import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -15,13 +15,13 @@ func main () {
func run () { func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("clock") window.SetTitle("Clock")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
clock := fun.NewAnalogClock(time.Now()) clock := fun.NewAnalogClock(time.Now())
container.Adopt(clock, true) container.Adopt(clock, true)
label := basic.NewLabel(formatTime(), false) label := basicElements.NewLabel(formatTime(), false)
container.Adopt(label, false) container.Adopt(label, false)
window.OnClose(tomo.Stop) window.OnClose(tomo.Stop)
@@ -33,7 +33,7 @@ func formatTime () (timeString string) {
return time.Now().Format("2006-01-02 15:04:05") return time.Now().Format("2006-01-02 15:04:05")
} }
func tick (label *basic.Label, clock *fun.AnalogClock) { func tick (label *basicElements.Label, clock *fun.AnalogClock) {
for { for {
tomo.Do (func () { tomo.Do (func () {
label.SetText(formatTime()) label.SetText(formatTime())
+5 -5
View File
@@ -1,7 +1,7 @@
package main package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -13,12 +13,12 @@ func run () {
window, _ := tomo.NewWindow(360, 2) window, _ := tomo.NewWindow(360, 2)
window.SetTitle("horizontal stack") window.SetTitle("horizontal stack")
container := basic.NewContainer(layouts.Horizontal { true, true }) container := basicElements.NewContainer(basicLayouts.Horizontal { true, true })
window.Adopt(container) window.Adopt(container)
container.Adopt(basic.NewLabel("this is sample text", true), true) container.Adopt(basicElements.NewLabel("this is sample text", true), true)
container.Adopt(basic.NewLabel("this is sample text", true), true) container.Adopt(basicElements.NewLabel("this is sample text", true), true)
container.Adopt(basic.NewLabel("this is sample text", true), true) container.Adopt(basicElements.NewLabel("this is sample text", true), true)
window.OnClose(tomo.Stop) window.OnClose(tomo.Stop)
window.Show() window.Show()
+58
View File
@@ -0,0 +1,58 @@
package main
import "os"
import "image"
import "bytes"
import _ "image/png"
import "github.com/jezek/xgbutil/gopher"
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("Tomo Logo")
file, err := os.Open("assets/banner.png")
if err != nil { fatalError(err); return }
logo, _, err := image.Decode(file)
file.Close()
if err != nil { fatalError(err); return }
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.Adopt(logoImage, true)
container.Adopt(button, false)
window.Adopt(container)
button.Focus()
window.OnClose(tomo.Stop)
window.Show()
}
func fatalError (err error) {
popups.NewDialog (
popups.DialogKindError,
"Error",
err.Error(),
popups.Button {
Name: "OK",
OnPress: tomo.Stop,
})
}
+8 -8
View File
@@ -2,7 +2,7 @@ package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -13,14 +13,14 @@ func main () {
func run () { func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Enter Details") window.SetTitle("Enter Details")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
// create inputs // create inputs
firstName := basic.NewTextBox("First name", "") firstName := basicElements.NewTextBox("First name", "")
lastName := basic.NewTextBox("Last name", "") lastName := basicElements.NewTextBox("Last name", "")
fingerLength := basic.NewTextBox("Length of fingers", "") fingerLength := basicElements.NewTextBox("Length of fingers", "")
button := basic.NewButton("Ok") button := basicElements.NewButton("Ok")
button.SetEnabled(false) button.SetEnabled(false)
button.OnClick (func () { button.OnClick (func () {
@@ -45,11 +45,11 @@ func run () {
fingerLength.OnChange(check) fingerLength.OnChange(check)
// add elements to container // add elements to container
container.Adopt(basic.NewLabel("Choose your words carefully.", false), true) container.Adopt(basicElements.NewLabel("Choose your words carefully.", false), true)
container.Adopt(firstName, false) container.Adopt(firstName, false)
container.Adopt(lastName, false) container.Adopt(lastName, false)
container.Adopt(fingerLength, false) container.Adopt(fingerLength, false)
container.Adopt(basic.NewSpacer(true), false) container.Adopt(basicElements.NewSpacer(true), false)
container.Adopt(button, false) container.Adopt(button, false)
window.OnClose(tomo.Stop) window.OnClose(tomo.Stop)
+1 -1
View File
@@ -11,7 +11,7 @@ func main () {
func run () { func run () {
window, _ := tomo.NewWindow(480, 2) window, _ := tomo.NewWindow(480, 2)
window.SetTitle("example label") window.SetTitle("example label")
window.Adopt(basic.NewLabel(text, true)) window.Adopt(basicElements.NewLabel(text, true))
window.OnClose(tomo.Stop) window.OnClose(tomo.Stop)
window.Show() window.Show()
} }
+21 -18
View File
@@ -2,7 +2,8 @@ package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts" 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/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing" import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -15,11 +16,11 @@ func run () {
window, _ := tomo.NewWindow(300, 2) window, _ := tomo.NewWindow(300, 2)
window.SetTitle("List Sidebar") window.SetTitle("List Sidebar")
container := basic.NewContainer(layouts.Horizontal { true, true }) container := basicElements.NewContainer(basicLayouts.Horizontal { true, true })
window.Adopt(container) window.Adopt(container)
var currentPage tomo.Element var currentPage elements.Element
turnPage := func (newPage tomo.Element) { turnPage := func (newPage elements.Element) {
container.Warp (func () { container.Warp (func () {
if currentPage != nil { if currentPage != nil {
container.Disown(currentPage) container.Disown(currentPage)
@@ -29,27 +30,29 @@ func run () {
}) })
} }
intro := basic.NewLabel ( intro := basicElements.NewLabel (
"The List element can be easily used as a sidebar. " + "The List element can be easily used as a sidebar. " +
"Click on entries to flip pages!", true) "Click on entries to flip pages!", true)
button := basic.NewButton("I do nothing!") button := basicElements.NewButton("I do nothing!")
button.OnClick (func () { button.OnClick (func () {
popups.NewDialog(popups.DialogKindInfo, "", "Sike!") popups.NewDialog(popups.DialogKindInfo, "", "Sike!")
}) })
mouse := testing.NewMouse() mouse := testing.NewMouse()
input := basic.NewTextBox("Write some text", "") input := basicElements.NewTextBox("Write some text", "")
form := basic.NewContainer(layouts.Vertical { true, false}) form := basicElements.NewContainer(basicLayouts.Vertical { true, false})
form.Adopt(basic.NewLabel("I have:", false), false) form.Adopt(basicElements.NewLabel("I have:", false), false)
form.Adopt(basic.NewSpacer(true), false) form.Adopt(basicElements.NewSpacer(true), false)
form.Adopt(basic.NewCheckbox("Skin", true), false) form.Adopt(basicElements.NewCheckbox("Skin", true), false)
form.Adopt(basic.NewCheckbox("Blood", false), false) form.Adopt(basicElements.NewCheckbox("Blood", false), false)
form.Adopt(basic.NewCheckbox("Bone", false), false) form.Adopt(basicElements.NewCheckbox("Bone", false), false)
art := testing.NewArtist()
list := basic.NewList ( list := basicElements.NewList (
basic.NewListEntry("button", func () { turnPage(button) }), basicElements.NewListEntry("button", func () { turnPage(button) }),
basic.NewListEntry("mouse", func () { turnPage(mouse) }), basicElements.NewListEntry("mouse", func () { turnPage(mouse) }),
basic.NewListEntry("input", func () { turnPage(input) }), basicElements.NewListEntry("input", func () { turnPage(input) }),
basic.NewListEntry("form", func () { turnPage(form) })) basicElements.NewListEntry("form", func () { turnPage(form) }),
basicElements.NewListEntry("art", func () { turnPage(art) }))
list.OnNoEntrySelected(func () { turnPage (intro) }) list.OnNoEntrySelected(func () { turnPage (intro) })
list.Collapse(96, 0) list.Collapse(96, 0)
+335
View File
@@ -0,0 +1,335 @@
package main
import "math"
import "time"
import "errors"
import "github.com/faiface/beep"
import "github.com/faiface/beep/speaker"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
import _ "net/http/pprof"
import "net/http"
const sampleRate = 44100
const bufferSize = 256
var tuning = music.EqualTemparment { A4: 440 }
var waveform = 0
var playing = map[music.Note] *toneStreamer { }
var adsr = ADSR {
Attack: 5 * time.Millisecond,
Decay: 400 * time.Millisecond,
Sustain: 0.7,
Release: 500 * time.Millisecond,
}
var gain = 0.3
func main () {
speaker.Init(sampleRate, bufferSize)
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Piano")
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
controlBar := basicElements.NewContainer(basicLayouts.Horizontal { true, false })
waveformColumn := basicElements.NewContainer(basicLayouts.Vertical { true, false })
waveformList := basicElements.NewList (
basicElements.NewListEntry("Sine", func(){ waveform = 0 }),
basicElements.NewListEntry("Triangle", func(){ waveform = 3 }),
basicElements.NewListEntry("Square", func(){ waveform = 1 }),
basicElements.NewListEntry("Saw", func(){ waveform = 2 }),
basicElements.NewListEntry("Supersaw", func(){ waveform = 4 }),
)
waveformList.OnNoEntrySelected (func(){waveformList.Select(0)})
waveformList.Select(0)
adsrColumn := basicElements.NewContainer(basicLayouts.Vertical { true, false })
adsrGroup := basicElements.NewContainer(basicLayouts.Horizontal { true, false })
attackSlider := basicElements.NewLerpSlider(0, 3 * time.Second, adsr.Attack, true)
decaySlider := basicElements.NewLerpSlider(0, 3 * time.Second, adsr.Decay, true)
sustainSlider := basicElements.NewSlider(adsr.Sustain, true)
releaseSlider := basicElements.NewLerpSlider(0, 3 * time.Second, adsr.Release, true)
gainSlider := basicElements.NewSlider(math.Sqrt(gain), false)
attackSlider.OnRelease (func () {
adsr.Attack = attackSlider.Value()
})
decaySlider.OnRelease (func () {
adsr.Decay = decaySlider.Value()
})
sustainSlider.OnRelease (func () {
adsr.Sustain = sustainSlider.Value()
})
releaseSlider.OnRelease (func () {
adsr.Release = releaseSlider.Value()
})
gainSlider.OnRelease (func () {
gain = math.Pow(gainSlider.Value(), 2)
})
patchColumn := basicElements.NewContainer(basicLayouts.Vertical { true, false })
patch := func (w int, a, d time.Duration, s float64, r time.Duration) func () {
return func () {
waveform = w
adsr = ADSR {
a * time.Millisecond,
d * time.Millisecond,
s,
r * time.Millisecond,
}
waveformList.Select(w)
attackSlider .SetValue(adsr.Attack)
decaySlider .SetValue(adsr.Decay)
sustainSlider.SetValue(adsr.Sustain)
releaseSlider.SetValue(adsr.Release)
}
}
patchList := basicElements.NewList (
basicElements.NewListEntry ("Bones", patch (
0, 0, 100, 0.0, 0)),
basicElements.NewListEntry ("Staccato", patch (
4, 70, 500, 0, 0)),
basicElements.NewListEntry ("Sustain", patch (
4, 70, 200, 0.8, 500)),
basicElements.NewListEntry ("Upright", patch (
1, 0, 500, 0.4, 70)),
basicElements.NewListEntry ("Space Pad", patch (
4, 1500, 0, 1.0, 3000)),
basicElements.NewListEntry ("Popcorn", patch (
2, 0, 40, 0.0, 0)),
basicElements.NewListEntry ("Racer", patch (
3, 70, 0, 0.7, 400)),
basicElements.NewListEntry ("Reverse", patch (
2, 3000, 60, 0, 0)),
)
patchList.Collapse(0, 32)
patchScrollBox := basicElements.NewScrollContainer(false, true)
piano := fun.NewPiano(2, 5)
piano.OnPress(playNote)
piano.OnRelease(stopNote)
// honestly, if you were doing something like this for real, i'd
// encourage you to build a custom layout because this is a bit cursed.
// i need to add more layouts...
window.Adopt(container)
controlBar.Adopt(patchColumn, true)
patchColumn.Adopt(basicElements.NewLabel("Presets", false), false)
patchColumn.Adopt(patchScrollBox, true)
patchScrollBox.Adopt(patchList)
controlBar.Adopt(basicElements.NewSpacer(true), false)
controlBar.Adopt(waveformColumn, false)
waveformColumn.Adopt(basicElements.NewLabel("Waveform", false), false)
waveformColumn.Adopt(waveformList, true)
controlBar.Adopt(basicElements.NewSpacer(true), false)
adsrColumn.Adopt(basicElements.NewLabel("ADSR", false), false)
adsrGroup.Adopt(attackSlider, false)
adsrGroup.Adopt(decaySlider, false)
adsrGroup.Adopt(sustainSlider, false)
adsrGroup.Adopt(releaseSlider, false)
adsrColumn.Adopt(adsrGroup, true)
adsrColumn.Adopt(gainSlider, false)
controlBar.Adopt(adsrColumn, false)
container.Adopt(controlBar, true)
container.Adopt(piano, false)
piano.Focus()
window.OnClose(tomo.Stop)
window.Show()
go func () {
http.ListenAndServe("localhost:6060", nil)
} ()
}
type Patch struct {
ADSR
Waveform int
}
func stopNote (note music.Note) {
if _, is := playing[note]; !is { return }
speaker.Lock()
playing[note].Release()
delete(playing, note)
speaker.Unlock()
}
func playNote (note music.Note) {
streamer, _ := Tone (
sampleRate,
int(tuning.Tune(note)),
waveform,
gain,
adsr)
stopNote(note)
speaker.Lock()
playing[note] = streamer
speaker.Unlock()
speaker.Play(playing[note])
}
// https://github.com/faiface/beep/blob/v1.1.0/generators/toner.go
// Adapted to be a bit more versatile.
type toneStreamer struct {
position float64
cycles uint64
delta float64
waveform int
gain float64
adsr ADSR
released bool
complete bool
adsrPhase int
adsrPosition float64
adsrDeltas [4]float64
}
type ADSR struct {
Attack time.Duration
Decay time.Duration
Sustain float64
Release time.Duration
}
func Tone (
sampleRate beep.SampleRate,
frequency int,
waveform int,
gain float64,
adsr ADSR,
) (
*toneStreamer,
error,
) {
if int(sampleRate) / frequency < 2 {
return nil, errors.New (
"tone generator: samplerate must be at least " +
"2 times greater then frequency")
}
tone := new(toneStreamer)
tone.waveform = waveform
tone.position = 0.0
steps := float64(sampleRate) / float64(frequency)
tone.delta = 1.0 / steps
tone.gain = gain
if adsr.Attack < time.Millisecond { adsr.Attack = time.Millisecond }
if adsr.Decay < time.Millisecond { adsr.Decay = time.Millisecond }
if adsr.Release < time.Millisecond { adsr.Release = time.Millisecond }
tone.adsr = adsr
attackSteps := adsr.Attack.Seconds() * float64(sampleRate)
decaySteps := adsr.Decay.Seconds() * float64(sampleRate)
releaseSteps := adsr.Release.Seconds() * float64(sampleRate)
tone.adsrDeltas[0] = 1 / attackSteps
tone.adsrDeltas[1] = 1 / decaySteps
tone.adsrDeltas[2] = 0
tone.adsrDeltas[3] = 1 / releaseSteps
return tone, nil
}
func (tone *toneStreamer) nextSample () (sample float64) {
switch tone.waveform {
case 0:
sample = math.Sin(tone.position * 2.0 * math.Pi)
case 1:
if tone.position > 0.5 {
sample = 1
} else {
sample = -1
}
case 2:
sample = (tone.position - 0.5) * 2
case 3:
sample = 1 - math.Abs(tone.position - 0.5) * 4
case 4:
unison := 5
detuneDelta := 0.00005
detune := 0.0 - (float64(unison) / 2) * detuneDelta
for i := 0; i < unison; i ++ {
_, offset := math.Modf(detune * float64(tone.cycles) + tone.position)
sample += (offset - 0.5) * 2
detune += detuneDelta
}
sample /= float64(unison)
}
adsrGain := 0.0
switch tone.adsrPhase {
case 0: adsrGain = tone.adsrPosition
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.adsrPhase = 1
}
case 1: adsrGain = 1 + tone.adsrPosition * (tone.adsr.Sustain - 1)
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.adsrPhase = 2
}
case 2: adsrGain = tone.adsr.Sustain
if tone.released {
tone.adsrPhase = 3
}
case 3: adsrGain = (1 - tone.adsrPosition) * tone.adsr.Sustain
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.complete = true
}
}
sample *= adsrGain * adsrGain
tone.adsrPosition += tone.adsrDeltas[tone.adsrPhase]
_, tone.position = math.Modf(tone.position + tone.delta)
tone.cycles ++
return
}
func (tone *toneStreamer) Stream (buf [][2]float64) (int, bool) {
if tone.complete {
return 0, false
}
for i := 0; i < len(buf); i++ {
sample := 0.0
if !tone.complete {
sample = tone.nextSample() * tone.gain
}
buf[i] = [2]float64{sample, sample}
}
return len(buf), true
}
func (tone *toneStreamer) Err () error {
return nil
}
func (tone *toneStreamer) Release () {
tone.released = true
}
+10 -10
View File
@@ -2,7 +2,7 @@ package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -14,12 +14,12 @@ func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Dialog Boxes") window.SetTitle("Dialog Boxes")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
container.Adopt(basic.NewLabel("Try out different dialogs:", false), true) container.Adopt(basicElements.NewLabel("Try out different dialogs:", false), true)
infoButton := basic.NewButton("popups.DialogKindInfo") infoButton := basicElements.NewButton("popups.DialogKindInfo")
infoButton.OnClick (func () { infoButton.OnClick (func () {
popups.NewDialog ( popups.NewDialog (
popups.DialogKindInfo, popups.DialogKindInfo,
@@ -29,7 +29,7 @@ func run () {
container.Adopt(infoButton, false) container.Adopt(infoButton, false)
infoButton.Focus() infoButton.Focus()
questionButton := basic.NewButton("popups.DialogKindQuestion") questionButton := basicElements.NewButton("popups.DialogKindQuestion")
questionButton.OnClick (func () { questionButton.OnClick (func () {
popups.NewDialog ( popups.NewDialog (
popups.DialogKindQuestion, popups.DialogKindQuestion,
@@ -41,25 +41,25 @@ func run () {
}) })
container.Adopt(questionButton, false) container.Adopt(questionButton, false)
warningButton := basic.NewButton("popups.DialogKindWarning") warningButton := basicElements.NewButton("popups.DialogKindWarning")
warningButton.OnClick (func () { warningButton.OnClick (func () {
popups.NewDialog ( popups.NewDialog (
popups.DialogKindQuestion, popups.DialogKindWarning,
"Warning", "Warning",
"They are fast approaching.") "They are fast approaching.")
}) })
container.Adopt(warningButton, false) container.Adopt(warningButton, false)
errorButton := basic.NewButton("popups.DialogKindError") errorButton := basicElements.NewButton("popups.DialogKindError")
errorButton.OnClick (func () { errorButton.OnClick (func () {
popups.NewDialog ( popups.NewDialog (
popups.DialogKindQuestion, popups.DialogKindError,
"Error", "Error",
"There is nowhere left to go.") "There is nowhere left to go.")
}) })
container.Adopt(errorButton, false) container.Adopt(errorButton, false)
cancelButton := basic.NewButton("No thank you.") cancelButton := basicElements.NewButton("No thank you.")
cancelButton.OnClick(tomo.Stop) cancelButton.OnClick(tomo.Stop)
container.Adopt(cancelButton, false) container.Adopt(cancelButton, false)
+6 -6
View File
@@ -3,7 +3,7 @@ package main
import "time" import "time"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -14,14 +14,14 @@ func main () {
func run () { func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Approaching") window.SetTitle("Approaching")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
container.Adopt (basic.NewLabel ( container.Adopt (basicElements.NewLabel (
"Rapidly approaching your location...", false), false) "Rapidly approaching your location...", false), false)
bar := basic.NewProgressBar(0) bar := basicElements.NewProgressBar(0)
container.Adopt(bar, false) container.Adopt(bar, false)
button := basic.NewButton("Stop") button := basicElements.NewButton("Stop")
button.SetEnabled(false) button.SetEnabled(false)
container.Adopt(button, false) container.Adopt(button, false)
@@ -30,7 +30,7 @@ func run () {
go fill(bar) go fill(bar)
} }
func fill (bar *basic.ProgressBar) { func fill (bar *basicElements.ProgressBar) {
for progress := 0.0; progress < 1.0; progress += 0.01 { for progress := 0.0; progress < 1.0; progress += 0.01 {
time.Sleep(time.Second / 24) time.Sleep(time.Second / 24)
tomo.Do (func () { tomo.Do (func () {
Binary file not shown.
+122
View File
@@ -0,0 +1,122 @@
package main
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
type Game struct {
*Raycaster
running bool
tickChan <- chan time.Time
stopChan chan bool
stamina float64
health float64
controlState ControlState
onStatUpdate func ()
}
func NewGame (world World, textures Textures) (game *Game) {
game = &Game {
Raycaster: NewRaycaster(world, textures),
stopChan: make(chan bool),
}
game.Raycaster.OnControlStateChange (func (state ControlState) {
game.controlState = state
})
game.stamina = 0.5
game.health = 1
return
}
func (game *Game) DrawTo (canvas canvas.Canvas) {
if canvas == nil {
game.stopChan <- true
} else if !game.running {
game.running = true
go game.run()
}
game.Raycaster.DrawTo(canvas)
}
func (game *Game) Stamina () float64 {
return game.stamina
}
func (game *Game) Health () float64 {
return game.health
}
func (game *Game) OnStatUpdate (callback func ()) {
game.onStatUpdate = callback
}
func (game *Game) tick () {
moved := false
statUpdate := false
speed := 0.07
if game.controlState.Sprint {
speed = 0.16
}
if game.stamina <= 0 {
speed = 0
}
if game.controlState.WalkForward {
game.Walk(speed)
moved = true
}
if game.controlState.WalkBackward {
game.Walk(-speed)
moved = true
}
if game.controlState.StrafeLeft {
game.Strafe(-speed)
moved = true
}
if game.controlState.StrafeRight {
game.Strafe(speed)
moved = true
}
if game.controlState.LookLeft {
game.Rotate(-0.1)
}
if game.controlState.LookRight {
game.Rotate(0.1)
}
if moved {
game.stamina -= speed / 50
statUpdate = true
} else if game.stamina < 1 {
game.stamina += 0.005
statUpdate = true
}
if game.stamina > 1 {
game.stamina = 1
}
if game.stamina < 0 {
game.stamina = 0
}
tomo.Do(game.Draw)
if statUpdate && game.onStatUpdate != nil {
tomo.Do(game.onStatUpdate)
}
}
func (game *Game) run () {
ticker := time.NewTicker(time.Second / 30)
game.tickChan = ticker.C
for game.running {
select {
case <- game.tickChan:
game.tick()
case <- game.stopChan:
ticker.Stop()
}
}
}
+75
View File
@@ -0,0 +1,75 @@
package main
import "bytes"
import _ "embed"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
//go:embed wall.png
var wallTextureBytes []uint8
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(640, 480)
window.SetTitle("Raycaster")
container := basicElements.NewContainer(basicLayouts.Vertical { false, false })
window.Adopt(container)
wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes))
game := NewGame (World {
Data: []int {
1,1,1,1,1,1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,1,1,1,1,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,1,0,1,
1,0,0,0,0,0,0,0,1,0,0,0,1,
1,0,0,0,0,0,0,0,1,0,1,1,1,
1,1,1,1,1,1,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,0,1,1,
1,0,0,1,0,0,0,0,0,0,0,0,1,
1,0,1,1,1,0,0,0,0,0,0,0,1,
1,0,0,1,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,1,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,
},
Stride: 13,
}, Textures {
wallTexture,
})
topBar := basicElements.NewContainer(basicLayouts.Horizontal { true, true })
staminaBar := basicElements.NewProgressBar(game.Stamina())
healthBar := basicElements.NewProgressBar(game.Health())
topBar.Adopt(basicElements.NewLabel("Stamina:", false), false)
topBar.Adopt(staminaBar, true)
topBar.Adopt(basicElements.NewLabel("Health:", false), false)
topBar.Adopt(healthBar, true)
container.Adopt(topBar, false)
container.Adopt(game, true)
game.Focus()
game.OnStatUpdate (func () {
staminaBar.SetProgress(game.Stamina())
})
window.OnClose(tomo.Stop)
window.Show()
popups.NewDialog (
popups.DialogKindInfo,
"Welcome to the backrooms",
"You've no-clipped into the backrooms!\n" +
"Move with WASD, and look with the arrow keys.\n" +
"Keep an eye on your health and stamina.")
}
+193
View File
@@ -0,0 +1,193 @@
package main
import "math"
import "image"
type World struct {
Data []int
Stride int
}
func (world World) At (position image.Point) int {
if position.X < 0 { return 0 }
if position.Y < 0 { return 0 }
if position.X >= world.Stride { return 0 }
index := position.X + position.Y * world.Stride
if index >= len(world.Data) { return 0 }
return world.Data[index]
}
type Vector struct {
X, Y float64
}
func (vector Vector) Point () (image.Point) {
return image.Pt(int(vector.X), int(vector.Y))
}
func (vector Vector) Add (other Vector) Vector {
return Vector {
vector.X + other.X,
vector.Y + other.Y,
}
}
func (vector Vector) Sub (other Vector) Vector {
return Vector {
vector.X - other.X,
vector.Y - other.Y,
}
}
func (vector Vector) Mul (by float64) Vector {
return Vector {
vector.X * by,
vector.Y * by,
}
}
func (vector Vector) Hypot () float64 {
return math.Hypot(vector.X, vector.Y)
}
type Camera struct {
Vector
Angle float64
Fov float64
}
func (camera *Camera) Rotate (by float64) {
camera.Angle += by
if camera.Angle < 0 { camera.Angle += math.Pi * 2 }
if camera.Angle > math.Pi * 2 { camera.Angle = 0 }
}
func (camera *Camera) Walk (by float64) {
delta := camera.Delta()
camera.X += delta.X * by
camera.Y += delta.Y * by
}
func (camera *Camera) Strafe (by float64) {
delta := camera.OffsetDelta()
camera.X += delta.X * by
camera.Y += delta.Y * by
}
func (camera *Camera) Delta () Vector {
return Vector {
math.Cos(camera.Angle),
math.Sin(camera.Angle),
}
}
func (camera *Camera) OffsetDelta () Vector {
offset := math.Pi / 2
return Vector {
math.Cos(camera.Angle + offset),
math.Sin(camera.Angle + offset),
}
}
type Ray struct {
Vector
Angle float64
}
func (ray *Ray) Cast (
world World,
max int,
) (
distance float64,
hit Vector,
wall int,
horizontal bool,
) {
// return ray.castV(world, max)
cellAt := world.At(ray.Point())
if cellAt > 0 {
return 0, Vector { }, cellAt, false
}
hDistance, hPos, hWall := ray.castH(world, max)
vDistance, vPos, vWall := ray.castV(world, max)
if hDistance < vDistance {
return hDistance, hPos, hWall, true
} else {
return vDistance, vPos, vWall, false
}
}
func (ray *Ray) castH (world World, max int) (distance float64, hit Vector, wall int) {
var position Vector
var delta Vector
var offset Vector
ray.Angle = math.Mod(ray.Angle, math.Pi * 2)
if ray.Angle < 0 {
ray.Angle += math.Pi * 2
}
tan := math.Tan(math.Pi - ray.Angle)
if ray.Angle > math.Pi {
// facing up
position.Y = math.Floor(ray.Y)
delta.Y = -1
offset.Y = -1
} else if ray.Angle < math.Pi {
// facing down
position.Y = math.Floor(ray.Y) + 1
delta.Y = 1
} else {
// facing straight left or right
return float64(max), Vector { }, 0
}
position.X = ray.X + (ray.Y - position.Y) / tan
delta.X = -delta.Y / tan
// cast da ray
steps := 0
for {
cell := world.At(position.Add(offset).Point())
if cell > 0 || steps > max { break }
position = position.Add(delta)
steps ++
}
return position.Sub(ray.Vector).Hypot(),
position,
world.At(position.Add(offset).Point())
}
func (ray *Ray) castV (world World, max int) (distance float64, hit Vector, wall int) {
var position Vector
var delta Vector
var offset Vector
tan := math.Tan(math.Pi - ray.Angle)
offsetAngle := math.Mod(ray.Angle + math.Pi / 2, math.Pi * 2)
if offsetAngle > math.Pi {
// facing left
position.X = math.Floor(ray.X)
delta.X = -1
offset.X = -1
} else if offsetAngle < math.Pi {
// facing right
position.X = math.Floor(ray.X) + 1
delta.X = 1
} else {
// facing straight left or right
return float64(max), Vector { }, 0
}
position.Y = ray.Y + (ray.X - position.X) * tan
delta.Y = -delta.X * tan
// cast da ray
steps := 0
for {
cell := world.At(position.Add(offset).Point())
if cell > 0 || steps > max { break }
position = position.Add(delta)
steps ++
}
return position.Sub(ray.Vector).Hypot(),
position,
world.At(position.Add(offset).Point())
}
+235
View File
@@ -0,0 +1,235 @@
package main
// import "fmt"
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/input"
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"
type ControlState struct {
WalkForward bool
WalkBackward bool
StrafeLeft bool
StrafeRight bool
LookLeft bool
LookRight bool
Sprint bool
}
type Raycaster struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
config config.Wrapped
Camera
controlState ControlState
world World
textures Textures
onControlStateChange func (ControlState)
renderDistance int
}
func NewRaycaster (world World, textures Textures) (element *Raycaster) {
element = &Raycaster {
Camera: Camera {
Vector: Vector {
X: 1,
Y: 1,
},
Angle: math.Pi / 3,
Fov: 1,
},
world: world,
textures: textures,
renderDistance: 8,
}
element.Core, element.core = core.NewCore(element.drawAll)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.Draw)
element.core.SetMinimumSize(64, 64)
return
}
func (element *Raycaster) OnControlStateChange (callback func (ControlState)) {
element.onControlStateChange = callback
}
func (element *Raycaster) Draw () {
if element.core.HasImage() {
element.drawAll()
element.core.DamageAll()
}
}
func (element *Raycaster) HandleMouseDown (x, y int, button input.Button) {
if !element.Focused() { element.Focus() }
}
func (element *Raycaster) HandleMouseUp (x, y int, button input.Button) { }
func (element *Raycaster) HandleMouseMove (x, y int) { }
func (element *Raycaster) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Raycaster) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
switch key {
case input.KeyLeft: element.controlState.LookLeft = true
case input.KeyRight: element.controlState.LookRight = true
case 'a', 'A': element.controlState.StrafeLeft = true
case 'd', 'D': element.controlState.StrafeRight = true
case 'w', 'W': element.controlState.WalkForward = true
case 's', 'S': element.controlState.WalkBackward = true
case input.KeyLeftControl: element.controlState.Sprint = true
default: return
}
if element.onControlStateChange != nil {
element.onControlStateChange(element.controlState)
}
}
func (element *Raycaster) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
switch key {
case input.KeyLeft: element.controlState.LookLeft = false
case input.KeyRight: element.controlState.LookRight = false
case 'a', 'A': element.controlState.StrafeLeft = false
case 'd', 'D': element.controlState.StrafeRight = false
case 'w', 'W': element.controlState.WalkForward = false
case 's', 'S': element.controlState.WalkBackward = false
case input.KeyLeftControl: element.controlState.Sprint = false
default: return
}
if element.onControlStateChange != nil {
element.onControlStateChange(element.controlState)
}
}
func (element *Raycaster) drawAll () {
bounds := element.Bounds()
// artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds)
width := bounds.Dx()
height := bounds.Dy()
halfway := bounds.Max.Y - height / 2
ray := Ray { Angle: element.Camera.Angle - element.Camera.Fov / 2 }
for x := 0; x < width; x ++ {
ray.X = element.Camera.X
ray.Y = element.Camera.Y
distance, hitPoint, wall, horizontal := ray.Cast (
element.world, element.renderDistance)
distance *= math.Cos(ray.Angle - element.Camera.Angle)
textureX := math.Mod(hitPoint.X + hitPoint.Y, 1)
if textureX < 0 { textureX += 1 }
wallHeight := height
if distance > 0 {
wallHeight = int((float64(height) / 2.0) / float64(distance))
}
shade := 1.0
if horizontal {
shade *= 0.8
}
shade *= 1 - distance / float64(element.renderDistance)
if shade < 0 { shade = 0 }
ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF }
floorColor := color.RGBA { 0x39, 0x49, 0x25, 0xFF }
// draw
data, stride := element.core.Buffer()
wallStart := halfway - wallHeight
wallEnd := halfway + wallHeight
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
switch {
case y < wallStart:
data[y * stride + x + bounds.Min.X] = ceilingColor
case y < wallEnd:
textureY :=
float64(y - halfway) /
float64(wallEnd - wallStart) + 0.5
// fmt.Println(textureY)
wallColor := element.textures.At (wall, Vector {
textureX,
textureY,
})
wallColor = shadeColor(wallColor, shade)
data[y * stride + x + bounds.Min.X] = wallColor
default:
data[y * stride + x + bounds.Min.X] = floorColor
}
}
// increment angle
ray.Angle += element.Camera.Fov / float64(width)
}
// element.drawMinimap()
}
func shadeColor (c color.RGBA, brightness float64) color.RGBA {
return color.RGBA {
uint8(float64(c.R) * brightness),
uint8(float64(c.G) * brightness),
uint8(float64(c.B) * brightness),
c.A,
}
}
func (element *Raycaster) drawMinimap () {
bounds := element.Bounds()
scale := 8
for y := 0; y < len(element.world.Data) / element.world.Stride; y ++ {
for x := 0; x < element.world.Stride; x ++ {
cellPt := image.Pt(x, y)
cell := element.world.At(cellPt)
cellBounds :=
image.Rectangle {
cellPt.Mul(scale),
cellPt.Add(image.Pt(1, 1)).Mul(scale),
}.Add(bounds.Min)
cellColor := color.RGBA { 0x22, 0x22, 0x22, 0xFF }
if cell > 0 {
cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF }
}
shapes.FillColorRectangle (
element.core,
cellColor,
cellBounds.Inset(1))
}}
playerPt := element.Camera.Mul(float64(scale)).Point().Add(bounds.Min)
playerAnglePt :=
element.Camera.Add(element.Camera.Delta()).
Mul(float64(scale)).Point().Add(bounds.Min)
ray := Ray { Vector: element.Camera.Vector, Angle: element.Camera.Angle }
_, hit, _, _ := ray.Cast(element.world, 8)
hitPt := hit.Mul(float64(scale)).Point().Add(bounds.Min)
playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8)
shapes.FillColorEllipse (
element.core,
artist.Hex(0xFFFFFFFF),
playerBounds)
shapes.ColorLine (
element.core,
artist.Hex(0xFFFFFFFF), 1,
playerPt,
playerAnglePt)
shapes.ColorLine (
element.core,
artist.Hex(0x00FF00FF), 1,
playerPt,
hitPt)
}
+48
View File
@@ -0,0 +1,48 @@
package main
import "io"
import "image"
import "image/color"
type Textures []Texture
type Texture struct {
Data []color.RGBA
Stride int
}
func (texture Textures) At (wall int, offset Vector) color.RGBA {
wall --
if wall < 0 || wall >= len(texture) { return color.RGBA { } }
image := texture[wall]
xOffset := int(offset.X * float64(image.Stride))
yOffset := int(offset.Y * float64(len(image.Data) / image.Stride))
index := xOffset + yOffset * image.Stride
if index < 0 { return color.RGBA { } }
if index >= len(image.Data) { return color.RGBA { } }
return image.Data[index]
}
func TextureFrom (source io.Reader) (texture Texture, err error) {
sourceImage, _, err := image.Decode(source)
if err != nil { return }
bounds := sourceImage.Bounds()
texture.Stride = bounds.Dx()
texture.Data = make([]color.RGBA, bounds.Dx() * bounds.Dy())
index := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
r, g, b, a := sourceImage.At(x, y).RGBA()
texture.Data[index] = color.RGBA {
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: uint8(a >> 8),
}
index ++
}}
return texture, nil
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+5 -5
View File
@@ -1,7 +1,7 @@
package main package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -12,13 +12,13 @@ func main () {
func run () { func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Scroll") window.SetTitle("Scroll")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
container.Adopt(basic.NewLabel("look at this non sense", false), false) container.Adopt(basicElements.NewLabel("look at this non sense", false), false)
textBox := basic.NewTextBox("", "sample text sample text") textBox := basicElements.NewTextBox("", "sample text sample text")
scrollContainer := basic.NewScrollContainer(true, false) scrollContainer := basicElements.NewScrollContainer(true, false)
scrollContainer.Adopt(textBox) scrollContainer.Adopt(textBox)
container.Adopt(scrollContainer, true) container.Adopt(scrollContainer, true)
+7 -7
View File
@@ -1,7 +1,7 @@
package main package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -13,14 +13,14 @@ func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Spaced Out") window.SetTitle("Spaced Out")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
container.Adopt (basic.NewLabel("This is at the top", false), false) container.Adopt (basicElements.NewLabel("This is at the top", false), false)
container.Adopt (basic.NewSpacer(true), false) container.Adopt (basicElements.NewSpacer(true), false)
container.Adopt (basic.NewLabel("This is in the middle", false), false) container.Adopt (basicElements.NewLabel("This is in the middle", false), false)
container.Adopt (basic.NewSpacer(false), true) container.Adopt (basicElements.NewSpacer(false), true)
container.Adopt (basic.NewLabel("This is at the bottom", false), false) container.Adopt (basicElements.NewLabel("This is at the bottom", false), false)
window.OnClose(tomo.Stop) window.OnClose(tomo.Stop)
window.Show() window.Show()
+5 -5
View File
@@ -1,7 +1,7 @@
package main package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -13,12 +13,12 @@ func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Switches") window.SetTitle("Switches")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
container.Adopt(basic.NewSwitch("hahahah", false), false) container.Adopt(basicElements.NewSwitch("hahahah", false), false)
container.Adopt(basic.NewSwitch("hehehehheheh", false), false) container.Adopt(basicElements.NewSwitch("hehehehheheh", false), false)
container.Adopt(basic.NewSwitch("you can flick da swicth", false), false) container.Adopt(basicElements.NewSwitch("you can flick da swicth", false), false)
window.OnClose(tomo.Stop) window.OnClose(tomo.Stop)
window.Show() window.Show()
+6 -6
View File
@@ -1,7 +1,7 @@
package main package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing" import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
@@ -14,15 +14,15 @@ func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(2, 2)
window.SetTitle("vertical stack") window.SetTitle("vertical stack")
container := basic.NewContainer(layouts.Vertical { true, true }) container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
label := basic.NewLabel("it is a label hehe", true) label := basicElements.NewLabel("it is a label hehe", true)
button := basic.NewButton("drawing pad") button := basicElements.NewButton("drawing pad")
okButton := basic.NewButton("OK") okButton := basicElements.NewButton("OK")
button.OnClick (func () { button.OnClick (func () {
container.DisownAll() container.DisownAll()
container.Adopt(basic.NewLabel("Draw here:", false), false) container.Adopt(basicElements.NewLabel("Draw here:", false), false)
container.Adopt(testing.NewMouse(), true) container.Adopt(testing.NewMouse(), true)
container.Adopt(okButton, false) container.Adopt(okButton, false)
okButton.Focus() okButton.Focus()
+26
View File
@@ -0,0 +1,26 @@
// Package fixedutil contains functions that make working with fixed precision
// values easier.
package fixedutil
import "image"
import "golang.org/x/image/math/fixed"
// Pt creates a fixed point from a regular point.
func Pt (point image.Point) fixed.Point26_6 {
return fixed.P(point.X, point.Y)
}
// RoundPt rounds a fixed point into a regular point.
func RoundPt (point fixed.Point26_6) image.Point {
return image.Pt(point.X.Round(), point.Y.Round())
}
// FloorPt creates a regular point from the floor of a fixed point.
func FloorPt (point fixed.Point26_6) image.Point {
return image.Pt(point.X.Floor(),point.Y.Floor())
}
// CeilPt creates a regular point from the ceiling of a fixed point.
func CeilPt (point fixed.Point26_6) image.Point {
return image.Pt(point.X.Ceil(),point.Y.Ceil())
}
+9
View File
@@ -3,10 +3,19 @@ module git.tebibyte.media/sashakoshka/tomo
go 1.19 go 1.19
require ( require (
github.com/faiface/beep v1.1.0
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66
golang.org/x/image v0.3.0 golang.org/x/image v0.3.0
) )
require (
github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
)
require ( require (
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect
+35
View File
@@ -2,25 +2,60 @@ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJ
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 h1:+wPhoJD8EH0/bXipIq8Lc2z477jfox9zkXPCJdhvHj8= github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 h1:+wPhoJD8EH0/bXipIq8Lc2z477jfox9zkXPCJdhvHj8=
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66/go.mod h1:KACeV+k6b+aoLTVrrurywEbu3UpqoQcQywj4qX8aQKM= github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66/go.mod h1:KACeV+k6b+aoLTVrrurywEbu3UpqoQcQywj4qX8aQKM=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg= golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+20 -1
View File
@@ -1,4 +1,4 @@
package tomo package input
import "unicode" import "unicode"
@@ -110,3 +110,22 @@ type Modifiers struct {
NumberPad bool NumberPad bool
} }
// KeynavDirection represents a keyboard navigation direction.
type KeynavDirection int
const (
KeynavDirectionNeutral KeynavDirection = 0
KeynavDirectionBackward KeynavDirection = -1
KeynavDirectionForward KeynavDirection = 1
)
// Canon returns a well-formed direction.
func (direction KeynavDirection) Canon () (canon KeynavDirection) {
if direction > 0 {
return KeynavDirectionForward
} else if direction == 0 {
return KeynavDirectionNeutral
} else {
return KeynavDirectionBackward
}
}
+32 -22
View File
@@ -1,8 +1,8 @@
package layouts package basicLayouts
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/elements"
// Dialog arranges elements in the form of a dialog box. The first element is // Dialog arranges elements in the form of a dialog box. The first element is
// positioned above as the main focus of the dialog, and is set to expand // positioned above as the main focus of the dialog, and is set to expand
@@ -19,13 +19,18 @@ type Dialog struct {
} }
// Arrange arranges a list of entries into a dialog. // Arrange arranges a list of entries into a dialog.
func (layout Dialog) Arrange (entries []tomo.LayoutEntry, bounds image.Rectangle) { func (layout Dialog) Arrange (
if layout.Pad { bounds = bounds.Inset(theme.Margin()) } entries []layouts.LayoutEntry,
margin int,
bounds image.Rectangle,
) {
if layout.Pad { bounds = bounds.Inset(margin) }
controlRowWidth, controlRowHeight := 0, 0 controlRowWidth, controlRowHeight := 0, 0
if len(entries) > 1 { if len(entries) > 1 {
controlRowWidth, controlRowWidth,
controlRowHeight = layout.minimumSizeOfControlRow(entries[1:]) controlRowHeight = layout.minimumSizeOfControlRow (
entries[1:], margin)
} }
if len(entries) > 0 { if len(entries) > 0 {
@@ -33,7 +38,7 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, bounds image.Rectangle
main.Bounds.Min = bounds.Min main.Bounds.Min = bounds.Min
mainHeight := bounds.Dy() - controlRowHeight mainHeight := bounds.Dy() - controlRowHeight
if layout.Gap { if layout.Gap {
mainHeight -= theme.Margin() mainHeight -= margin
} }
main.Bounds.Max = main.Bounds.Min.Add(image.Pt(bounds.Dx(), mainHeight)) main.Bounds.Max = main.Bounds.Min.Add(image.Pt(bounds.Dx(), mainHeight))
entries[0] = main entries[0] = main
@@ -53,7 +58,7 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, bounds image.Rectangle
freeSpace -= entryMinWidth freeSpace -= entryMinWidth
} }
if index > 0 && layout.Gap { if index > 0 && layout.Gap {
freeSpace -= theme.Margin() freeSpace -= margin
} }
} }
expandingElementWidth := 0 expandingElementWidth := 0
@@ -69,7 +74,7 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, bounds image.Rectangle
// set the size and position of each element in the control row // set the size and position of each element in the control row
for index, entry := range entries[1:] { for index, entry := range entries[1:] {
if index > 0 && layout.Gap { dot.X += theme.Margin() } if index > 0 && layout.Gap { dot.X += margin }
entry.Bounds.Min = dot entry.Bounds.Min = dot
entryWidth := 0 entryWidth := 0
@@ -95,7 +100,8 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, bounds image.Rectangle
// MinimumSize returns the minimum width and height that will be needed to // MinimumSize returns the minimum width and height that will be needed to
// arrange the given list of entries. // arrange the given list of entries.
func (layout Dialog) MinimumSize ( func (layout Dialog) MinimumSize (
entries []tomo.LayoutEntry, entries []layouts.LayoutEntry,
margin int,
) ( ) (
width, height int, width, height int,
) { ) {
@@ -106,9 +112,10 @@ func (layout Dialog) MinimumSize (
} }
if len(entries) > 1 { if len(entries) > 1 {
if layout.Gap { height += theme.Margin() } if layout.Gap { height += margin }
additionalWidth, additionalWidth,
additionalHeight := layout.minimumSizeOfControlRow(entries[1:]) additionalHeight := layout.minimumSizeOfControlRow (
entries[1:], margin)
height += additionalHeight height += additionalHeight
if additionalWidth > width { if additionalWidth > width {
width = additionalWidth width = additionalWidth
@@ -116,8 +123,8 @@ func (layout Dialog) MinimumSize (
} }
if layout.Pad { if layout.Pad {
width += theme.Margin() * 2 width += margin * 2
height += theme.Margin() * 2 height += margin * 2
} }
return return
} }
@@ -125,18 +132,19 @@ func (layout Dialog) MinimumSize (
// FlexibleHeightFor Returns the minimum height the layout needs to lay out the // FlexibleHeightFor Returns the minimum height the layout needs to lay out the
// specified elements at the given width, taking into account flexible elements. // specified elements at the given width, taking into account flexible elements.
func (layout Dialog) FlexibleHeightFor ( func (layout Dialog) FlexibleHeightFor (
entries []tomo.LayoutEntry, entries []layouts.LayoutEntry,
margin int,
width int, width int,
) ( ) (
height int, height int,
) { ) {
if layout.Pad { if layout.Pad {
width -= theme.Margin() * 2 width -= margin * 2
} }
if len(entries) > 0 { if len(entries) > 0 {
mainChildHeight := 0 mainChildHeight := 0
if child, flexible := entries[0].Element.(tomo.Flexible); flexible { if child, flexible := entries[0].Element.(elements.Flexible); flexible {
mainChildHeight = child.FlexibleHeightFor(width) mainChildHeight = child.FlexibleHeightFor(width)
} else { } else {
_, mainChildHeight = entries[0].MinimumSize() _, mainChildHeight = entries[0].MinimumSize()
@@ -145,13 +153,14 @@ func (layout Dialog) FlexibleHeightFor (
} }
if len(entries) > 1 { if len(entries) > 1 {
if layout.Gap { height += theme.Margin() } if layout.Gap { height += margin }
_, additionalHeight := layout.minimumSizeOfControlRow(entries[1:]) _, additionalHeight := layout.minimumSizeOfControlRow (
entries[1:], margin)
height += additionalHeight height += additionalHeight
} }
if layout.Pad { if layout.Pad {
height += theme.Margin() * 2 height += margin * 2
} }
return return
} }
@@ -159,7 +168,8 @@ func (layout Dialog) FlexibleHeightFor (
// TODO: possibly flatten this method to account for flexible elements within // TODO: possibly flatten this method to account for flexible elements within
// the control row. // the control row.
func (layout Dialog) minimumSizeOfControlRow ( func (layout Dialog) minimumSizeOfControlRow (
entries []tomo.LayoutEntry, entries []layouts.LayoutEntry,
margin int,
) ( ) (
width, height int, width, height int,
) { ) {
@@ -170,7 +180,7 @@ func (layout Dialog) minimumSizeOfControlRow (
} }
width += entryWidth width += entryWidth
if layout.Gap && index > 0 { if layout.Gap && index > 0 {
width += theme.Margin() width += margin
} }
} }
return return

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