Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dev/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export function mockScreen(options: { kitty?: boolean; W?: number; H?: number }
}
post?.(buf)
},
// Same pass, but the caller paints every cell itself, so a test can hand the engine a full UI (a popup) with its
// own per-cell colors and see what the engine gives back.
ui(paint: (i: number, x: number, y: number, buffers: typeof buffers) => void) {
const buf = { width: w, height: h, buffers }
layer?.(buf)
for (let i = 0; i < w * h; i++) paint(i, i % w, (i / w) | 0, buffers)
post?.(buf)
},
}
}

Expand Down
70 changes: 70 additions & 0 deletions dev/panels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Checks that the scene keeps out of a panel the UI draws over the page: with a dialog open, no cell of the dialog
// window may get scene colors or glyphs, on any frame, including the surface recount every 10 frames.
// bun dev/panels.ts [wallpaper]
import { seed } from "./seed"
import { H, W, mockScreen, session } from "./mock"
import { wallpapers } from "./wallpapers"

const id = process.argv[2] ?? "ocean"
seed(1)
const screen = mockScreen()
screen.engine.start(wallpapers.find((w) => w.id === id)!, { activity: "calm", time: "day" })

// A dark theme's own colors, all near-neutral like the ones isSurface accepts: the page, the dialog window over it, and
// the dialog's dimmed backdrop around the window.
const PAGE = [6, 35, 29]
const DIALOG = [10, 45, 38]
const SCRIM = [3, 17, 15]
const win = { x0: Math.floor(W / 2) - 30, x1: Math.floor(W / 2) + 30, y0: Math.floor(H / 4), y1: H - Math.floor(H / 4) }
const swin = { x0: win.x0 - 4, x1: win.x1 + 4, y0: win.y0 - 2, y1: win.y1 + 2 }
const OPEN = 40
const RUN = 40
const cells = W * H
const inWindow = (i: number) => {
const x = i % W
const y = (i / W) | 0
return x >= win.x0 && x < win.x1 && y >= win.y0 && y < win.y1
}

type Snap = { char: Uint32Array; bg: Uint16Array }
const snaps: Snap[] = []
for (let f = 0; f < OPEN + RUN; f++) {
if (f >= OPEN) {
screen.ui((i, x, y, buffers) => {
const inside = x >= win.x0 && x < win.x1 && y >= win.y0 && y < win.y1
const scrim = x >= swin.x0 && x < swin.x1 && y >= swin.y0 && y < swin.y1
buffers.char[i] = 32
buffers.bg.set([...(inside ? DIALOG : scrim ? SCRIM : PAGE), 255], i * 4)
buffers.fg.set([...PAGE, 255], i * 4)
})
} else screen.frame(session(W, H))
if (f >= OPEN) snaps.push({ char: new Uint32Array(screen.buffers.char), bg: new Uint16Array(screen.buffers.bg) })
}
screen.engine.stop()

let repainted = 0
let glyphs = 0
let recounted = 0
for (let s = 1; s < snaps.length; s++) {
const a = snaps[s - 1]
const b = snaps[s]
let frameRepaint = 0
for (let i = 0; i < cells; i++) {
if (!inWindow(i)) continue
let diff = a.char[i] !== b.char[i]
for (let c = 0; c < 3; c++) if (a.bg[i * 4 + c] !== b.bg[i * 4 + c]) diff = true
if (diff) frameRepaint++
}
repainted += frameRepaint
if ((OPEN + s) % 10 === 0) recounted += frameRepaint
}
const last = snaps[snaps.length - 1]
for (let i = 0; i < cells; i++) if (inWindow(i) && last.char[i] !== 32) glyphs++

