Skip to content
Open
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
444 changes: 444 additions & 0 deletions packages/core/docs/wide-grapheme-alpha-blending-tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 17 additions & 3 deletions packages/core/src/Renderable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1333,15 +1333,15 @@ export abstract class Renderable extends BaseRenderable {

const shouldPushScissor = this._overflow !== "visible" && this.width > 0 && this.height > 0
if (shouldPushScissor) {
const scissorRect = this.getScissorRect()
const scissorRect = this.getOverflowClipRect()
renderList.push({
action: "pushScissorRect",
x: scissorRect.x,
y: scissorRect.y,
width: scissorRect.width,
height: scissorRect.height,
screenX: this.x,
screenY: this.y,
screenX: scissorRect.x,
screenY: scissorRect.y,
})
}
const visibleChildren = this._getVisibleChildren()
Expand Down Expand Up @@ -1403,6 +1403,20 @@ export abstract class Renderable extends BaseRenderable {
}
}

public getOverflowClipRect(): { x: number; y: number; width: number; height: number } {
const scissorRect = this.getScissorRect()
if (!this.buffered) {
return scissorRect
}

return {
x: scissorRect.x + this.x,
y: scissorRect.y + this.y,
width: scissorRect.width,
height: scissorRect.height,
}
}

protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {
// Default implementation: do nothing
// Override this method to provide custom rendering
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ export class OptimizedBuffer {
this.lib.bufferFillRect(this.bufferPtr, x, y, width, height, bg)
}

// Clip any wide grapheme spans crossed by this fill instead of tinting the
// full span. Overlay edge bands use this to keep box geometry straight.
public fillRectClipWideGraphemes(x: number, y: number, width: number, height: number, bg: RGBA): void {
this.guard()
this.lib.bufferFillRectClipWideGraphemes(this.bufferPtr, x, y, width, height, bg)
}

public colorMatrix(
matrix: Float32Array,
cellMask: Float32Array,
Expand Down
45 changes: 36 additions & 9 deletions packages/core/src/examples/wide-grapheme-overlay-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
OptimizedBuffer,
t,
bold,
underline,
fg,
type MouseEvent,
type KeyEvent,
Expand All @@ -16,12 +15,12 @@ import type { CliRenderer, RenderContext } from "../index.js"
import { setupCommonDemoKeys } from "./lib/standalone-keys.js"

const GRAPHEME_LINES: string[] = [
"東京都 北京市 서울시 大阪府 名古屋 横浜市 上海市",
"👨‍👩‍👧‍👦 👩🏽‍💻 🏳️‍🌈 🇺🇸 🇩🇪 🇯🇵 🇮🇳 家族 絵文字 🎉🎊🎈",
"こんにちは世界 你好世界 안녕하세요 สวัสดี مرحبا",
"漢字テスト 中文测试 한국어 日本語 繁體中文 简体中文",
"🚀 Full-width: ABCDEF Half: abcdef ½ ⅞ ⅓",
"混合テキスト mixed text with 漢字 and emoji 🎯",
"W2 CJK: 東京都 北京市 서울시 大阪府 名古屋 横浜市 上海市",
"W2 emoji: 👨‍👩‍👧‍👦 👩🏽‍💻 👩‍🚀 🇺🇸 🇩🇪 🇯🇵 🇮🇳 🎉🎊🎈",
"VS16 off/on: ♥ / ♥️ ☀ / ☀️ ✈ / ✈️ ☎ / ☎️ ☺ / ☺️ ✂ / ✂️",
"ZWJ + flags: 👩‍🚀 🧑‍🚀 👨‍👩‍👧‍👦 🧑‍💻 🇺🇸 🇩🇪 🇯🇵 🇮🇳",
"Mixed: こんにちは世界 你好世界 안녕하세요 สวัสดี مرحبا",
"Half/full: ABCDEF abcdef ½ ⅞ ⅓ 漢字 emoji 🎯",
]

const HEADER_HEIGHT = 2
Expand All @@ -37,6 +36,7 @@ class DraggableBox extends BoxRenderable {
private dragOffsetX = 0
private dragOffsetY = 0
private alphaPercentage: number
private showAlphaLabel: boolean

constructor(
ctx: RenderContext,
Expand All @@ -47,23 +47,33 @@ class DraggableBox extends BoxRenderable {
height: number,
bg: RGBA,
zIndex: number,
bordered = false,
showAlphaLabel = true,
) {
super(ctx, {
id,
width,
height,
zIndex,
backgroundColor: bg,
border: bordered,
borderColor: bordered ? RGBA.fromInts(255, 255, 255, 235) : undefined,
borderStyle: bordered ? "rounded" : undefined,
position: "absolute",
left: x,
top: y,
})
this.alphaPercentage = Math.round(bg.a * 100)
this.showAlphaLabel = showAlphaLabel
}

protected renderSelf(buffer: OptimizedBuffer): void {
super.renderSelf(buffer)

if (!this.showAlphaLabel) {
return
}

const alphaText = `${this.alphaPercentage}%`
const centerX = this.x + Math.floor(this.width / 2 - alphaText.length / 2)
const centerY = this.y + Math.floor(this.height / 2)
Expand Down Expand Up @@ -94,7 +104,7 @@ class DraggableBox extends BoxRenderable {
const newY = event.y - this.dragOffsetY

this.x = Math.max(0, Math.min(newX, this._ctx.width - this.width))
this.y = Math.max(0, Math.min(newY, this._ctx.height - this.height))
this.y = Math.max(HEADER_HEIGHT, Math.min(newY, this._ctx.height - this.height))

event.stopPropagation()
}
Expand Down Expand Up @@ -139,7 +149,8 @@ function toggleScrim(renderer: CliRenderer) {
function updateHeader() {
if (!headerDisplay) return
const dimLabel = scrimVisible ? "D: hide scrim" : "D: show scrim"
headerDisplay.content = t`${bold(fg("#00D4AA")("Wide Grapheme Overlay"))} ${fg("#A8A8B2")(`| ${dimLabel} | Drag boxes over CJK/emoji | Ctrl+C: quit`)}`
headerDisplay.content = t`${bold(fg("#00D4AA")("Wide Grapheme Overlay"))} ${fg("#A8A8B2")(`| ${dimLabel} | Ctrl+C: quit`)}
${fg("#7A8394")("Compare the small bordered probe, one large bordered box, and the plain translucent fills across the CJK/emoji lines")}`
}

export function run(renderer: CliRenderer): void {
Expand Down Expand Up @@ -167,6 +178,21 @@ export function run(renderer: CliRenderer): void {
const background = new GraphemeBackground(renderer, "wg-background", renderer.terminalWidth, bgHeight)
root.add(background)

const edgeProbe = new DraggableBox(
renderer,
"wg-edge-probe",
11,
HEADER_HEIGHT,
6,
3,
RGBA.fromValues(64 / 255, 176 / 255, 255 / 255, 128 / 255),
90,
true,
false,
)
root.add(edgeProbe)
draggableBoxes.push(edgeProbe)

// Full-screen dimming scrim (same as opencode dialog backdrop: RGBA(0,0,0,150))
scrim = new BoxRenderable(renderer, {
id: "wg-scrim",
Expand All @@ -191,6 +217,7 @@ export function run(renderer: CliRenderer): void {
8,
RGBA.fromValues(64 / 255, 176 / 255, 255 / 255, 128 / 255),
100,
true,
)
root.add(box1)
draggableBoxes.push(box1)
Expand Down
Loading
Loading