Compare commits
111
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d9bc4f868 | ||
|
|
1f2e8aa677 | ||
|
|
ef59f46559 | ||
|
|
829f1525b8 | ||
|
|
b1d15fb4ec | ||
|
|
ee45b2fa60 | ||
|
|
8dd506a007 | ||
|
|
de10cde630 | ||
|
|
449922851f | ||
|
|
26787d8670 | ||
|
|
cda2d1f0ae | ||
|
|
241c297626 | ||
|
|
2859dc3313 | ||
|
|
7e51dc5e5a | ||
|
|
81090267a6 | ||
|
|
bf2fdb5eaa | ||
|
|
211219eb01 | ||
|
|
79ab1c8ac0 | ||
|
|
d167559830 | ||
|
|
48237f5687 | ||
|
|
0ba3c982c4 | ||
|
|
c7e44633b1 | ||
|
|
b575413a0a | ||
|
|
29e4a7572b | ||
|
|
ddb960571f | ||
|
|
ce1d938f7a | ||
|
|
20fa445cdd | ||
|
|
e966771f5b | ||
|
|
e9e1ccc35e | ||
|
|
0c39c2dd57 | ||
|
|
f8240fb518 | ||
|
|
fc0a9292d9 | ||
|
|
b9cbf83a18 | ||
|
|
270b49f825 | ||
|
|
e3369ab3d4 | ||
|
|
50e9c3b1c9 | ||
|
|
e2e846a0e5 | ||
|
|
fa934fa485 | ||
|
|
56dc9ba54c | ||
|
|
7235c86e22 | ||
|
|
367aee4570 | ||
|
|
bd55b6c17d | ||
|
|
a0e7bf1373 | ||
|
|
234503f104 | ||
|
|
ae551c47ea | ||
|
|
0c22977693 | ||
|
|
4d87972235 | ||
|
|
d59b7d812d | ||
|
|
09f782953e | ||
|
|
fa42cf1f5f | ||
|
|
dcaf9919e4 | ||
|
|
d18da8b07a | ||
|
|
88502cf628 | ||
|
|
21abd147bf | ||
|
|
4bc8566820 | ||
|
|
8ac5108211 | ||
|
|
7f0462d588 | ||
|
|
82e92f1e2e | ||
|
|
9e8e986977 | ||
|
|
2d9a941da8 | ||
|
|
c64ce8da67 | ||
|
|
a893831a21 | ||
|
|
7f1c3ae870 | ||
|
|
d7a6193c04 | ||
|
|
a74f9809af | ||
|
|
f9032a9a95 | ||
|
|
0e3de11203 | ||
|
|
981c11bd44 | ||
|
|
dce0321e9b | ||
|
|
5e448edb21 | ||
|
|
c33faa402b | ||
|
|
182cb1e35b | ||
|
|
cfc2b5e130 | ||
|
|
6e7cf285cc | ||
|
|
e3aea7fc9e | ||
|
|
5446ffe40b | ||
|
|
06e97461fa | ||
|
|
b38232ee24 | ||
|
|
2cd670f4cd | ||
|
|
c7bebabed5 | ||
|
|
b15c260dfc | ||
|
|
16ce15621e | ||
|
|
16a0e76145 | ||
|
|
ce20b7d02c | ||
|
|
c5ee7c8cdb | ||
|
|
5c7e243566 | ||
|
|
bec8b817c8 | ||
|
|
6cc0f36000 | ||
|
|
a0e57921a4 | ||
|
|
6936353516 | ||
|
|
3998d842b1 | ||
|
|
0bdbaa39ca | ||
|
|
f8ebe5b1e4 | ||
|
|
8d90dbdc92 | ||
|
|
43fea5c8ba | ||
|
|
2ff32ca8ea | ||
|
|
d79701d01b | ||
|
|
bdf599f93c | ||
|
|
8ccaa0faba | ||
|
|
83b8040520 | ||
|
|
4722656c7d | ||
|
|
14d1836209 | ||
|
|
36b995c514 | ||
|
|
46574cfb10 | ||
|
|
8606968c74 | ||
|
|
892c74a9da | ||
|
|
99942466f8 | ||
|
|
da6fe2c845 | ||
|
|
04d2ea4767 | ||
|
|
f71f789b60 | ||
|
|
8f0f2be9e9 |
@@ -1,5 +1,7 @@
|
|||||||
# 
|
# 
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Package artist provides a simple 2D drawing library for canvas.Canvas.
|
||||||
|
package artist
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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)
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Package patterns provides a basic set of types that satisfy the
|
||||||
|
// artist.Pattern interface.
|
||||||
|
package patterns
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
}}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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.
|
|
||||||
}
|
|
||||||
@@ -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]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||||
|
|||||||
+83
-38
@@ -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())
|
||||||
})
|
})
|
||||||
window.resizeChildToFit()
|
if !window.childMinimumSizeChangeCallback(child.MinimumSize()) {
|
||||||
window.childMinimumSizeChangeCallback(child.MinimumSize())
|
window.resizeChildToFit()
|
||||||
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,30 +210,56 @@ func (window *Window) OnClose (callback func ()) {
|
|||||||
window.onClose = callback
|
window.onClose = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) reallocateCanvas () {
|
func (window *Window) SetTheme (theme theme.Theme) {
|
||||||
window.canvas = tomo.NewBasicCanvas (
|
window.theme = theme
|
||||||
window.metrics.width,
|
if child, ok := window.child.(elements.Themeable); ok {
|
||||||
window.metrics.height)
|
child.SetTheme(theme)
|
||||||
if window.xCanvas != nil {
|
|
||||||
window.xCanvas.Destroy()
|
|
||||||
}
|
}
|
||||||
window.xCanvas = xgraphics.New (
|
}
|
||||||
window.backend.connection,
|
|
||||||
image.Rect (
|
func (window *Window) SetConfig (config config.Config) {
|
||||||
0, 0,
|
window.config = config
|
||||||
window.metrics.width,
|
if child, ok := window.child.(elements.Configurable); ok {
|
||||||
window.metrics.height))
|
child.SetConfig(config)
|
||||||
window.xCanvas.CreatePixmap()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (window *Window) reallocateCanvas () {
|
||||||
|
window.canvas.Reallocate(window.metrics.width, window.metrics.height)
|
||||||
|
|
||||||
|
previousWidth, previousHeight := 0, 0
|
||||||
|
if window.xCanvas != nil {
|
||||||
|
previousWidth = window.xCanvas.Bounds().Dx()
|
||||||
|
previousHeight = window.xCanvas.Bounds().Dy()
|
||||||
|
}
|
||||||
|
|
||||||
|
newWidth := window.metrics.width
|
||||||
|
newHeight := window.metrics.height
|
||||||
|
larger := newWidth > previousWidth || newHeight > previousHeight
|
||||||
|
smaller := newWidth < previousWidth / 2 || newHeight < previousHeight / 2
|
||||||
|
if larger || smaller {
|
||||||
|
if window.xCanvas != nil {
|
||||||
|
window.xCanvas.Destroy()
|
||||||
|
}
|
||||||
|
window.xCanvas = xgraphics.New (
|
||||||
|
window.backend.connection,
|
||||||
|
image.Rect (
|
||||||
|
0, 0,
|
||||||
|
(newWidth / 64) * 64 + 64,
|
||||||
|
(newHeight / 64) * 64 + 64))
|
||||||
|
window.xCanvas.CreatePixmap()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) redrawChildEntirely () {
|
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
@@ -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") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,4 +1,4 @@
|
|||||||
package tomo
|
package data
|
||||||
|
|
||||||
import "io"
|
import "io"
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+108
-63
@@ -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.DamageAll()
|
element.core.DamageRegion (append (
|
||||||
|
element.drawBackground(true),
|
||||||
|
element.drawText(true))...)
|
||||||
|
} else {
|
||||||
|
element.drawAll()
|
||||||
|
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
@@ -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
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Package basicElements provides standard elements that are commonly used in
|
||||||
|
// GUI applications.
|
||||||
|
package basicElements
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
+62
-17
@@ -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 ()
|
||||||
}
|
}
|
||||||
@@ -21,15 +23,29 @@ type Label struct {
|
|||||||
// NewLabel creates a new label. If wrap is set to true, the text inside will be
|
// NewLabel creates a new label. If wrap is set to true, the text inside will be
|
||||||
// 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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+114
-45
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
if x > element.horizontal.bar.Min.X {
|
element.horizontal.dragging = true
|
||||||
element.scrollChildBy(16, 0)
|
element.horizontal.dragOffset =
|
||||||
} else {
|
element.horizontal.bar.Dx() / 2 +
|
||||||
element.scrollChildBy(-16, 0)
|
element.Bounds().Min.X
|
||||||
|
element.dragHorizontalBar(point)
|
||||||
|
case input.ButtonMiddle:
|
||||||
|
viewport := element.child.ScrollViewportBounds().Dx()
|
||||||
|
if x > element.horizontal.bar.Min.X {
|
||||||
|
element.scrollChildBy(viewport, 0)
|
||||||
|
} else {
|
||||||
|
element.scrollChildBy(-viewport, 0)
|
||||||
|
}
|
||||||
|
case input.ButtonRight:
|
||||||
|
if x > element.horizontal.bar.Min.X {
|
||||||
|
element.scrollChildBy(velocity, 0)
|
||||||
|
} else {
|
||||||
|
element.scrollChildBy(-velocity, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if point.In(element.vertical.bar) {
|
} 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) {
|
||||||
if y > element.vertical.bar.Min.Y {
|
switch button {
|
||||||
element.scrollChildBy(0, 16)
|
case input.ButtonLeft:
|
||||||
} else {
|
element.vertical.dragging = true
|
||||||
element.scrollChildBy(0, -16)
|
element.vertical.dragOffset =
|
||||||
|
element.vertical.bar.Dy() / 2 +
|
||||||
|
element.Bounds().Min.Y
|
||||||
|
element.dragVerticalBar(point)
|
||||||
|
case input.ButtonMiddle:
|
||||||
|
viewport := element.child.ScrollViewportBounds().Dy()
|
||||||
|
if y > element.vertical.bar.Min.Y {
|
||||||
|
element.scrollChildBy(0, viewport)
|
||||||
|
} else {
|
||||||
|
element.scrollChildBy(0, -viewport)
|
||||||
|
}
|
||||||
|
case input.ButtonRight:
|
||||||
|
if y > element.vertical.bar.Min.Y {
|
||||||
|
element.scrollChildBy(0, velocity)
|
||||||
|
} else {
|
||||||
|
element.scrollChildBy(0, -velocity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if child, ok := element.child.(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]
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+191
-107
@@ -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.text,
|
element.dot = textmanip.SelectLeft (
|
||||||
element.cursor,
|
element.text,
|
||||||
modifiers.Control)
|
element.dot,
|
||||||
|
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.text,
|
element.dot = textmanip.SelectRight (
|
||||||
element.cursor,
|
element.text,
|
||||||
modifiers.Control)
|
element.dot,
|
||||||
|
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,63 +310,109 @@ 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 (
|
foreground, 1,
|
||||||
element,
|
cursorPosition.Add(offset),
|
||||||
foreground, 1,
|
image.Pt (
|
||||||
cursorPosition.Add(offset),
|
cursorPosition.X,
|
||||||
image.Pt (
|
cursorPosition.Y + element.valueDrawer.
|
||||||
cursorPosition.X,
|
LineHeight().Round()).Add(offset))
|
||||||
cursorPosition.Y + element.valueDrawer.
|
|
||||||
LineHeight().Round()).Add(offset))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-34
@@ -2,59 +2,44 @@ 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
|
||||||
minimumHeight int
|
minimumHeight int
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
core.focused = true
|
if core.focused == false {
|
||||||
if core.drawFocusChange != nil { core.drawFocusChange() }
|
core.focused = true
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Package testing provides elements that are used to test different parts of
|
||||||
|
// Tomo's API.
|
||||||
|
package testing
|
||||||
+45
-21
@@ -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,4 +1,4 @@
|
|||||||
package tomo
|
package elements
|
||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
} ()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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 () {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.")
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user