const size = (win.x1 - win.x0) * (win.y1 - win.y0)
console.log(`dialog window ${size} cells: ${glyphs} carry a scene glyph, ${repainted} cell repaints over the run (${recounted} of them on surface recounts)`)
if (glyphs || repainted) {
console.log(`FAIL: the scene paints into the dialog window`)
process.exit(1)
}
console.log("ok")
26 changes: 20 additions & 6 deletions src/engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@ export function createEngine(
dump?: string
// Writes the first frame past the middle of a crossfade, likewise.
dumpFade?: string
// The theme's own page background: the color the UI paints where nothing covers it. Passed in rather than guessed,
// because the most common color in a frame is a widget's surface whenever a screenful of dialogs, messages or code
// covers the page, and painting the scene there is what makes those widgets flicker.
background?: RGB
layer?: (renderer: Context["renderer"], draw: (buffer: OptimizedBuffer) => void) => { dispose(): void }
} = {},
): Engine {
Expand All @@ -767,6 +771,9 @@ export function createEngine(
let dumped = false
let fadeDumped = false
let base = -2
// The theme's page background, when the caller knows it: the scene paints only where the UI used it, so a dialog,
// a menu, a message or a code block keeps its own surface. -2 means "not known", and the most common color is used.
const pageBase = options.background ? ((options.background[0] & 255) << 16) | ((options.background[1] & 255) << 8) | (options.background[2] & 255) : -2
let mask = new Float32Array(0)
const quad = new Float32Array(24)
const order = new Uint8Array(8)
Expand Down Expand Up @@ -1112,13 +1119,17 @@ export function createEngine(
const k = key(bg, i * 4)
counts.set(k, (counts.get(k) ?? 0) + 1)
}
base = -2
let best = 0
for (const [k, n] of counts) if (n > best) ((best = n), (base = k))
const known = pageBase !== -2
base = known ? pageBase : -2
if (!known) {
let best = 0
for (const [k, n] of counts) if (n > best) ((best = n), (base = k))
}
surfaces.clear()
tints.clear()
surfaces.add(base)
for (const [k, n] of counts) if (k !== base && n > cells * 0.002 && isSurface(k, light)) surfaces.add(k)
// When the page color is known, every other surface is a widget's, not the page: none of them shows the scene.
if (!known) for (const [k, n] of counts) if (k !== base && n > cells * 0.002 && isSurface(k, light)) surfaces.add(k)
}
const look = LEVELS[settings!.brightness ?? "normal"]
const curve = light ? look.light : look.dark
Expand All @@ -1139,9 +1150,12 @@ export function createEngine(
for (let i = 0; i < cells; i++) {
const k = key(bg, i * 4)
const tint = tintOf(k)
const on = tint !== NO_TINT
// The scene shows only where the UI painted the page's own background. A cell whose background is some other
// color - a dialog, a menu, a message or tool block, the prompt - belongs to the UI, and tinting it or fitting
// scene glyphs into it makes the wallpaper show through that widget and flicker as the scene moves.
const on = tint !== NO_TINT && k === base
surface[i] = on ? 1 : 0
cellTint[i] = tint
cellTint[i] = on ? tint : NO_TINT
// 1 for text, 2 for a blank cell of a panel (a surface other than the base, such as a dialog), else 0.
const shape = !on ? 0 : char[i] !== SPACE && !(isBlock(char[i]) && tintOf(fgKey(fg, i * 4)) !== NO_TINT) ? 1 : k !== base && tint === TINT_ZERO ? 2 : 0
if (shapes[i] !== shape) ((shapes[i] = shape), (reshaped = true))
Expand Down
9 changes: 8 additions & 1 deletion tui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ export default Plugin.define({
if (external.skipped.length) context.ui.toast.show({ message: `Skipped wallpapers:\n${external.skipped.join("\n")}`, variant: "error" })
const wallpapers = [...WALLPAPERS, ...external.wallpapers]
const collections = [...new Set(wallpapers.map(collectionOf))]
const engine = createEngine(context, { dump: process.env.WALLPAPER_DUMP, dumpFade: process.env.WALLPAPER_DUMP_FADE })
// The theme's own page color: what the UI paints where nothing covers it, so the scene can leave every other
// surface - a dialog, a menu, a message or a code block - to the UI.
const page = context.theme.background.base
const engine = createEngine(context, {
dump: process.env.WALLPAPER_DUMP,
dumpFade: process.env.WALLPAPER_DUMP_FADE,
background: page ? [page.r, page.g, page.b] : undefined,
})
// Environment overrides for development; they win over the stored choice.
const pinned = process.env.WALLPAPER
const pinnedActivity = process.env.WALLPAPER_ACTIVITY
Expand Down