The renderer kept a mirror of the pushed IME snippet (the 'IME model') to translate commit positions, but it transiently desynced from the buffer on fling/tap sequences (observed as a few-byte mapping drift on both the x86_64 emulator and the ARM phone), corrupting text. The model string also sat on the main goroutine next to the JNI render path, where the app observed states that were impossible for Go memory (string contents changing between reads microseconds apart), pointing at corruption in the native bridge layer. Restructure along the lines of the Android InputConnection contract and Gio's own reference editor (widget/editor.go): - Commits carry absolute file runes (the pushed snippet's coordinate space) straight to the logic goroutine, which maps them to bytes against the WHOLE buffer (runeToByteWhole, an 8 KiB-step scan). Scrolling moves the window, not the buffer, so the mapping is exact mid-fling by construction — no mirror to desync. - Drift guard in HandleIMECommit: a small commit (range <= 2 runes) is always anchored at the caret the IME was last told about; if the IME reports it ending elsewhere, its snippet text is stale (a dropped restartInput, as Gboard does during flings) and its position is in the stale text's coordinates — snap the commit to the cursor, the only position it cannot drift from. - FlushIME simplifies to: push the snippet when the frame's (context+window) text differs from the last push (gioui dedupes against its own cache), force the selection re-push in the same frame. After a commit the frame text equals what the IME already holds locally, so the restart is naturally suppressed; a fling re-anchors the IME once per text change. - Remove the renderer model (adoptFrame/ModelTranslate/ ApplyIMEEdit/ApplyIMEKey/IMECaret), the IME freeze/settle machinery (IMEFrozen, markIMEScrollActive, imeSettleChan), and the window-relative imeRuneToByte. Also fixed along the way (both found while chasing the corruption): - real.ReadFileAt: loop over short reads. A single ReadAt on Android FUSE can return a short read, silently truncating a chunk and shifting every byte offset after it. - logic: a late lazy-chunk result no longer clobbers a buffer that SetContent has already fully loaded. - e2e: large-file IME test (1.6 MB file, fling + commit). - app icon (scripts/make_icon.py + cmd/pad/appicon.png) so gogio builds the mipmap/adaptive icon set. Verified: go vet + staticcheck, go test -race (all packages), and the emulator scenario loop (open moby excerpt, fling to mid-file, tap, type 'a', byte-compare the saved file) 75/75 clean.
196 lines
8.1 KiB
Python
196 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate cmd/pad/appicon.png, the Pad app icon.
|
|
|
|
Design: a soft, light cream pillow on a dark ink field, with "P_" (the
|
|
underscore standing in for the text cursor) embroidered on it in slate
|
|
thread. gogio picks appicon.png up automatically from the main package
|
|
(cmd/pad) and builds the Android mipmap/adaptive-icon set and the iOS
|
|
icon set from it.
|
|
|
|
Usage: scripts/make_icon.py
|
|
Requires: python3 with PIL.
|
|
"""
|
|
import pathlib
|
|
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
|
|
|
SIZE = 1024
|
|
INK = (32, 38, 50) # background
|
|
PILLOW = (243, 231, 211) # base fabric
|
|
PILLOW_LIT = (252, 246, 234)
|
|
PILLOW_RIM = (206, 187, 158)
|
|
THREAD = (92, 104, 134) # embroidered slate thread
|
|
THREAD_DARK = (58, 67, 92)
|
|
|
|
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"
|
|
|
|
|
|
def rrect_mask(w, h, r):
|
|
m = Image.new("L", (w, h), 0)
|
|
ImageDraw.Draw(m).rounded_rectangle([0, 0, w - 1, h - 1], radius=r, fill=255)
|
|
return m
|
|
|
|
|
|
def main() -> None:
|
|
img = Image.new("RGB", (SIZE, SIZE), INK)
|
|
|
|
# --- pillow geometry (kept inside the adaptive-icon safe zone) ---
|
|
pw, ph, pr = 672, 612, 195 # pillow size + corner radius
|
|
px0, py0 = (SIZE - pw) // 2, (SIZE - ph) // 2 + 6
|
|
|
|
# --- soft drop shadow (slightly below, so light comes from above) ---
|
|
shadow = Image.new("L", (SIZE, SIZE), 0)
|
|
d = ImageDraw.Draw(shadow)
|
|
d.rounded_rectangle([px0 + 16, py0 + 30, px0 + pw - 16, py0 + ph + 36],
|
|
radius=pr + 10, fill=120)
|
|
shadow = shadow.filter(ImageFilter.GaussianBlur(42))
|
|
img = Image.composite(Image.new("RGB", (SIZE, SIZE), (18, 21, 29)), img,
|
|
shadow)
|
|
|
|
# --- pillow body: plump radial shading ---
|
|
base = Image.new("RGB", (SIZE, SIZE), INK)
|
|
base.paste(Image.new("RGB", (pw, ph), PILLOW), (px0, py0))
|
|
sharp = rrect_mask(pw, ph, pr)
|
|
inner = sharp.filter(ImageFilter.GaussianBlur(pr * 0.8))
|
|
lit = Image.new("RGB", (pw, ph), PILLOW_LIT)
|
|
rim = Image.new("RGB", (pw, ph), PILLOW_RIM)
|
|
base.paste(lit, (px0, py0), inner) # bright center
|
|
rim_mask = Image.composite(Image.new("L", (pw, ph), 0), sharp,
|
|
inner) # edge only
|
|
rim_mask = rim_mask.point(lambda v: min(255, v * 2))
|
|
base.paste(rim, (px0, py0), rim_mask) # shaded edge
|
|
|
|
# top sheen: an out-of-focus light patch
|
|
sheen = Image.new("L", (SIZE, SIZE), 0)
|
|
d = ImageDraw.Draw(sheen)
|
|
d.ellipse([px0 + 110, py0 + 30, px0 + pw - 110, py0 + ph // 2 + 40],
|
|
fill=80)
|
|
sheen = sheen.filter(ImageFilter.GaussianBlur(75))
|
|
base = Image.composite(Image.new("RGB", (SIZE, SIZE), PILLOW_LIT),
|
|
base, sheen)
|
|
|
|
# corner dimples: soft darkening where a puffed pillow pinches
|
|
for cx, cy in [
|
|
(px0 + pr - 20, py0 + pr - 20), (px0 + pw - pr + 20, py0 + pr - 20),
|
|
(px0 + pr - 20, py0 + ph - pr + 20),
|
|
(px0 + pw - pr + 20, py0 + ph - pr + 20),
|
|
]:
|
|
dip = Image.new("L", (SIZE, SIZE), 0)
|
|
ImageDraw.Draw(dip).ellipse([cx - 70, cy - 70, cx + 70, cy + 70],
|
|
fill=55)
|
|
dip = dip.filter(ImageFilter.GaussianBlur(30))
|
|
base = Image.composite(
|
|
Image.blend(base, Image.new("RGB", (SIZE, SIZE),
|
|
(214, 196, 168)), 0.5),
|
|
base, dip)
|
|
|
|
# --- dashed seam line, like a quilted pillow ---
|
|
# Sample the rounded-rect path finely, then draw dash segments along it.
|
|
m = 44 # margin from the pillow edge
|
|
r = pr - m # corner radius of the seam
|
|
import math as _m
|
|
# Corners clockwise: center, angle start -> end (screen coords, y down).
|
|
# Each arc is followed by the straight side to the next arc's start.
|
|
arcs = [
|
|
(m + r, m + r, _m.pi, 1.5 * _m.pi), # top-left
|
|
(pw - m - r, m + r, 1.5 * _m.pi, 2 * _m.pi), # top-right
|
|
(pw - m - r, ph - m - r, 0, 0.5 * _m.pi), # bottom-right
|
|
(m + r, ph - m - r, 0.5 * _m.pi, _m.pi), # bottom-left
|
|
]
|
|
samples = []
|
|
for i, (cx, cy, a0, a1) in enumerate(arcs):
|
|
nx, ny, na0, _ = arcs[(i + 1) % 4]
|
|
n = max(2, int(abs(a1 - a0) * r // 3))
|
|
for k in range(n + 1):
|
|
a = a0 + (a1 - a0) * k / n
|
|
samples.append((cx + r * _m.cos(a), cy + r * _m.sin(a)))
|
|
# straight side from this arc's end to the next arc's start
|
|
end = (nx + r * _m.cos(na0), ny + r * _m.sin(na0))
|
|
last = samples[-1]
|
|
n = max(2, int(_m.hypot(end[0] - last[0], end[1] - last[1]) // 3))
|
|
for k in range(1, n + 1):
|
|
samples.append((last[0] + (end[0] - last[0]) * k / n,
|
|
last[1] + (end[1] - last[1]) * k / n))
|
|
# cumulative arc length
|
|
cum = [0.0]
|
|
for i in range(1, len(samples)):
|
|
cum.append(cum[-1] + _m.hypot(samples[i][0] - samples[i - 1][0],
|
|
samples[i][1] - samples[i - 1][1]))
|
|
total = cum[-1]
|
|
dash, gap = 26.0, 18.0
|
|
seam_ring = Image.new("L", (pw, ph), 0)
|
|
d = ImageDraw.Draw(seam_ring)
|
|
s0 = 0.0
|
|
while s0 < total:
|
|
s1 = min(s0 + dash, total)
|
|
pts = [(x, y) for (x, y), c in zip(samples, cum) if s0 <= c <= s1]
|
|
if len(pts) > 1:
|
|
d.line(pts, fill=255, width=9, joint="curve")
|
|
s0 += dash + gap
|
|
seam_ring = seam_ring.filter(ImageFilter.GaussianBlur(1.2))
|
|
seam = seam_ring
|
|
seam_full = Image.new("L", (SIZE, SIZE), 0)
|
|
seam_full.paste(seam, (px0, py0))
|
|
seam_col = Image.new("RGB", (SIZE, SIZE), (203, 183, 154))
|
|
base = Image.composite(seam_col, base, seam_full)
|
|
|
|
# composite pillow onto background via its sharp mask
|
|
full_sharp = Image.new("L", (SIZE, SIZE), 0)
|
|
full_sharp.paste(sharp, (px0, py0))
|
|
img.paste(base, (0, 0), full_sharp)
|
|
|
|
# --- embroidered "P_" ---
|
|
# Center the CAP of the P on the pillow (optical center); the
|
|
# underscore hangs below the baseline like a cursor on the next line.
|
|
font = ImageFont.truetype(FONT, 300)
|
|
# Drawing "P" at (x, y) puts its cap top at y + pb[1]; center the cap
|
|
# on the pillow, keep the whole "P_" string on the same baseline.
|
|
probe = ImageDraw.Draw(img)
|
|
pb = probe.textbbox((0, 0), "P", font=font)
|
|
cb_h = pb[3] - pb[1] # cap height
|
|
ty = SIZE // 2 - cb_h // 2 - pb[1]
|
|
tb = probe.textbbox((0, 0), "P_", font=font)
|
|
tw, th = tb[2] - tb[0], tb[3] - tb[1]
|
|
tx = (SIZE - tw) // 2 - tb[0]
|
|
|
|
# stitch texture: fine diagonal thread lines, tiled
|
|
tile = 6
|
|
pat = Image.new("L", (tile, tile), 160)
|
|
ImageDraw.Draw(pat).line([(0, tile - 1), (tile - 1, 0)], fill=255,
|
|
width=1)
|
|
pat = pat.filter(ImageFilter.GaussianBlur(0.5))
|
|
mw, mh = tw + 40, tb[3] + 80
|
|
stitch = pat.resize((mw, mh), Image.NEAREST)
|
|
|
|
# text mask in local coords
|
|
# local (0,0) == the absolute drawing origin (tx, ty); paste happens at
|
|
# (tx-20, ty-20), so text goes at (20 - tb[0], 20).
|
|
mask = Image.new("L", (mw, mh), 0)
|
|
ImageDraw.Draw(mask).text((20 - tb[0], 20), "P_", font=font,
|
|
fill=255)
|
|
|
|
# drop shadow of the stitches on the fabric (thread sits on top of it)
|
|
sh = mask.filter(ImageFilter.GaussianBlur(5))
|
|
sh_full = Image.new("L", (SIZE, SIZE), 0)
|
|
sh_full.paste(sh, (tx - 20, ty - 20 + 7))
|
|
img = Image.composite(Image.new("RGB", (SIZE, SIZE), (182, 164, 140)),
|
|
img, sh_full)
|
|
|
|
# thread color modulated by the stitch pattern (subtle per-stitch depth)
|
|
stitch_full = Image.new("L", (SIZE, SIZE), 0)
|
|
stitch_full.paste(stitch, (tx - 20, ty - 20))
|
|
mod = Image.composite(Image.new("RGB", (SIZE, SIZE), THREAD_DARK),
|
|
Image.new("RGB", (SIZE, SIZE), THREAD),
|
|
stitch_full)
|
|
mask_full = Image.new("L", (SIZE, SIZE), 0)
|
|
mask_full.paste(mask, (tx - 20, ty - 20))
|
|
img = Image.composite(mod, img, mask_full)
|
|
|
|
out = pathlib.Path(__file__).resolve().parent.parent / "cmd" / "pad" / "appicon.png"
|
|
img.save(out)
|
|
print(f"wrote {out} ({SIZE}x{SIZE})")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|