A terminal with history, a Go-colored code block, a log viewer that follows, a hex viewer, a memory map, a JSON tree, a line diff, a GAME OVER crash screen, an FPS meter, profiler bars, breakpoints, variables, command history, a shortcut list, a debug grid, a pixel editor that exports string art, a palette editor, a sprite sheet, a glyph map and a 6502 register view. layout.ScrollArea takes Follow, and ChatLog now uses it. data.TreeNode takes a Value drawn in its Slot. pixel gains Has, and Sprite.Art writes MustSprite art. pixel.Editor takes the caller's value at each input event, so a value changed between frames is never typed onto.
202 lines
4.5 KiB
Go
202 lines
4.5 KiB
Go
package pixel
|
||
|
||
import (
|
||
_ "embed"
|
||
"encoding/binary"
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
//go:embed font12.bin
|
||
var fontData []byte
|
||
|
||
// Text line height in dots at ×1.
|
||
const LineHeight = 16
|
||
|
||
type font struct {
|
||
ascent, descent, n int
|
||
index, data []byte
|
||
}
|
||
|
||
type glyph struct {
|
||
adv, w, h, x, y, stride int
|
||
rows []byte
|
||
}
|
||
|
||
var font12 = loadFont(fontData)
|
||
|
||
// tofu stands in for runes the font lacks.
|
||
var tofu = glyph{adv: 8, w: 6, h: 9, x: 1, stride: 1, rows: []byte{0xFC, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0xFC}}
|
||
|
||
func loadFont(b []byte) *font {
|
||
if len(b) < 10 || string(b[:4]) != "8BF1" {
|
||
panic("pixel: font12.bin has a bad header")
|
||
}
|
||
n := int(binary.LittleEndian.Uint32(b[6:10]))
|
||
if len(b) < 10+8*n {
|
||
panic("pixel: font12.bin index truncated")
|
||
}
|
||
return &font{ascent: int(b[4]), descent: int(b[5]), n: n, index: b[10 : 10+8*n], data: b}
|
||
}
|
||
|
||
// Has reports whether the font draws r, not tofu.
|
||
func Has(r rune) bool {
|
||
_, ok := font12.find(r)
|
||
return ok
|
||
}
|
||
|
||
func (f *font) find(r rune) (glyph, bool) {
|
||
lo, hi := 0, f.n
|
||
for lo < hi {
|
||
m := int(uint(lo+hi) >> 1)
|
||
if rune(binary.LittleEndian.Uint32(f.index[8*m:])) < r {
|
||
lo = m + 1
|
||
} else {
|
||
hi = m
|
||
}
|
||
}
|
||
if lo == f.n || rune(binary.LittleEndian.Uint32(f.index[8*lo:])) != r {
|
||
return glyph{}, false
|
||
}
|
||
off := int(binary.LittleEndian.Uint32(f.index[8*lo+4:]))
|
||
d := f.data
|
||
if off+5 > len(d) {
|
||
panic("pixel: font12.bin glyph truncated")
|
||
}
|
||
g := glyph{adv: int(d[off]), w: int(d[off+1]), h: int(d[off+2]), x: int(int8(d[off+3])), y: int(int8(d[off+4]))}
|
||
g.stride = (g.w + 7) / 8
|
||
end := off + 5 + g.h*g.stride
|
||
if end > len(d) {
|
||
panic("pixel: font12.bin glyph truncated")
|
||
}
|
||
g.rows = d[off+5 : end]
|
||
return g, true
|
||
}
|
||
|
||
func glyphOf(r rune) glyph {
|
||
if g, ok := font12.find(r); ok {
|
||
return g
|
||
}
|
||
return tofu
|
||
}
|
||
|
||
func (g glyph) on(x, y int) bool { return g.rows[y*g.stride+x/8]&(0x80>>(x%8)) != 0 }
|
||
|
||
// top is the bitmap's first row below the line top.
|
||
func (g glyph) top() int { return font12.ascent - g.y - g.h }
|
||
|
||
// Measure returns one line's width in dots at ×1.
|
||
func Measure(s string) int {
|
||
w := 0
|
||
for _, r := range s {
|
||
w += glyphOf(r).adv
|
||
}
|
||
return w
|
||
}
|
||
|
||
// Lines wraps s to width dots; zero width keeps lines.
|
||
func Lines(s string, width int) []string {
|
||
var out []string
|
||
for _, p := range strings.Split(s, "\n") {
|
||
rs := []rune(p)
|
||
for _, r := range wrapRanges(rs, width) {
|
||
out = append(out, string(rs[r[0]:r[1]]))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// wrapRanges splits a paragraph; break lines drop trailing spaces.
|
||
func wrapRanges(rs []rune, width int) [][2]int {
|
||
if width <= 0 || Measure(string(rs)) <= width {
|
||
return [][2]int{{0, len(rs)}}
|
||
}
|
||
var out [][2]int
|
||
for start := 0; start < len(rs); {
|
||
w, brk, i := 0, -1, start
|
||
for ; i < len(rs); i++ {
|
||
if i > start && breakable(rs, i) {
|
||
brk = i
|
||
}
|
||
w += glyphOf(rs[i]).adv
|
||
if w > width && i > start {
|
||
break
|
||
}
|
||
}
|
||
if i == len(rs) {
|
||
out = append(out, [2]int{start, len(rs)})
|
||
break
|
||
}
|
||
if brk <= start {
|
||
brk = hardBreak(rs, start, i)
|
||
}
|
||
end := brk
|
||
for end > start && rs[end-1] == ' ' {
|
||
end--
|
||
}
|
||
out = append(out, [2]int{start, end})
|
||
start = skip(rs, brk)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// breakable reports whether a line may break before rs[i].
|
||
func breakable(rs []rune, i int) bool {
|
||
if n := skip(rs, i); n < len(rs) && closer(rs[n]) || opener(rs[back(rs, 0, i)]) {
|
||
return false
|
||
}
|
||
return rs[i] == ' ' || rs[i-1] == ' ' || wide(rs[i]) || wide(rs[i-1])
|
||
}
|
||
|
||
// hardBreak splits an overlong run, keeping punctuation attached.
|
||
func hardBreak(rs []rune, start, i int) int {
|
||
q, hangs := i, false
|
||
for q > start && (rs[q-1] == ' ' || opener(rs[q-1])) {
|
||
hangs = hangs || opener(rs[q-1])
|
||
q--
|
||
}
|
||
switch {
|
||
case hangs && q > start:
|
||
return q
|
||
case hangs:
|
||
for i < len(rs) && (rs[i] == ' ' || opener(rs[i])) {
|
||
i++
|
||
}
|
||
i = min(i+1, len(rs))
|
||
}
|
||
for n := skip(rs, i); n < len(rs) && closer(rs[n]); n = skip(rs, i) {
|
||
i = n + 1
|
||
}
|
||
return i
|
||
}
|
||
|
||
func skip(rs []rune, i int) int {
|
||
for i < len(rs) && rs[i] == ' ' {
|
||
i++
|
||
}
|
||
return i
|
||
}
|
||
|
||
// back returns the last non-space index before i.
|
||
func back(rs []rune, start, i int) int {
|
||
p := i - 1
|
||
for p > start && rs[p] == ' ' {
|
||
p--
|
||
}
|
||
return p
|
||
}
|
||
|
||
func closer(r rune) bool {
|
||
return unicode.In(r, unicode.Pe, unicode.Pf) || strings.ContainsRune(stops, r)
|
||
}
|
||
|
||
func opener(r rune) bool { return unicode.In(r, unicode.Ps, unicode.Pi) }
|
||
|
||
// Sentence ends that must not start a line.
|
||
const stops = ",.;:!?,。、;:?!…‥·・"
|
||
|
||
func wide(r rune) bool {
|
||
return unicode.In(r, unicode.Han, unicode.Hiragana, unicode.Katakana, unicode.Hangul) ||
|
||
r >= 0x3000 && r <= 0x303F || r >= 0xFF00 && r <= 0xFFEF
|
||
}
|