2023-04-07 21:33:39 -06:00
|
|
|
package elements
|
|
|
|
|
|
|
|
import "image"
|
2023-04-08 17:23:23 -06:00
|
|
|
import "unicode"
|
2023-04-07 22:40:10 -06:00
|
|
|
import "image/color"
|
2023-04-07 21:33:39 -06:00
|
|
|
import "golang.org/x/image/font"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
2023-04-08 17:23:23 -06:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
|
2023-04-07 22:40:10 -06:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
2023-04-07 21:33:39 -06:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
|
|
|
// import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
|
|
|
2023-04-07 22:40:10 -06:00
|
|
|
type Cell struct {
|
|
|
|
Rune rune
|
|
|
|
Style tomo.FontStyle
|
|
|
|
Background tomo.Color
|
|
|
|
Foreground tomo.Color
|
2023-04-07 21:33:39 -06:00
|
|
|
}
|
|
|
|
|
2023-04-07 22:40:10 -06:00
|
|
|
type gridCell struct {
|
|
|
|
Cell
|
|
|
|
clean bool
|
2023-04-07 21:33:39 -06:00
|
|
|
}
|
|
|
|
|
2023-04-07 22:40:10 -06:00
|
|
|
func (cell *Cell) initColor () {
|
|
|
|
cell.Background = tomo.ColorBackground
|
|
|
|
cell.Foreground = tomo.ColorForeground
|
2023-04-07 21:33:39 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Grid is an array of monospaced character cells. Each one has a foreground and
|
2023-04-07 22:40:10 -06:00
|
|
|
// background color.
|
2023-04-07 21:33:39 -06:00
|
|
|
type Grid struct {
|
|
|
|
*core.Core
|
|
|
|
*core.FocusableCore
|
|
|
|
core core.CoreControl
|
2023-04-07 22:40:10 -06:00
|
|
|
focusableControl core.FocusableCoreControl
|
2023-04-07 21:33:39 -06:00
|
|
|
|
|
|
|
cells []gridCell
|
|
|
|
stride int
|
|
|
|
cellWidth int
|
|
|
|
cellHeight int
|
2023-04-08 12:41:29 -06:00
|
|
|
gridBounds image.Rectangle
|
2023-04-07 21:33:39 -06:00
|
|
|
|
|
|
|
cursor image.Point
|
|
|
|
|
2023-04-08 12:31:45 -06:00
|
|
|
ignorePush bool
|
|
|
|
|
2023-04-08 17:23:23 -06:00
|
|
|
ascent int
|
2023-04-07 22:40:10 -06:00
|
|
|
face font.Face
|
2023-04-07 21:33:39 -06:00
|
|
|
config config.Wrapped
|
|
|
|
theme theme.Wrapped
|
2023-04-07 22:40:10 -06:00
|
|
|
colors [19]color.RGBA
|
2023-04-07 21:33:39 -06:00
|
|
|
|
|
|
|
onResize func ()
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewGrid () (element *Grid) {
|
|
|
|
element = &Grid { }
|
|
|
|
element.theme.Case = tomo.C("tomo", "grid")
|
2023-04-07 22:40:10 -06:00
|
|
|
element.Core, element.core = core.NewCore(element, element.drawAllAndPush)
|
|
|
|
element.FocusableCore,
|
|
|
|
element.focusableControl = core.NewFocusableCore(element.core, element.drawAndPush)
|
|
|
|
element.updateFontAndColors()
|
2023-04-07 21:33:39 -06:00
|
|
|
element.updateMinimumSize()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-04-07 22:40:10 -06:00
|
|
|
func (element *Grid) Size () (columns, rows int) {
|
|
|
|
columns = element.stride
|
|
|
|
if element.stride > 0 {
|
|
|
|
rows = len(element.cells) / element.stride
|
|
|
|
}
|
|
|
|
return
|
2023-04-07 21:33:39 -06:00
|
|
|
}
|
|
|
|
|
2023-04-07 22:40:10 -06:00
|
|
|
func (element *Grid) At (point image.Point) Cell {
|
|
|
|
if !element.inBounds(point) { return Cell { } }
|
|
|
|
return element.cells[element.index(point)].Cell
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) Set (point image.Point, cell Cell) {
|
|
|
|
if !element.inBounds(point) { return }
|
|
|
|
element.cells[element.index(point)].Cell = cell
|
|
|
|
element.cells[element.index(point)].clean = false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) Push () {
|
2023-04-08 12:31:45 -06:00
|
|
|
if !element.ignorePush {
|
|
|
|
element.drawAndPush()
|
|
|
|
}
|
2023-04-07 22:40:10 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) OnResize (callback func ()) {
|
|
|
|
element.onResize = callback
|
2023-04-07 21:33:39 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) HandleMouseDown (x, y int, button input.Button) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) HandleMouseUp (x, y int, button input.Button) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
|
|
|
// TODO we need to grab shift ctrl c for copying text
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
|
|
|
|
|
|
|
// SetTheme sets the element's theme.
|
|
|
|
func (element *Grid) SetTheme (new tomo.Theme) {
|
|
|
|
if new == element.theme.Theme { return }
|
|
|
|
element.theme.Theme = new
|
2023-04-07 22:40:10 -06:00
|
|
|
element.updateFontAndColors()
|
2023-04-07 21:33:39 -06:00
|
|
|
element.updateMinimumSize()
|
|
|
|
element.drawAndPush()
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetConfig sets the element's configuration.
|
|
|
|
func (element *Grid) SetConfig (new tomo.Config) {
|
|
|
|
if new == element.config.Config { return }
|
|
|
|
element.config.Config = new
|
|
|
|
element.updateMinimumSize()
|
|
|
|
element.drawAndPush()
|
|
|
|
}
|
|
|
|
|
2023-04-07 22:40:10 -06:00
|
|
|
|
|
|
|
// -------- private methods -------- //
|
|
|
|
|
|
|
|
|
|
|
|
func (element *Grid) inBounds (point image.Point) bool {
|
|
|
|
if point.X < 0 { return false }
|
|
|
|
if point.Y < 0 { return false }
|
|
|
|
width, height := element.Size()
|
|
|
|
if point.X >= width { return false }
|
|
|
|
if point.Y >= height { return false }
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) index (point image.Point) int {
|
|
|
|
return point.X + element.stride * point.Y
|
|
|
|
}
|
|
|
|
|
2023-04-07 21:33:39 -06:00
|
|
|
func (element *Grid) alloc () bool {
|
|
|
|
bounds := element.Bounds()
|
|
|
|
width := bounds.Dx() / element.cellWidth
|
|
|
|
height := bounds.Dy() / element.cellHeight
|
2023-04-07 22:40:10 -06:00
|
|
|
oldWidth, oldHeight := element.Size()
|
|
|
|
if width == oldWidth && height == oldHeight { return false }
|
2023-04-08 12:41:29 -06:00
|
|
|
|
|
|
|
element.gridBounds = image.Rect (
|
|
|
|
0, 0,
|
|
|
|
width * element.cellWidth,
|
|
|
|
height * element.cellHeight).Add(bounds.Min)
|
2023-04-07 21:33:39 -06:00
|
|
|
|
|
|
|
oldCells := element.cells
|
2023-04-08 12:31:45 -06:00
|
|
|
heightLarger := height > oldHeight
|
2023-04-07 21:33:39 -06:00
|
|
|
|
|
|
|
element.stride = width
|
|
|
|
element.cells = make([]gridCell, width * height)
|
|
|
|
|
|
|
|
// TODO: attempt to wrap text?
|
|
|
|
|
|
|
|
if heightLarger {
|
2023-04-08 12:31:45 -06:00
|
|
|
newRowsStart := oldHeight * width
|
|
|
|
for index := range element.cells[newRowsStart:] {
|
|
|
|
element.cells[index + newRowsStart].initColor()
|
|
|
|
}
|
|
|
|
}
|
2023-04-07 21:33:39 -06:00
|
|
|
|
|
|
|
commonHeight := height
|
|
|
|
if heightLarger { commonHeight = oldHeight }
|
|
|
|
for index := range element.cells[:commonHeight * width] {
|
|
|
|
x := index % width
|
|
|
|
if x < oldWidth {
|
2023-04-08 12:31:45 -06:00
|
|
|
row := index / width
|
|
|
|
element.cells[index] = oldCells[x + row * oldWidth]
|
2023-04-07 21:33:39 -06:00
|
|
|
} else {
|
|
|
|
element.cells[index].initColor()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-08 12:31:45 -06:00
|
|
|
element.ignorePush = true
|
2023-04-07 21:33:39 -06:00
|
|
|
if element.onResize != nil { element.onResize() }
|
2023-04-08 12:31:45 -06:00
|
|
|
element.ignorePush = false
|
2023-04-07 21:33:39 -06:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-04-07 22:40:10 -06:00
|
|
|
func (element *Grid) updateFontAndColors () {
|
2023-04-07 21:33:39 -06:00
|
|
|
element.face = element.theme.FontFace (
|
|
|
|
tomo.FontStyleMonospace,
|
|
|
|
tomo.FontSizeNormal)
|
|
|
|
emSpace, _ := element.face.GlyphAdvance('M')
|
|
|
|
metrics := element.face.Metrics()
|
|
|
|
element.cellWidth = emSpace.Round()
|
|
|
|
element.cellHeight = metrics.Height.Round()
|
2023-04-08 17:23:23 -06:00
|
|
|
element.ascent = metrics.Ascent.Round()
|
2023-04-07 22:40:10 -06:00
|
|
|
|
|
|
|
for index := range element.colors {
|
|
|
|
element.colors[index] = element.theme.Color (
|
|
|
|
tomo.Color(index),
|
|
|
|
element.state())
|
|
|
|
}
|
2023-04-07 21:33:39 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) updateMinimumSize () {
|
|
|
|
element.core.SetMinimumSize(element.cellWidth, element.cellHeight)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) state () tomo.State {
|
|
|
|
return tomo.State {
|
2023-04-07 22:40:10 -06:00
|
|
|
Focused: element.Focused(),
|
2023-04-07 21:33:39 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) drawAndPush () {
|
2023-04-07 22:40:10 -06:00
|
|
|
if element.core.HasImage () {
|
|
|
|
element.core.DamageRegion(element.draw(element.alloc()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) drawAllAndPush () {
|
|
|
|
element.alloc()
|
2023-04-07 21:33:39 -06:00
|
|
|
if element.core.HasImage () {
|
|
|
|
element.core.DamageRegion(element.draw(true))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-07 22:40:10 -06:00
|
|
|
func (element *Grid) corner (index int) image.Point {
|
|
|
|
return image.Point {
|
|
|
|
X: (index % element.stride) * element.cellWidth,
|
|
|
|
Y: (index / element.stride) * element.cellHeight,
|
|
|
|
}.Add(element.Bounds().Min)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) bound (index int) image.Rectangle {
|
|
|
|
corner := element.corner(index)
|
|
|
|
return image.Rectangle {
|
|
|
|
Min: corner,
|
|
|
|
Max: image.Pt (
|
|
|
|
corner.X + element.cellWidth,
|
|
|
|
corner.Y + element.cellHeight),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-08 17:23:23 -06:00
|
|
|
func (element *Grid) draw (force bool) (updatedRegion image.Rectangle) {
|
2023-04-07 22:40:10 -06:00
|
|
|
bounds := element.Bounds()
|
|
|
|
|
|
|
|
for index, cell := range element.cells {
|
|
|
|
if force || !cell.clean {
|
2023-04-08 17:23:23 -06:00
|
|
|
cellBounds := element.bound(index)
|
|
|
|
updatedRegion = updatedRegion.Union(cellBounds)
|
2023-04-07 22:40:10 -06:00
|
|
|
shapes.FillColorRectangle (
|
|
|
|
element.core,
|
|
|
|
element.colors[cell.Background],
|
2023-04-08 17:23:23 -06:00
|
|
|
cellBounds)
|
|
|
|
element.drawCellGlyph(cellBounds, cell.Cell)
|
2023-04-07 22:40:10 -06:00
|
|
|
}}
|
|
|
|
|
2023-04-08 12:41:29 -06:00
|
|
|
if force {
|
|
|
|
shapes.FillColorRectangleShatter (
|
|
|
|
element.core,
|
|
|
|
element.theme.Color(tomo.ColorBackground, element.state()),
|
|
|
|
bounds, element.gridBounds)
|
|
|
|
}
|
|
|
|
|
2023-04-08 17:23:23 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Grid) drawCellGlyph (cellBounds image.Rectangle, cell Cell) {
|
|
|
|
if cell.Rune < 32 || unicode.IsSpace(cell.Rune) { return }
|
|
|
|
|
|
|
|
glyphColor := element.colors[cell.Foreground]
|
|
|
|
destinationRectangle, mask, maskPoint, _, ok := element.face.Glyph (
|
|
|
|
fixedutil.Pt(cellBounds.Min),
|
|
|
|
cell.Rune)
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
// tofu
|
|
|
|
shapes.StrokeColorRectangle (
|
|
|
|
element.core, glyphColor, cellBounds.Inset(1), 1)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
maxX := destinationRectangle.Dx()
|
|
|
|
maxY := destinationRectangle.Dy()
|
|
|
|
|
|
|
|
data, stride := element.core.Buffer()
|
|
|
|
|
|
|
|
for y := 0; y < maxY; y ++ {
|
|
|
|
for x := 0; x < maxX; x ++ {
|
|
|
|
_, _, _,
|
|
|
|
alpha := mask.At(x + maskPoint.X, y + maskPoint.Y).RGBA()
|
|
|
|
dstX := x + destinationRectangle.Min.X
|
|
|
|
dstY := y + destinationRectangle.Min.Y + element.ascent
|
|
|
|
dstIndex := dstX + dstY * stride
|
|
|
|
data[dstIndex] = blend (
|
|
|
|
data[dstIndex],
|
|
|
|
glyphColor,
|
|
|
|
float64(alpha) / 0xFFFF)
|
|
|
|
}}
|
2023-04-07 21:33:39 -06:00
|
|
|
}
|
2023-04-08 17:23:23 -06:00
|
|
|
|
|
|
|
func blend (bottom, top color.RGBA, fac float64) color.RGBA {
|
|
|
|
return color.RGBA {
|
|
|
|
R: uint8(float64(bottom.R) * (1 - fac) + float64(top.R) * fac),
|
|
|
|
G: uint8(float64(bottom.G) * (1 - fac) + float64(top.G) * fac),
|
|
|
|
B: uint8(float64(bottom.B) * (1 - fac) + float64(top.B) * fac),
|
|
|
|
A: 0xFF,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|