Add code components 366-385; ScrollArea Follow, editor sync

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.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 07:18:30 -04:00
1 parent 0e21870b64
commit 51af14709d
42 files changed
+3415 -37

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
package pixel
import "testing"
func TestArtRoundTrips(t *testing.T) {
art := "#a.\n.kr\n\n...\nmA#\n"
sp := MustSprite(art)
if got := sp.Art(); got != art {
t.Fatalf("Art() = %q, want %q", got, art)
}
one := SpriteOf(2, 1, func(x, y int) (Slot, bool) { return Good, x == 1 })
if got := one.Art(); got != ".g\n" {
t.Fatalf("SpriteOf Art() = %q", got)
}
}
func TestHasKnowsTofu(t *testing.T) {
if !Has('A') || !Has('中') || !Has('♪') || Has('拖') || Has('\u0378') {
t.Fatal("Has does not match the font")
}
}
+4
View File
@@ -81,6 +81,10 @@ func Editor(c *ui.Context, value *string, o EditorOptions) Edit {
composing := cb > ca
base.TextCaret(ui.Rect{X: k.D(cx - scroll), Y: k.D(cl * LineHeight), W: k.dot, H: k.D(LineHeight)})
base.HandleInput(func(ev ui.InputEvent) bool {
// The caller may have changed the value since this frame.
if string(s.text) != *value {
s.reset(*value)
}
took := s.handle(ev, env)
if s.changed {
*value = string(s.text)
+9
View File
@@ -336,3 +336,12 @@ func TestEditorSpaceDoesNotScroll(t *testing.T) {
t.Fatalf("value %q, scrolled to %v", value, scroll.Y)
}
}
func TestEditorTakesAValueChangedBetweenFrames(t *testing.T) {
h := newEdit(t, "help", 200, pixel.EditorOptions{})
h.focus()
// The caller clears it; a key lands before a frame.
h.value = ""
h.tt.Type("x")
h.want("x")
}
+6
View File
@@ -39,6 +39,12 @@ func loadFont(b []byte) *font {
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 {
+4
View File
@@ -35,6 +35,8 @@ var messages = map[Locale]map[string]string{
"mute": "MUTE", "camera": "CAMERA", "hang_up": "HANG UP", "new_count": "%d NEW", "votes": "%d VOTES", "unread": "%d UNREAD",
"day_date": "%[1]s %[2]d %[3]s", "category": "CATEGORY", "days": "SUN MON TUE WED THU FRI SAT",
"prev_track": "PREVIOUS", "next_track": "NEXT", "play": "PLAY", "volume": "VOLUME", "rec": "REC", "playing": "PLAYING", "seek": "SEEK",
"copy": "COPY", "copied": "COPIED!", "command": "COMMAND", "breakpoint": "BREAKPOINT",
"tool": "TOOL", "pen": "PEN", "fill": "FILL", "erase": "ERASE", "pick": "PICK", "grid": "GRID", "color": "COLOR", "preview": "PREVIEW",
},
Zh: {
"ok": "确定", "cancel": "取消", "yes": "是", "no": "否", "close": "关闭",
@@ -68,6 +70,8 @@ var messages = map[Locale]map[string]string{
"mute": "静音", "camera": "摄像头", "hang_up": "挂断", "new_count": "%d 条新", "votes": "%d 票", "unread": "%d 未读",
"day_date": "%[4]d月%[2]d日 周%[1]s", "category": "类别", "days": "日 一 二 三 四 五 六",
"prev_track": "上一首", "next_track": "下一首", "play": "播放", "volume": "音量", "rec": "录制", "playing": "正在播放", "seek": "进度",
"copy": "复制", "copied": "已复制!", "command": "命令", "breakpoint": "断点",
"tool": "工具", "pen": "铅笔", "fill": "填充", "erase": "橡皮", "pick": "吸管", "grid": "网格", "color": "颜色", "preview": "预览",
},
}
+24
View File
@@ -107,6 +107,30 @@ func SpriteOf(w, h int, at func(x, y int) (Slot, bool)) *Sprite {
return &Sprite{W: w, H: h, frames: [][]int8{px}}
}
// Art writes s as MustSprite reads it, frames apart.
func (s *Sprite) Art() string {
var b strings.Builder
for f, px := range s.frames {
if f > 0 {
b.WriteByte('\n')
}
for y := range s.H {
for _, v := range px[y*s.W : (y+1)*s.W] {
switch v {
case cellClear:
b.WriteByte('.')
case cellInk:
b.WriteByte('#')
default:
b.WriteByte(artSlots[v])
}
}
b.WriteByte('\n')
}
}
return b.String()
}
// Mirror returns s flipped left to right.
func (s *Sprite) Mirror() *Sprite {
m := &Sprite{W: s.W, H: s.H}