Skip to content
Merged
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
11 changes: 9 additions & 2 deletions packages/core/src/Renderable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,8 +475,8 @@ export abstract class Renderable extends BaseRenderable {
public set translateX(value: number) {
if (this._translateX === value) return
this._translateX = value
this.requestRender()
if (this.parent) this.parent.childrenPrimarySortDirty = true
this.requestRender()
}

public get translateY(): number {
Expand All @@ -486,8 +486,8 @@ export abstract class Renderable extends BaseRenderable {
public set translateY(value: number) {
if (this._translateY === value) return
this._translateY = value
this.requestRender()
if (this.parent) this.parent.childrenPrimarySortDirty = true
this.requestRender()
}

public get x(): number {
Expand Down Expand Up @@ -1289,6 +1289,8 @@ export abstract class Renderable extends BaseRenderable {
y: scissorRect.y,
width: scissorRect.width,
height: scissorRect.height,
screenX: this.x,
screenY: this.y,
})
}
const visibleChildren = this._getVisibleChildren()
Expand Down Expand Up @@ -1524,6 +1526,8 @@ interface RenderCommandPushScissorRect extends RenderCommandBase {
y: number
width: number
height: number
screenX: number
screenY: number
}

interface RenderCommandPopScissorRect extends RenderCommandBase {
Expand Down Expand Up @@ -1594,6 +1598,7 @@ export class RootRenderable extends Renderable {
this.updateLayout(deltaTime, this.renderList)

// 3. Render all collected renderables
this._ctx.clearHitGridScissorRects()
for (let i = 1; i < this.renderList.length; i++) {
const command = this.renderList[i]
switch (command.action) {
Expand All @@ -1605,9 +1610,11 @@ export class RootRenderable extends Renderable {
break
case "pushScissorRect":
buffer.pushScissorRect(command.x, command.y, command.width, command.height)
this._ctx.pushHitGridScissorRect(command.screenX, command.screenY, command.width, command.height)
break
case "popScissorRect":
buffer.popScissorRect()
this._ctx.popHitGridScissorRect()
break
case "pushOpacity":
buffer.pushOpacity(command.opacity)
Expand Down
112 changes: 112 additions & 0 deletions packages/core/src/examples/scrollbox-mouse-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env bun
import { BoxRenderable, type CliRenderer, createCliRenderer, TextRenderable, RGBA, t, fg, bold } from "../index"
import { ScrollBoxRenderable } from "../renderables/ScrollBox"
import { setupCommonDemoKeys } from "./lib/standalone-keys"

let scrollBox: ScrollBoxRenderable | null = null
let statusText: TextRenderable | null = null
let hoveredItem: string | null = null

export function run(renderer: CliRenderer): void {
renderer.setBackgroundColor("#1a1b26")

const mainContainer = new BoxRenderable(renderer, {
id: "main-container",
flexGrow: 1,
maxHeight: "100%",
maxWidth: "100%",
flexDirection: "column",
backgroundColor: "#1a1b26",
})

const header = new BoxRenderable(renderer, {
id: "header",
width: "100%",
height: 3,
backgroundColor: "#24283b",
paddingLeft: 1,
flexShrink: 0,
})

const title = new TextRenderable(renderer, {
content: t`${bold(fg("#7aa2f7")("ScrollBox Mouse Hit Test"))} - Scroll and hover items to test hit detection`,
})
header.add(title)

statusText = new TextRenderable(renderer, {
content: t`${fg("#565f89")("Hovered:")} ${fg("#c0caf5")("none")}`,
})
header.add(statusText)

scrollBox = new ScrollBoxRenderable(renderer, {
id: "scroll-box",
rootOptions: {
backgroundColor: "#24283b",
border: true,
},
contentOptions: {
backgroundColor: "#16161e",
},
})

for (let i = 0; i < 50; i++) {
const item = new BoxRenderable(renderer, {
id: `item-${i}`,
width: "100%",
height: 2,
backgroundColor: i % 2 === 0 ? "#292e42" : "#2f3449",
paddingLeft: 1,
onMouseOver: () => {
hoveredItem = `item-${i}`
updateStatus()
},
onMouseOut: () => {
if (hoveredItem === `item-${i}`) {
hoveredItem = null
updateStatus()
}
},
onClick: () => {
console.log(`Clicked item-${i}`)
},
})

const text = new TextRenderable(renderer, {
content: t`${fg("#7aa2f7")(`[${i.toString().padStart(2, "0")}]`)} ${fg("#c0caf5")(`Item ${i} - Hover over me to test hit detection`)}`,
})
item.add(text)
scrollBox.add(item)
}

mainContainer.add(header)
mainContainer.add(scrollBox)
renderer.root.add(mainContainer)

scrollBox.focus()

function updateStatus() {
if (statusText) {
const hovered = hoveredItem || "none"
statusText.content = t`${fg("#565f89")("Hovered:")} ${fg("#9ece6a")(hovered)}`
}
}
}

export function destroy(renderer: CliRenderer): void {
renderer.root.getChildren().forEach((child) => {
renderer.root.remove(child.id)
child.destroyRecursively()
})
scrollBox = null
statusText = null
hoveredItem = null
}

if (import.meta.main) {
const renderer = await createCliRenderer({
exitOnCtrlC: true,
})

run(renderer)
setupCommonDemoKeys(renderer)
}
206 changes: 206 additions & 0 deletions packages/core/src/examples/scrollbox-overlay-hit-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env bun
import {
BoxRenderable,
type CliRenderer,
createCliRenderer,
TextRenderable,
t,
fg,
bold,
type KeyEvent,
} from "../index"
import { ScrollBoxRenderable } from "../renderables/ScrollBox"
import { setupCommonDemoKeys } from "./lib/standalone-keys"

let overlay: BoxRenderable | null = null
let dialog: BoxRenderable | null = null
let scrollBox: ScrollBoxRenderable | null = null
let baseStatusText: TextRenderable | null = null
let dialogStatusText: TextRenderable | null = null
let keyHandler: ((key: KeyEvent) => void) | null = null
let dialogOpen = false
let lastClick = "none"

const updateStatus = () => {
const content = t`${fg("#9aa5ce")("Last click:")} ${fg("#9ece6a")(lastClick)}`
if (baseStatusText) {
baseStatusText.content = content
}
if (dialogStatusText) {
dialogStatusText.content = content
}
}

const setDialogVisible = (visible: boolean) => {
dialogOpen = visible
if (overlay) {
overlay.visible = visible
}
}

const setLastClick = (value: string) => {
lastClick = value
updateStatus()
}

export function run(renderer: CliRenderer): void {
renderer.setBackgroundColor("#1a1b26")

const app = new BoxRenderable(renderer, {
id: "app",
flexDirection: "column",
width: "100%",
height: "100%",
backgroundColor: "#1a1b26",
paddingLeft: 1,
paddingTop: 1,
gap: 1,
})
renderer.root.add(app)

const title = new TextRenderable(renderer, {
content: t`${bold(fg("#7aa2f7")("Scrollbox Overlay Hit Test"))}`,
})
app.add(title)

const instructions = new TextRenderable(renderer, {
content: t`${fg("#c0caf5")("Press 'd' to toggle dialog, 'esc' to close, 'q' to quit")}`,
})
app.add(instructions)

baseStatusText = new TextRenderable(renderer, {
content: t`${fg("#9aa5ce")("Last click:")} ${fg("#9ece6a")(lastClick)}`,
})
app.add(baseStatusText)

overlay = new BoxRenderable(renderer, {
id: "overlay",
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "#ff000033",
zIndex: 100,
visible: false,
onMouseDown: () => {
setLastClick("overlay (red)")
setDialogVisible(false)
},
})
renderer.root.add(overlay)

dialog = new BoxRenderable(renderer, {
id: "dialog",
position: "absolute",
top: "25%",
left: "25%",
width: "50%",
height: "50%",
flexDirection: "column",
gap: 1,
padding: 1,
backgroundColor: "#0f172a",
border: true,
borderColor: "#7aa2f7",
onMouseDown: (event) => {
setLastClick("dialog (blue)")
event.stopPropagation()
},
})
overlay.add(dialog)

const dialogTitle = new TextRenderable(renderer, {
content: t`${bold(fg("#7aa2f7")("Dialog"))} ${fg("#565f89")("- scroll, then click outside the list")}`,
})
dialog.add(dialogTitle)

const dialogHint = new TextRenderable(renderer, {
content: t`${fg("#c0caf5")("Click the red overlay above/below the dialog to close it")}`,
})
dialog.add(dialogHint)

dialogStatusText = new TextRenderable(renderer, {
content: t`${fg("#9aa5ce")("Last click:")} ${fg("#9ece6a")(lastClick)}`,
})
dialog.add(dialogStatusText)

scrollBox = new ScrollBoxRenderable(renderer, {
id: "scrollbox",
flexGrow: 1,
scrollY: true,
onMouseDown: (event) => {
setLastClick("scrollbox (yellow)")
event.stopPropagation()
},
rootOptions: {
backgroundColor: "#eab308",
border: true,
borderColor: "#0f172a",
},
contentOptions: {
backgroundColor: "#111827",
},
})
dialog.add(scrollBox)

for (let i = 0; i < 50; i++) {
const item = new BoxRenderable(renderer, {
id: `line-${i}`,
width: "100%",
height: 1,
paddingLeft: 1,
backgroundColor: i % 2 === 0 ? "#1f2937" : "#111827",
})
const text = new TextRenderable(renderer, {
content: t`${fg("#cbd5f5")(`Line ${i + 1}: This is some content`)}`,
})
item.add(text)
scrollBox.add(item)
}

keyHandler = (key: KeyEvent) => {
if (key.name === "q") {
renderer.destroy()
process.exit(0)
}
if (key.name === "d") {
setDialogVisible(!dialogOpen)
}
if (key.name === "escape") {
setDialogVisible(false)
}
}
renderer.keyInput.on("keypress", keyHandler)

updateStatus()
}

export function destroy(renderer: CliRenderer): void {
if (keyHandler) {
renderer.keyInput.off("keypress", keyHandler)
}

renderer.root.getChildren().forEach((child) => {
renderer.root.remove(child.id)
child.destroyRecursively()
})

overlay = null
dialog = null
scrollBox = null
baseStatusText = null
dialogStatusText = null
keyHandler = null
dialogOpen = false
lastClick = "none"
}

if (import.meta.main) {
const renderer = await createCliRenderer({
exitOnCtrlC: true,
})

run(renderer)
setupCommonDemoKeys(renderer)
}
Loading
Loading