Elements are now pass-scoped value handles: tests that read bounds after a frame bind a ui.Handle. pixel tells frames apart by the build pass's root, since the Tester clock no longer moves on its own. Fix the regressions the upgrade brought: - Clipboard reads in input handlers go through c.Services(). - MyGo notes an overlay's opener after building it, so AutoFocus inside lost it; pixel.AutoFocus focuses through a Handle, and archtest bans the raw call. Dialogs hand the focus back again. - Elements around a press stay hovered: Tooltip tells a press-given focus by focus arriving under the pointer. - The web host moves the Tester clock with real time, at most 500 ms a call, and gives components the real clock. The gallery's source index also accepts CRLF checkouts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
253 lines
6.6 KiB
Go
253 lines
6.6 KiB
Go
package code
|
||
|
||
import (
|
||
"fmt"
|
||
"log/slog"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||
)
|
||
|
||
// Tool is what a press on the canvas does.
|
||
type Tool int
|
||
|
||
const (
|
||
Pen Tool = iota
|
||
Fill
|
||
Erase
|
||
Pick
|
||
)
|
||
|
||
type PixelEditorOptions struct {
|
||
// Zoom is dots per sprite dot, 2–16; zero means 8.
|
||
Zoom int
|
||
Disabled bool
|
||
}
|
||
|
||
type PixelEditorResult struct {
|
||
Element ui.Element
|
||
// Changed is the art; Copied is COPY.
|
||
Changed, Copied bool
|
||
}
|
||
|
||
type brushKey struct{}
|
||
|
||
// brush is tool, slot, cursor, last dab, grid, art size.
|
||
type brush struct {
|
||
tool, slot, cur, last, w, h int
|
||
grid bool
|
||
}
|
||
|
||
// PixelEditor paints *art, one frame of string art.
|
||
func PixelEditor(c *ui.Context, text string, art *string, o PixelEditorOptions) PixelEditorResult {
|
||
k := pixel.K(c)
|
||
z := pixel.Or("PixelEditor Zoom", o.Zoom, 8)
|
||
sp := pixel.MustSprite(*art)
|
||
if z < 2 || z > 16 || sp.Frames() != 1 {
|
||
panic(fmt.Sprintf("code: PixelEditor of %d frames at zoom %d", sp.Frames(), z))
|
||
}
|
||
w, h := sp.W, sp.H
|
||
// cells hold a slot, or -1 where clear.
|
||
cells := make([]int, w*h)
|
||
for i := range cells {
|
||
cells[i] = -1
|
||
if s, ok := sp.At(0, i%w, i/w, pixel.Ink); ok {
|
||
cells[i] = int(s)
|
||
}
|
||
}
|
||
var res PixelEditorResult
|
||
res.Element = ui.Row(c).Gap(k.D(6)).AlignItems(ui.Start)
|
||
b := ui.Local(res.Element, brushKey{}, func() brush { return brush{slot: int(pixel.Ink), last: -1, grid: true} })
|
||
// New art drops a dab measured on the old.
|
||
if b.w != w || b.h != h {
|
||
b.w, b.h, b.last, b.cur = w, h, -1, min(b.cur, w*h-1)
|
||
}
|
||
apply := func(i int) bool {
|
||
switch Tool(b.tool) {
|
||
case Pen, Erase:
|
||
v := b.slot
|
||
if Tool(b.tool) == Erase {
|
||
v = -1
|
||
}
|
||
if cells[i] == v {
|
||
return false
|
||
}
|
||
cells[i] = v
|
||
return true
|
||
case Fill:
|
||
return flood(cells, w, h, i, b.slot)
|
||
}
|
||
// The picker takes a slot, then hands back the pen.
|
||
if cells[i] >= 0 {
|
||
b.slot, b.tool = cells[i], int(Pen)
|
||
slog.Debug("8bit: pixel editor picked", "label", text, "slot", pixel.Slot(b.slot))
|
||
}
|
||
return false
|
||
}
|
||
res.Element.Children(func() {
|
||
shell, base, st, picked := pixel.Pad(c, text, w, w*h, z, z, &b.cur, o.Disabled)
|
||
if !base.IsDisabled() {
|
||
// Pen and eraser lay dots along a drag.
|
||
tool := Tool(b.tool)
|
||
if x, y, _ := base.PointerPosition(); base.Pressed() && (tool == Pen || tool == Erase) {
|
||
cx, cy := int(x/k.Dot())/z, int(y/k.Dot())/z
|
||
if i := cy*w + cx; cx >= 0 && cy >= 0 && cx < w && cy < h && i != b.last {
|
||
// A fast drag still lays every dot between.
|
||
from := i
|
||
if b.last >= 0 {
|
||
from = b.last
|
||
}
|
||
steps(from%w, from/w, cx, cy, func(j int) { res.Changed = apply(j) || res.Changed }, w)
|
||
b.last = i
|
||
}
|
||
} else {
|
||
b.last = -1
|
||
}
|
||
if picked >= 0 {
|
||
res.Changed = apply(picked) || res.Changed
|
||
}
|
||
}
|
||
v := cells[b.cur]
|
||
say := "clear"
|
||
if v >= 0 {
|
||
say = pixel.Slot(v).String()
|
||
}
|
||
base.Description(fmt.Sprintf("%d, %d: %s", b.cur%w, b.cur/w, say))
|
||
canvas(shell, k, st, cells, w, z, b.cur, b.grid)
|
||
tools(c, k, text, b, art, o.Disabled, &res)
|
||
})
|
||
if res.Changed {
|
||
*art = pixel.SpriteOf(w, h, func(x, y int) (pixel.Slot, bool) {
|
||
v := cells[y*w+x]
|
||
return pixel.Slot(max(v, 0)), v >= 0
|
||
}).Art()
|
||
slog.Debug("8bit: pixel editor painted", "label", text, "tool", b.tool, "slot", pixel.Slot(b.slot))
|
||
}
|
||
return res
|
||
}
|
||
|
||
// canvas paints cells, checkers where clear, grid and cursor.
|
||
func canvas(shell ui.Element, k pixel.Kit, st pixel.State, cells []int, w, z, cur int, grid bool) {
|
||
shell.Draw(func(p *ui.Painter, r ui.Rect) {
|
||
g := k.Canvas(p, r)
|
||
for i, v := range cells {
|
||
x, y := i%w*z, i/w*z
|
||
if v < 0 {
|
||
g.Rect(x, y, z, z, pixel.Lit)
|
||
g.Rect(x, y, z/2, z/2, pixel.Face)
|
||
g.Rect(x+z/2, y+z/2, z-z/2, z-z/2, pixel.Face)
|
||
continue
|
||
}
|
||
g.Rect(x, y, z, z, pixel.Slot(v))
|
||
}
|
||
if grid {
|
||
for x := z; x < g.W; x += z {
|
||
g.VLine(x, 0, g.H, pixel.Shade)
|
||
}
|
||
for y := z; y < g.H; y += z {
|
||
g.HLine(0, y, g.W, pixel.Shade)
|
||
}
|
||
}
|
||
if st.Focus {
|
||
x, y := cur%w*z, cur/w*z
|
||
g.Rect(x, y, z, 1, pixel.Accent)
|
||
g.Rect(x, y+z-1, z, 1, pixel.Accent)
|
||
g.Rect(x, y, 1, z, pixel.Accent)
|
||
g.Rect(x+z-1, y, 1, z, pixel.Accent)
|
||
}
|
||
g.Finish(st)
|
||
})
|
||
}
|
||
|
||
// tools are the tool switch, slot pad, GRID and COPY.
|
||
func tools(c *ui.Context, k pixel.Kit, text string, b *brush, art *string, disabled bool, res *PixelEditorResult) {
|
||
ui.Column(c).Gap(k.D(4)).Children(func() {
|
||
names := []string{k.Msg("pen"), k.Msg("fill"), k.Msg("erase"), k.Msg("pick")}
|
||
if input.Segmented(c, k.Msg("tool"), names, &b.tool, input.SegmentedOptions{Disabled: disabled}).Changed {
|
||
slog.Debug("8bit: pixel editor tool", "label", text, "tool", names[b.tool])
|
||
}
|
||
old := b.slot
|
||
// The pad's cursor is the slot laid down.
|
||
shell, base, st, _ := pixel.Pad(c, k.Msg("color"), 8, int(pixel.Slots), 10, 10, &b.slot, disabled)
|
||
base.Description(pixel.Slot(b.slot).String())
|
||
if b.slot != old {
|
||
slog.Debug("8bit: pixel editor slot", "label", text, "slot", pixel.Slot(b.slot))
|
||
}
|
||
at := b.slot
|
||
shell.Draw(func(p *ui.Painter, r ui.Rect) {
|
||
g := k.Canvas(p, r)
|
||
for s := range int(pixel.Slots) {
|
||
x, y := s%8*10, s/8*10
|
||
if s == at {
|
||
g.Rect(x, y, 10, 10, pixel.Accent)
|
||
}
|
||
g.Frame(x+1, y+1, 8, 8, pixel.Flat, pixel.Slot(s))
|
||
}
|
||
g.Finish(st)
|
||
})
|
||
if input.Toggle(c, k.Msg("grid"), &b.grid, input.ToggleOptions{Disabled: disabled}).Changed {
|
||
slog.Debug("8bit: pixel editor grid", "label", text, "on", b.grid)
|
||
}
|
||
if input.Button(c, k.Msg("copy"), input.ButtonOptions{Disabled: disabled}).Clicked {
|
||
c.WriteClipboard(*art)
|
||
res.Copied = true
|
||
slog.Debug("8bit: pixel editor copied", "label", text)
|
||
}
|
||
})
|
||
}
|
||
|
||
// flood lays slot over the like cells around i.
|
||
func flood(cells []int, w, h, i, slot int) bool {
|
||
from := cells[i]
|
||
if from == slot {
|
||
return false
|
||
}
|
||
stack := []int{i}
|
||
for len(stack) > 0 {
|
||
j := stack[len(stack)-1]
|
||
stack = stack[:len(stack)-1]
|
||
if cells[j] != from {
|
||
continue
|
||
}
|
||
cells[j] = slot
|
||
x, y := j%w, j/w
|
||
for _, n := range [4][2]int{{x - 1, y}, {x + 1, y}, {x, y - 1}, {x, y + 1}} {
|
||
if n[0] >= 0 && n[1] >= 0 && n[0] < w && n[1] < h {
|
||
stack = append(stack, n[1]*w+n[0])
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// steps calls at for each cell on a line.
|
||
func steps(x0, y0, x1, y1 int, at func(i int), w int) {
|
||
dx, dy := abs(x1-x0), -abs(y1-y0)
|
||
sx, sy := sign(x1-x0), sign(y1-y0)
|
||
for e := dx + dy; ; {
|
||
at(y0*w + x0)
|
||
if x0 == x1 && y0 == y1 {
|
||
return
|
||
}
|
||
if e2 := 2 * e; e2 >= dy {
|
||
e, x0 = e+dy, x0+sx
|
||
} else {
|
||
e, y0 = e+dx, y0+sy
|
||
}
|
||
}
|
||
}
|
||
|
||
func abs(v int) int { return max(v, -v) }
|
||
|
||
func sign(v int) int {
|
||
switch {
|
||
case v > 0:
|
||
return 1
|
||
case v < 0:
|
||
return -1
|
||
}
|
||
return 0
|
||
}
|