Skip to content

Commit 7e2858e

Browse files
authored
feat: add scroll-box component (anomalyco#120)
1 parent 02692cd commit 7e2858e

File tree

7 files changed

+621
-10
lines changed

7 files changed

+621
-10
lines changed

packages/core/src/Renderable.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ export interface RenderableOptions<T extends Renderable = Renderable> extends Pa
101101
onMouseScroll?: (this: T, event: MouseEvent) => void
102102

103103
onKeyDown?: (key: ParsedKey) => void
104+
105+
onSizeChange?: (this: T) => void
104106
}
105107

106108
function validateOptions(id: string, options: RenderableOptions<Renderable>): void {
@@ -213,6 +215,7 @@ export abstract class Renderable extends EventEmitter {
213215
private _live: boolean = false
214216
protected _liveCount: number = 0
215217

218+
private _sizeChangeListener: (() => void) | undefined = undefined
216219
private _mouseListener: ((event: MouseEvent) => void) | null = null
217220
private _mouseListeners: Partial<Record<MouseEventType, (event: MouseEvent) => void>> = {}
218221
private _keyListeners: Partial<Record<"down", (key: ParsedKey) => void>> = {}
@@ -958,6 +961,7 @@ export abstract class Renderable extends EventEmitter {
958961
}
959962

960963
protected onResize(width: number, height: number): void {
964+
this.onSizeChange?.()
961965
this.emit("resize")
962966
// Override in subclasses for additional resize logic
963967
}
@@ -1249,6 +1253,13 @@ export abstract class Renderable extends EventEmitter {
12491253
return this._keyListeners["down"]
12501254
}
12511255

1256+
public set onSizeChange(handler: (() => void) | undefined) {
1257+
this._sizeChangeListener = handler
1258+
}
1259+
public get onSizeChange(): (() => void) | undefined {
1260+
return this._sizeChangeListener
1261+
}
1262+
12521263
private applyEventOptions(options: RenderableOptions<Renderable>): void {
12531264
this.onMouse = options.onMouse
12541265
this.onMouseDown = options.onMouseDown
@@ -1261,6 +1272,7 @@ export abstract class Renderable extends EventEmitter {
12611272
this.onMouseOut = options.onMouseOut
12621273
this.onMouseScroll = options.onMouseScroll
12631274
this.onKeyDown = options.onKeyDown
1275+
this.onSizeChange = options.onSizeChange
12641276
}
12651277
}
12661278

packages/core/src/examples/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as opentuiDemo from "./opentui-demo"
2424
import * as nestedZIndexDemo from "./nested-zindex-demo"
2525
import * as relativePositioningDemo from "./relative-positioning-demo"
2626
import * as transparencyDemo from "./transparency-demo"
27+
import * as scrollExample from "./scroll-example"
2728
import * as shaderCubeExample from "./shader-cube-demo"
2829
import * as spriteAnimationExample from "./sprite-animation-demo"
2930
import * as spriteParticleExample from "./sprite-particle-generator-demo"
@@ -170,6 +171,12 @@ const examples: Example[] = [
170171
run: textureLoadingExample.run,
171172
destroy: textureLoadingExample.destroy,
172173
},
174+
{
175+
name: "ScrollBox Demo",
176+
description: "Scrollable container with customization",
177+
run: scrollExample.run,
178+
destroy: scrollExample.destroy,
179+
},
173180
{
174181
name: "Shader Cube",
175182
description: "3D cube with custom shaders",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { ASCIIFontRenderable, BoxRenderable, type CliRenderer, createCliRenderer, TextRenderable } from "../index"
2+
import { ScrollBoxRenderable } from "../renderables/ScrollBox"
3+
import { setupCommonDemoKeys } from "./lib/standalone-keys"
4+
5+
let scrollBox: ScrollBoxRenderable | null = null
6+
let renderer: CliRenderer | null = null
7+
8+
export function run(rendererInstance: CliRenderer): void {
9+
renderer = rendererInstance
10+
renderer.setBackgroundColor("#001122")
11+
12+
scrollBox = new ScrollBoxRenderable(renderer, {
13+
id: "scroll-box",
14+
width: "100%",
15+
height: "100%",
16+
rootOptions: {
17+
backgroundColor: "#730000",
18+
border: true,
19+
},
20+
wrapperOptions: {
21+
backgroundColor: "#9f0045",
22+
},
23+
viewportOptions: {
24+
backgroundColor: "#005dbb",
25+
},
26+
contentOptions: {
27+
backgroundColor: "#7fbfff",
28+
},
29+
scrollbarOptions: {
30+
showArrows: true,
31+
thumbOptions: {
32+
backgroundColor: "#fe9d15",
33+
},
34+
trackOptions: {
35+
backgroundColor: "#fff693",
36+
},
37+
},
38+
})
39+
40+
scrollBox.focus()
41+
42+
renderer.root.add(scrollBox)
43+
44+
for (let index = 0; index < 20; index++) addItem(`Item ${index + 1}`)
45+
46+
const item = new BoxRenderable(renderer, {
47+
id: "scroll-item",
48+
width: 120,
49+
margin: 5,
50+
height: 5,
51+
backgroundColor: "red",
52+
})
53+
54+
scrollBox.content.add(item)
55+
56+
item.add(
57+
new ASCIIFontRenderable(renderer, {
58+
text: "OPENTUI Scroll",
59+
margin: "auto",
60+
}),
61+
)
62+
63+
for (let index = 0; index < 20; index++) addItem(`Item ${index + 1}`)
64+
65+
function addItem(content: string) {
66+
scrollBox!.content.add(
67+
new TextRenderable(renderer!, {
68+
content,
69+
}),
70+
)
71+
}
72+
}
73+
74+
export function destroy(rendererInstance: CliRenderer): void {
75+
if (scrollBox) {
76+
rendererInstance.root.remove(scrollBox.id)
77+
scrollBox.destroy()
78+
scrollBox = null
79+
}
80+
renderer = null
81+
}
82+
83+
if (import.meta.main) {
84+
const renderer = await createCliRenderer({
85+
exitOnCtrlC: true,
86+
})
87+
88+
run(renderer)
89+
setupCommonDemoKeys(renderer)
90+
}

packages/core/src/lib/parse.mouse.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ export class MouseParser {
1818
private mouseButtonsPressed = new Set<number>()
1919

2020
private static readonly SCROLL_DIRECTIONS: Record<number, "up" | "down" | "left" | "right"> = {
21-
64: "up",
22-
65: "down",
23-
66: "left",
24-
67: "right",
21+
0: "up",
22+
1: "down",
23+
2: "left",
24+
3: "right",
2525
}
2626

2727
public reset(): void {
@@ -36,10 +36,10 @@ export class MouseParser {
3636
const [, buttonCode, x, y, pressRelease] = sgrMatch
3737
const rawButtonCode = parseInt(buttonCode)
3838

39-
const scrollDirection = MouseParser.SCROLL_DIRECTIONS[rawButtonCode]
40-
const isScroll = scrollDirection !== undefined
41-
4239
const button = rawButtonCode & 3
40+
const isScroll = (rawButtonCode & 64) !== 0
41+
const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button]
42+
4343
const isMotion = (rawButtonCode & 32) !== 0
4444
const modifiers = {
4545
shift: (rawButtonCode & 4) !== 0,
@@ -93,10 +93,10 @@ export class MouseParser {
9393
const x = str.charCodeAt(4) - 33
9494
const y = str.charCodeAt(5) - 33
9595

96-
const scrollDirection = MouseParser.SCROLL_DIRECTIONS[buttonByte]
97-
const isScroll = scrollDirection !== undefined
98-
9996
const button = buttonByte & 3
97+
const isScroll = (buttonByte & 64) !== 0
98+
const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button]
99+
100100
const modifiers = {
101101
shift: (buttonByte & 4) !== 0,
102102
alt: (buttonByte & 8) !== 0,

0 commit comments

Comments
 (0)