Skip to content

Commit a45c871

Browse files
authored
Merge branch 'main' into node-module-res
2 parents 082c19c + 93c949f commit a45c871

File tree

2 files changed

+287
-0
lines changed

2 files changed

+287
-0
lines changed

packages/core/src/examples/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import * as grayscaleBufferDemo from "./grayscale-buffer-demo.js"
7272
import * as focusRestoreDemo from "./focus-restore-demo.js"
7373
import { setupCommonDemoKeys } from "./lib/standalone-keys.js"
7474
import * as corePluginSlotsDemo from "./core-plugin-slots-demo.js"
75+
import * as wideGraphemeOverlayDemo from "./wide-grapheme-overlay-demo.js"
7576

7677
interface Example {
7778
name: string
@@ -442,6 +443,12 @@ const examples: Example[] = [
442443
run: fullUnicodeExample.run,
443444
destroy: fullUnicodeExample.destroy,
444445
},
446+
{
447+
name: "Wide Grapheme Overlay Demo",
448+
description: "Drag transparent boxes over CJK/emoji, toggle dimming scrim with D key",
449+
run: wideGraphemeOverlayDemo.run,
450+
destroy: wideGraphemeOverlayDemo.destroy,
451+
},
445452
{
446453
name: "Split Mode Demo (Experimental)",
447454
description: "Renderer confined to bottom area with normal terminal output above",
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import {
2+
createCliRenderer,
3+
FrameBufferRenderable,
4+
RGBA,
5+
TextRenderable,
6+
BoxRenderable,
7+
OptimizedBuffer,
8+
t,
9+
bold,
10+
underline,
11+
fg,
12+
type MouseEvent,
13+
type KeyEvent,
14+
} from "../index.js"
15+
import type { CliRenderer, RenderContext } from "../index.js"
16+
import { setupCommonDemoKeys } from "./lib/standalone-keys.js"
17+
18+
const GRAPHEME_LINES: string[] = [
19+
"東京都 北京市 서울시 大阪府 名古屋 横浜市 上海市",
20+
"👨‍👩‍👧‍👦 👩🏽‍💻 🏳️‍🌈 🇺🇸 🇩🇪 🇯🇵 🇮🇳 家族 絵文字 🎉🎊🎈",
21+
"こんにちは世界 你好世界 안녕하세요 สวัสดี مرحبا",
22+
"漢字テスト 中文测试 한국어 日本語 繁體中文 简体中文",
23+
"🚀 Full-width: ABCDEF Half: abcdef ½ ⅞ ⅓",
24+
"混合テキスト mixed text with 漢字 and emoji 🎯",
25+
]
26+
27+
const HEADER_HEIGHT = 2
28+
29+
let nextZIndex = 101
30+
let draggableBoxes: DraggableBox[] = []
31+
let scrimVisible = false
32+
let scrim: BoxRenderable | null = null
33+
let headerDisplay: TextRenderable | null = null
34+
35+
class DraggableBox extends BoxRenderable {
36+
private isDragging = false
37+
private dragOffsetX = 0
38+
private dragOffsetY = 0
39+
private alphaPercentage: number
40+
41+
constructor(
42+
ctx: RenderContext,
43+
id: string,
44+
x: number,
45+
y: number,
46+
width: number,
47+
height: number,
48+
bg: RGBA,
49+
zIndex: number,
50+
) {
51+
super(ctx, {
52+
id,
53+
width,
54+
height,
55+
zIndex,
56+
backgroundColor: bg,
57+
position: "absolute",
58+
left: x,
59+
top: y,
60+
})
61+
this.alphaPercentage = Math.round(bg.a * 100)
62+
}
63+
64+
protected renderSelf(buffer: OptimizedBuffer): void {
65+
super.renderSelf(buffer)
66+
67+
const alphaText = `${this.alphaPercentage}%`
68+
const centerX = this.x + Math.floor(this.width / 2 - alphaText.length / 2)
69+
const centerY = this.y + Math.floor(this.height / 2)
70+
71+
buffer.drawText(alphaText, centerX, centerY, RGBA.fromInts(255, 255, 255, 220))
72+
}
73+
74+
protected onMouseEvent(event: MouseEvent): void {
75+
switch (event.type) {
76+
case "down":
77+
this.isDragging = true
78+
this.dragOffsetX = event.x - this.x
79+
this.dragOffsetY = event.y - this.y
80+
this.zIndex = nextZIndex++
81+
event.stopPropagation()
82+
break
83+
84+
case "drag-end":
85+
if (this.isDragging) {
86+
this.isDragging = false
87+
event.stopPropagation()
88+
}
89+
break
90+
91+
case "drag":
92+
if (this.isDragging) {
93+
const newX = event.x - this.dragOffsetX
94+
const newY = event.y - this.dragOffsetY
95+
96+
this.x = Math.max(0, Math.min(newX, this._ctx.width - this.width))
97+
this.y = Math.max(0, Math.min(newY, this._ctx.height - this.height))
98+
99+
event.stopPropagation()
100+
}
101+
break
102+
}
103+
}
104+
}
105+
106+
class GraphemeBackground extends FrameBufferRenderable {
107+
constructor(ctx: RenderContext, id: string, width: number, height: number) {
108+
super(ctx, {
109+
id,
110+
width,
111+
height,
112+
position: "absolute",
113+
left: 0,
114+
top: HEADER_HEIGHT,
115+
respectAlpha: false,
116+
})
117+
118+
this.fillGraphemes(width, height)
119+
}
120+
121+
private fillGraphemes(width: number, height: number) {
122+
const fgColor = RGBA.fromInts(220, 220, 220, 255)
123+
const bgColor = RGBA.fromInts(10, 14, 20, 255)
124+
this.frameBuffer.clear(bgColor)
125+
for (let y = 0; y < height; y++) {
126+
const line = GRAPHEME_LINES[y % GRAPHEME_LINES.length]
127+
this.frameBuffer.drawText(line, 2, y, fgColor, bgColor)
128+
}
129+
}
130+
}
131+
132+
function toggleScrim(renderer: CliRenderer) {
133+
scrimVisible = !scrimVisible
134+
if (scrim) scrim.visible = scrimVisible
135+
updateHeader()
136+
renderer.requestRender()
137+
}
138+
139+
function updateHeader() {
140+
if (!headerDisplay) return
141+
const dimLabel = scrimVisible ? "D: hide scrim" : "D: show scrim"
142+
headerDisplay.content = t`${bold(fg("#00D4AA")("Wide Grapheme Overlay"))} ${fg("#A8A8B2")(`| ${dimLabel} | Drag boxes over CJK/emoji | Ctrl+C: quit`)}`
143+
}
144+
145+
export function run(renderer: CliRenderer): void {
146+
renderer.start()
147+
renderer.setBackgroundColor("#0A0E14")
148+
149+
const root = new BoxRenderable(renderer, { id: "wg-overlay-root" })
150+
renderer.root.add(root)
151+
152+
// Header row
153+
headerDisplay = new TextRenderable(renderer, {
154+
id: "wg-header",
155+
height: HEADER_HEIGHT,
156+
position: "absolute",
157+
left: 2,
158+
top: 0,
159+
zIndex: 200,
160+
selectable: false,
161+
})
162+
updateHeader()
163+
root.add(headerDisplay)
164+
165+
// Background filled with repeating wide grapheme lines, below the header
166+
const bgHeight = renderer.terminalHeight - HEADER_HEIGHT
167+
const background = new GraphemeBackground(renderer, "wg-background", renderer.terminalWidth, bgHeight)
168+
root.add(background)
169+
170+
// Full-screen dimming scrim (same as opencode dialog backdrop: RGBA(0,0,0,150))
171+
scrim = new BoxRenderable(renderer, {
172+
id: "wg-scrim",
173+
position: "absolute",
174+
left: 0,
175+
top: HEADER_HEIGHT,
176+
width: renderer.terminalWidth,
177+
height: bgHeight,
178+
backgroundColor: RGBA.fromInts(0, 0, 0, 150),
179+
zIndex: 50,
180+
})
181+
scrim.visible = false
182+
root.add(scrim)
183+
184+
// Draggable boxes at various alpha levels
185+
const box1 = new DraggableBox(
186+
renderer,
187+
"wg-box-50",
188+
4,
189+
HEADER_HEIGHT + 1,
190+
25,
191+
8,
192+
RGBA.fromValues(64 / 255, 176 / 255, 255 / 255, 128 / 255),
193+
100,
194+
)
195+
root.add(box1)
196+
draggableBoxes.push(box1)
197+
198+
const box2 = new DraggableBox(
199+
renderer,
200+
"wg-box-75",
201+
20,
202+
HEADER_HEIGHT + 5,
203+
25,
204+
8,
205+
RGBA.fromValues(255 / 255, 107 / 255, 129 / 255, 192 / 255),
206+
100,
207+
)
208+
root.add(box2)
209+
draggableBoxes.push(box2)
210+
211+
const box3 = new DraggableBox(
212+
renderer,
213+
"wg-box-25",
214+
40,
215+
HEADER_HEIGHT + 3,
216+
25,
217+
8,
218+
RGBA.fromValues(139 / 255, 69 / 255, 193 / 255, 64 / 255),
219+
100,
220+
)
221+
root.add(box3)
222+
draggableBoxes.push(box3)
223+
224+
const box4 = new DraggableBox(
225+
renderer,
226+
"wg-box-opaque",
227+
60,
228+
HEADER_HEIGHT + 7,
229+
25,
230+
8,
231+
RGBA.fromValues(30 / 255, 30 / 255, 42 / 255, 1.0),
232+
100,
233+
)
234+
root.add(box4)
235+
draggableBoxes.push(box4)
236+
237+
renderer.keyInput.on("keypress", (key: KeyEvent) => {
238+
if (key.name === "d") {
239+
key.preventDefault()
240+
toggleScrim(renderer)
241+
}
242+
})
243+
244+
renderer.on("resize", (width: number, height: number) => {
245+
const h = height - HEADER_HEIGHT
246+
background.width = width
247+
background.height = h
248+
if (scrim) {
249+
scrim.width = width
250+
scrim.height = h
251+
}
252+
renderer.requestRender()
253+
})
254+
}
255+
256+
export function destroy(renderer: CliRenderer): void {
257+
renderer.clearFrameCallbacks()
258+
259+
for (const box of draggableBoxes) {
260+
renderer.root.remove(box.id)
261+
}
262+
draggableBoxes = []
263+
nextZIndex = 101
264+
scrimVisible = false
265+
scrim = null
266+
headerDisplay = null
267+
268+
renderer.root.remove("wg-overlay-root")
269+
renderer.setCursorPosition(0, 0, false)
270+
}
271+
272+
if (import.meta.main) {
273+
const renderer = await createCliRenderer({
274+
exitOnCtrlC: true,
275+
})
276+
277+
run(renderer)
278+
setupCommonDemoKeys(renderer)
279+
renderer.start()
280+
}

0 commit comments

Comments
 (0)