@effect-tui/react 0.12.3 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -7
- package/dist/jsx-runtime.d.ts +1 -2
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/src/components/Markdown.js +7 -7
- package/dist/src/components/Markdown.js.map +1 -1
- package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
- package/dist/src/components/MultilineTextInput.js +11 -0
- package/dist/src/components/MultilineTextInput.js.map +1 -1
- package/dist/src/components/TextInput.d.ts.map +1 -1
- package/dist/src/components/TextInput.js +15 -0
- package/dist/src/components/TextInput.js.map +1 -1
- package/dist/src/dev.d.ts +22 -32
- package/dist/src/dev.d.ts.map +1 -1
- package/dist/src/dev.js +42 -96
- package/dist/src/dev.js.map +1 -1
- package/dist/src/hooks/index.d.ts +3 -0
- package/dist/src/hooks/index.d.ts.map +1 -1
- package/dist/src/hooks/index.js +1 -0
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/hooks/use-mouse.d.ts +10 -1
- package/dist/src/hooks/use-mouse.d.ts.map +1 -1
- package/dist/src/hooks/use-mouse.js +10 -2
- package/dist/src/hooks/use-mouse.js.map +1 -1
- package/dist/src/hooks/use-paste.d.ts +13 -3
- package/dist/src/hooks/use-paste.d.ts.map +1 -1
- package/dist/src/hooks/use-paste.js +15 -5
- package/dist/src/hooks/use-paste.js.map +1 -1
- package/dist/src/hooks/use-scroll.d.ts +59 -24
- package/dist/src/hooks/use-scroll.d.ts.map +1 -1
- package/dist/src/hooks/use-scroll.js +238 -79
- package/dist/src/hooks/use-scroll.js.map +1 -1
- package/dist/src/hooks/use-shortcut.d.ts +16 -0
- package/dist/src/hooks/use-shortcut.d.ts.map +1 -0
- package/dist/src/hooks/use-shortcut.js +29 -0
- package/dist/src/hooks/use-shortcut.js.map +1 -0
- package/dist/src/hosts/base.d.ts +16 -0
- package/dist/src/hosts/base.d.ts.map +1 -1
- package/dist/src/hosts/base.js +30 -0
- package/dist/src/hosts/base.js.map +1 -1
- package/dist/src/hosts/box.d.ts.map +1 -1
- package/dist/src/hosts/box.js +7 -8
- package/dist/src/hosts/box.js.map +1 -1
- package/dist/src/hosts/canvas.d.ts.map +1 -1
- package/dist/src/hosts/canvas.js +5 -3
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/hosts/codeblock.d.ts.map +1 -1
- package/dist/src/hosts/codeblock.js +5 -4
- package/dist/src/hosts/codeblock.js.map +1 -1
- package/dist/src/hosts/flex-container.d.ts.map +1 -1
- package/dist/src/hosts/flex-container.js +5 -8
- package/dist/src/hosts/flex-container.js.map +1 -1
- package/dist/src/hosts/index.d.ts +1 -1
- package/dist/src/hosts/index.d.ts.map +1 -1
- package/dist/src/hosts/index.js +2 -3
- package/dist/src/hosts/index.js.map +1 -1
- package/dist/src/hosts/overlay-item.js +2 -2
- package/dist/src/hosts/overlay-item.js.map +1 -1
- package/dist/src/hosts/overlay.d.ts.map +1 -1
- package/dist/src/hosts/overlay.js +6 -11
- package/dist/src/hosts/overlay.js.map +1 -1
- package/dist/src/hosts/scroll.d.ts +8 -0
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +50 -26
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/spacer.d.ts.map +1 -1
- package/dist/src/hosts/spacer.js +1 -3
- package/dist/src/hosts/spacer.js.map +1 -1
- package/dist/src/hosts/text.d.ts +24 -45
- package/dist/src/hosts/text.d.ts.map +1 -1
- package/dist/src/hosts/text.js +69 -215
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/hosts/zstack.d.ts.map +1 -1
- package/dist/src/hosts/zstack.js +4 -10
- package/dist/src/hosts/zstack.js.map +1 -1
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/reconciler/types.d.ts +2 -0
- package/dist/src/reconciler/types.d.ts.map +1 -1
- package/dist/src/renderer/core/FrameBuilder.d.ts.map +1 -1
- package/dist/src/renderer/core/FrameBuilder.js +2 -0
- package/dist/src/renderer/core/FrameBuilder.js.map +1 -1
- package/dist/src/renderer/input/InputProcessor.d.ts.map +1 -1
- package/dist/src/renderer/input/InputProcessor.js +13 -3
- package/dist/src/renderer/input/InputProcessor.js.map +1 -1
- package/dist/src/renderer/lifecycle/EventBus.d.ts +2 -2
- package/dist/src/renderer/lifecycle/EventBus.d.ts.map +1 -1
- package/dist/src/renderer/lifecycle/EventBus.js +13 -1
- package/dist/src/renderer/lifecycle/EventBus.js.map +1 -1
- package/dist/src/renderer/lifecycle/RenderCache.d.ts +3 -0
- package/dist/src/renderer/lifecycle/RenderCache.d.ts.map +1 -0
- package/dist/src/renderer/lifecycle/RenderCache.js +9 -0
- package/dist/src/renderer/lifecycle/RenderCache.js.map +1 -0
- package/dist/src/renderer/lifecycle/index.d.ts +1 -0
- package/dist/src/renderer/lifecycle/index.d.ts.map +1 -1
- package/dist/src/renderer/lifecycle/index.js +1 -0
- package/dist/src/renderer/lifecycle/index.js.map +1 -1
- package/dist/src/renderer-types.d.ts +9 -2
- package/dist/src/renderer-types.d.ts.map +1 -1
- package/dist/src/renderer.d.ts +13 -2
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +42 -11
- package/dist/src/renderer.js.map +1 -1
- package/dist/src/shortcuts.d.ts +15 -0
- package/dist/src/shortcuts.d.ts.map +1 -0
- package/dist/src/shortcuts.js +149 -0
- package/dist/src/shortcuts.js.map +1 -0
- package/dist/src/test/mock-streams.d.ts.map +1 -1
- package/dist/src/test/mock-streams.js +0 -3
- package/dist/src/test/mock-streams.js.map +1 -1
- package/dist/src/test/render-tui.d.ts.map +1 -1
- package/dist/src/test/render-tui.js +1 -0
- package/dist/src/test/render-tui.js.map +1 -1
- package/dist/src/utils/alignment.d.ts +4 -0
- package/dist/src/utils/alignment.d.ts.map +1 -1
- package/dist/src/utils/alignment.js +12 -0
- package/dist/src/utils/alignment.js.map +1 -1
- package/dist/src/utils/index.d.ts +3 -2
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +3 -2
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/styles.d.ts +6 -1
- package/dist/src/utils/styles.d.ts.map +1 -1
- package/dist/src/utils/styles.js +9 -0
- package/dist/src/utils/styles.js.map +1 -1
- package/dist/src/utils/text-wrap.d.ts +10 -0
- package/dist/src/utils/text-wrap.d.ts.map +1 -0
- package/dist/src/utils/text-wrap.js +64 -0
- package/dist/src/utils/text-wrap.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jsx-runtime.ts +1 -2
- package/package.json +2 -2
- package/src/components/Markdown.tsx +7 -7
- package/src/components/MultilineTextInput.tsx +14 -0
- package/src/components/TextInput.tsx +18 -0
- package/src/dev.tsx +59 -107
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-mouse.ts +19 -3
- package/src/hooks/use-paste.ts +24 -5
- package/src/hooks/use-scroll.ts +345 -105
- package/src/hooks/use-shortcut.ts +54 -0
- package/src/hosts/base.ts +35 -0
- package/src/hosts/box.ts +7 -8
- package/src/hosts/canvas.ts +5 -3
- package/src/hosts/codeblock.ts +5 -4
- package/src/hosts/flex-container.ts +5 -7
- package/src/hosts/index.ts +1 -4
- package/src/hosts/overlay-item.ts +2 -2
- package/src/hosts/overlay.ts +6 -12
- package/src/hosts/scroll.ts +55 -26
- package/src/hosts/spacer.ts +1 -3
- package/src/hosts/text.ts +89 -256
- package/src/hosts/zstack.ts +4 -11
- package/src/index.ts +19 -6
- package/src/reconciler/types.ts +3 -0
- package/src/renderer/core/FrameBuilder.ts +3 -0
- package/src/renderer/input/InputProcessor.ts +13 -3
- package/src/renderer/lifecycle/EventBus.ts +14 -4
- package/src/renderer/lifecycle/RenderCache.ts +13 -0
- package/src/renderer/lifecycle/index.ts +1 -0
- package/src/renderer-types.ts +10 -2
- package/src/renderer.ts +85 -14
- package/src/shortcuts.ts +180 -0
- package/src/test/mock-streams.ts +0 -3
- package/src/test/render-tui.ts +1 -0
- package/src/utils/alignment.ts +18 -0
- package/src/utils/index.ts +3 -1
- package/src/utils/styles.ts +18 -1
- package/src/utils/text-wrap.ts +66 -0
package/src/hosts/base.ts
CHANGED
|
@@ -113,6 +113,21 @@ export abstract class BaseHost implements HostInstance {
|
|
|
113
113
|
// which would overwrite any values set here.
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Optional pre-frame hook. BaseHost will call prepareSelf() and then recurse into children.
|
|
118
|
+
*/
|
|
119
|
+
prepareFrame(): void {
|
|
120
|
+
this.prepareSelf()
|
|
121
|
+
for (const child of this.children) {
|
|
122
|
+
child.prepareFrame?.()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Override in subclasses to precompute caches once per frame. */
|
|
127
|
+
protected prepareSelf(): void {
|
|
128
|
+
// Default no-op
|
|
129
|
+
}
|
|
130
|
+
|
|
116
131
|
/**
|
|
117
132
|
* Resolve a prop that may be a MotionValue/ColorMotionValue.
|
|
118
133
|
* If it's a spring, subscribes to changes and returns current value.
|
|
@@ -193,6 +208,14 @@ export abstract class BaseHost implements HostInstance {
|
|
|
193
208
|
abstract render(buffer: CellBuffer, palette: Palette): void
|
|
194
209
|
|
|
195
210
|
layout(rect: Rect): void {
|
|
211
|
+
this.layoutWithConstraints(rect)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Apply frame constraints and update this.rect.
|
|
216
|
+
* Returns the constrained rect for convenience in subclasses.
|
|
217
|
+
*/
|
|
218
|
+
protected layoutWithConstraints(rect: Rect): Rect {
|
|
196
219
|
// Apply frame constraints to the assigned rect
|
|
197
220
|
let { w, h } = rect
|
|
198
221
|
|
|
@@ -214,6 +237,8 @@ export abstract class BaseHost implements HostInstance {
|
|
|
214
237
|
this._lastLayoutH = h
|
|
215
238
|
this.onLayout({ width: w, height: h, x: rect.x, y: rect.y })
|
|
216
239
|
}
|
|
240
|
+
|
|
241
|
+
return this.rect
|
|
217
242
|
}
|
|
218
243
|
|
|
219
244
|
/**
|
|
@@ -322,6 +347,16 @@ export abstract class BaseHost implements HostInstance {
|
|
|
322
347
|
this.onLayout = typeof props.onLayout === "function" ? (props.onLayout as typeof this.onLayout) : undefined
|
|
323
348
|
}
|
|
324
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Apply a default greedy value when the prop is omitted.
|
|
352
|
+
* Subclasses with greedy defaults should call this after super.updateProps().
|
|
353
|
+
*/
|
|
354
|
+
protected applyGreedyDefault(props: Record<string, unknown>, fallback: number): void {
|
|
355
|
+
if (!("greedy" in props)) {
|
|
356
|
+
this.greedy = fallback
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
325
360
|
destroy(): void {
|
|
326
361
|
this.clearSpringSubscriptions()
|
|
327
362
|
// Override in subclasses if cleanup needed
|
package/src/hosts/box.ts
CHANGED
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
type BorderKind,
|
|
7
7
|
borderChars,
|
|
8
8
|
drawBorder,
|
|
9
|
+
fillRectWithInheritedBg,
|
|
9
10
|
type Padding,
|
|
10
11
|
type PaddingInput,
|
|
11
|
-
resolveInheritedBgStyle,
|
|
12
12
|
resolvePadding,
|
|
13
13
|
toColorValue,
|
|
14
14
|
} from "../utils/index.js"
|
|
@@ -82,14 +82,14 @@ export class BoxHost extends SingleChildHost {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
override layout(rect: Rect): void {
|
|
85
|
-
|
|
85
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
86
86
|
|
|
87
87
|
const t = this.borderThickness
|
|
88
88
|
const innerRect: Rect = {
|
|
89
|
-
x:
|
|
90
|
-
y:
|
|
91
|
-
w: Math.max(0,
|
|
92
|
-
h: Math.max(0,
|
|
89
|
+
x: layoutRect.x + t + this.padding.left,
|
|
90
|
+
y: layoutRect.y + t + this.padding.top,
|
|
91
|
+
w: Math.max(0, layoutRect.w - this.insetX),
|
|
92
|
+
h: Math.max(0, layoutRect.h - this.insetY),
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// Layout single child
|
|
@@ -105,8 +105,7 @@ export class BoxHost extends SingleChildHost {
|
|
|
105
105
|
|
|
106
106
|
// Always clear our rect to ensure stale backgrounds are removed.
|
|
107
107
|
// If no explicit bg, inherit from parent (so child boxes are transparent over parents)
|
|
108
|
-
|
|
109
|
-
buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyleId)
|
|
108
|
+
fillRectWithInheritedBg(buffer, palette, { x, y, w, h }, this.bg, this.parent)
|
|
110
109
|
|
|
111
110
|
// Draw border
|
|
112
111
|
if (this.border !== "none" && w >= 2 && h >= 2) {
|
package/src/hosts/canvas.ts
CHANGED
|
@@ -79,10 +79,12 @@ export class CanvasHost extends BaseHost {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
measure(maxW: number, maxH: number): Size {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const constrained = this.constrainProposal(maxW, maxH)
|
|
83
|
+
const size = {
|
|
84
|
+
w: this.fixedWidth ?? constrained.w,
|
|
85
|
+
h: this.fixedHeight ?? constrained.h,
|
|
85
86
|
}
|
|
87
|
+
return this.constrainResult(size)
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
render(buffer: CellBuffer, palette: Palette): void {
|
package/src/hosts/codeblock.ts
CHANGED
|
@@ -50,6 +50,7 @@ export class CodeBlockHost extends BaseHost {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
measure(maxW: number, maxH: number): Size {
|
|
53
|
+
const constrained = this.constrainProposal(maxW, maxH)
|
|
53
54
|
this.cachedLineWidths = this.lines.map((l) => lineDisplayWidth(l))
|
|
54
55
|
this.gutterWidth = this.computeGutterWidth()
|
|
55
56
|
|
|
@@ -58,10 +59,10 @@ export class CodeBlockHost extends BaseHost {
|
|
|
58
59
|
|
|
59
60
|
const innerHeight = Math.max(1, this.lines.length) + this.insetY
|
|
60
61
|
|
|
61
|
-
return {
|
|
62
|
-
w: Math.min(
|
|
63
|
-
h: Math.min(
|
|
64
|
-
}
|
|
62
|
+
return this.constrainResult({
|
|
63
|
+
w: Math.min(constrained.w, contentW),
|
|
64
|
+
h: Math.min(constrained.h, innerHeight),
|
|
65
|
+
})
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
override layout(rect: Rect): void {
|
|
@@ -103,17 +103,15 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
override layout(rect: Rect): void {
|
|
106
|
-
|
|
107
|
-
// Use this.rect (constrained) not rect (raw input)
|
|
108
|
-
if (!this.rect) return
|
|
106
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
109
107
|
const stretchCross = this.axis === "vertical" ? this.alignment === "leading" : this.alignment === "top"
|
|
110
108
|
const insetX = this.padding.left + this.padding.right
|
|
111
109
|
const insetY = this.padding.top + this.padding.bottom
|
|
112
110
|
const innerRect: Rect = {
|
|
113
|
-
x:
|
|
114
|
-
y:
|
|
115
|
-
w: Math.max(0,
|
|
116
|
-
h: Math.max(0,
|
|
111
|
+
x: layoutRect.x + this.padding.left,
|
|
112
|
+
y: layoutRect.y + this.padding.top,
|
|
113
|
+
w: Math.max(0, layoutRect.w - insetX),
|
|
114
|
+
h: Math.max(0, layoutRect.h - insetY),
|
|
117
115
|
}
|
|
118
116
|
layoutFlex(
|
|
119
117
|
this.axis,
|
package/src/hosts/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { OverlayHost } from "./overlay.js"
|
|
|
8
8
|
import { OverlayItemHost } from "./overlay-item.js"
|
|
9
9
|
import { ScrollHost } from "./scroll.js"
|
|
10
10
|
import { SpacerHost } from "./spacer.js"
|
|
11
|
-
import { RawTextHost, SpanHost,
|
|
11
|
+
import { RawTextHost, SpanHost, TextHost } from "./text.js"
|
|
12
12
|
import { VStackHost } from "./vstack.js"
|
|
13
13
|
import { ZStackHost } from "./zstack.js"
|
|
14
14
|
|
|
@@ -25,12 +25,10 @@ export { SpacerHost, type SpacerProps } from "./spacer.js"
|
|
|
25
25
|
export {
|
|
26
26
|
RawTextHost,
|
|
27
27
|
SpanHost,
|
|
28
|
-
StyledTextHost,
|
|
29
28
|
TextHost,
|
|
30
29
|
type SpanProps,
|
|
31
30
|
type SpanStyle,
|
|
32
31
|
type StyledSpan,
|
|
33
|
-
type StyledTextProps,
|
|
34
32
|
type TextProps,
|
|
35
33
|
} from "./text.js"
|
|
36
34
|
export { VStackHost, type VStackProps } from "./vstack.js"
|
|
@@ -41,7 +39,6 @@ export { ZStackHost, type ZStackProps } from "./zstack.js"
|
|
|
41
39
|
export const hostRegistry: Record<string, new (props: any, ctx: HostContext) => BaseHost> = {
|
|
42
40
|
text: TextHost,
|
|
43
41
|
span: SpanHost,
|
|
44
|
-
styledtext: StyledTextHost,
|
|
45
42
|
spacer: SpacerHost,
|
|
46
43
|
vstack: VStackHost,
|
|
47
44
|
hstack: HStackHost,
|
|
@@ -38,8 +38,8 @@ export class OverlayItemHost extends SingleChildHost {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
override layout(rect: Rect): void {
|
|
41
|
-
|
|
42
|
-
this.child?.layout(
|
|
41
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
42
|
+
this.child?.layout(layoutRect)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
override render(buffer: CellBuffer, palette: Palette): void {
|
package/src/hosts/overlay.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import type { CellBuffer, Palette, Rect, Size } from "@effect-tui/core"
|
|
20
20
|
import type { CommonProps, HostContext } from "../reconciler/types.js"
|
|
21
|
-
import {
|
|
21
|
+
import { alignedChildRect } from "../utils/index.js"
|
|
22
22
|
import { BaseHost } from "./base.js"
|
|
23
23
|
import type { OverlayItemHost } from "./overlay-item.js"
|
|
24
24
|
|
|
@@ -29,6 +29,7 @@ export class OverlayHost extends BaseHost {
|
|
|
29
29
|
|
|
30
30
|
constructor(props: OverlayProps, ctx: HostContext) {
|
|
31
31
|
super("overlay", props, ctx)
|
|
32
|
+
this.updateProps(props as unknown as Record<string, unknown>)
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
override measure(maxW: number, maxH: number): Size {
|
|
@@ -56,29 +57,22 @@ export class OverlayHost extends BaseHost {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
override layout(rect: Rect): void {
|
|
59
|
-
|
|
60
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
60
61
|
|
|
61
62
|
// Layout base child to fill our rect
|
|
62
63
|
const baseChild = this.children[0]
|
|
63
64
|
if (baseChild) {
|
|
64
|
-
baseChild.layout(
|
|
65
|
+
baseChild.layout(layoutRect)
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// Layout overlay children with their alignment
|
|
68
69
|
for (let i = 1; i < this.children.length; i++) {
|
|
69
70
|
const child = this.children[i]
|
|
70
|
-
const size = this.cachedSizes[i] ?? child.measure(
|
|
71
|
+
const size = this.cachedSizes[i] ?? child.measure(layoutRect.w, layoutRect.h)
|
|
71
72
|
|
|
72
73
|
// Read alignment from OverlayItemHost (use type check, not instanceof, for bundler compatibility)
|
|
73
74
|
const alignment = child.type === "overlayItem" ? (child as OverlayItemHost).alignment : {}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
child.layout({
|
|
77
|
-
x,
|
|
78
|
-
y,
|
|
79
|
-
w: Math.min(rect.w, size.w),
|
|
80
|
-
h: Math.min(rect.h, size.h),
|
|
81
|
-
})
|
|
75
|
+
child.layout(alignedChildRect(layoutRect, size, alignment.h ?? "center", alignment.v ?? "center"))
|
|
82
76
|
}
|
|
83
77
|
}
|
|
84
78
|
|
package/src/hosts/scroll.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// scroll.ts — Scrollable container host
|
|
2
2
|
import type { CellBuffer, Color, Palette } from "@effect-tui/core"
|
|
3
3
|
import type { CommonProps, HostContext, Rect, Size } from "../reconciler/types.js"
|
|
4
|
-
import {
|
|
4
|
+
import { fillRectWithInheritedBg } from "../utils/index.js"
|
|
5
5
|
import { SingleChildHost } from "./single-child.js"
|
|
6
6
|
|
|
7
7
|
export interface ScrollProps extends CommonProps {
|
|
@@ -28,6 +28,8 @@ export interface ScrollProps extends CommonProps {
|
|
|
28
28
|
onViewportSize?: (width: number, height: number) => void
|
|
29
29
|
/** Called when effective offset changes (for syncing with useScroll when sticky adjusts) */
|
|
30
30
|
onEffectiveOffset?: (offset: number) => void
|
|
31
|
+
/** Called when effective horizontal offset changes */
|
|
32
|
+
onEffectiveOffsetX?: (offsetX: number) => void
|
|
31
33
|
/** Called when layout rect changes (for hit testing) */
|
|
32
34
|
onRect?: (x: number, y: number, w: number, h: number) => void
|
|
33
35
|
}
|
|
@@ -46,18 +48,24 @@ export class ScrollHost extends SingleChildHost {
|
|
|
46
48
|
onContentSize?: (width: number, height: number) => void
|
|
47
49
|
onViewportSize?: (width: number, height: number) => void
|
|
48
50
|
onEffectiveOffset?: (offset: number) => void
|
|
51
|
+
onEffectiveOffsetX?: (offsetX: number) => void
|
|
49
52
|
onRect?: (x: number, y: number, w: number, h: number) => void
|
|
50
53
|
|
|
51
54
|
// Measured content dimensions (full size before clipping)
|
|
52
55
|
private contentWidth = 0
|
|
53
56
|
private contentHeight = 0
|
|
54
57
|
// Track last reported sizes to avoid redundant callbacks
|
|
55
|
-
private lastReportedContentW =
|
|
56
|
-
private lastReportedContentH =
|
|
58
|
+
private lastReportedContentW = -1
|
|
59
|
+
private lastReportedContentH = -1
|
|
60
|
+
private lastViewportW = -1
|
|
61
|
+
private lastViewportH = -1
|
|
57
62
|
private lastRectX = -1
|
|
58
63
|
private lastRectY = -1
|
|
64
|
+
private lastRectW = -1
|
|
65
|
+
private lastRectH = -1
|
|
59
66
|
// Track if we were at end (for sticky behavior)
|
|
60
67
|
private wasAtEnd = true
|
|
68
|
+
private wasAtEndX = true
|
|
61
69
|
// Effective offset after sticky adjustment (used for rendering)
|
|
62
70
|
private effectiveOffset = 0
|
|
63
71
|
private effectiveOffsetX = 0
|
|
@@ -100,7 +108,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
override layout(rect: Rect): void {
|
|
103
|
-
|
|
111
|
+
const layoutRect = this.layoutWithConstraints(rect)
|
|
104
112
|
|
|
105
113
|
// Report content size if changed (deferred from measure() to keep it pure)
|
|
106
114
|
if (this.contentWidth !== this.lastReportedContentW || this.contentHeight !== this.lastReportedContentH) {
|
|
@@ -110,33 +118,41 @@ export class ScrollHost extends SingleChildHost {
|
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
// Report viewport size if changed (for useScroll hook)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
121
|
+
if (this.onViewportSize && (layoutRect.w !== this.lastViewportW || layoutRect.h !== this.lastViewportH)) {
|
|
122
|
+
this.lastViewportW = layoutRect.w
|
|
123
|
+
this.lastViewportH = layoutRect.h
|
|
124
|
+
this.onViewportSize(layoutRect.w, layoutRect.h)
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
// Report rect if position changed (for hit testing)
|
|
119
|
-
if (
|
|
120
|
-
this.lastRectX
|
|
121
|
-
this.lastRectY
|
|
122
|
-
|
|
128
|
+
if (
|
|
129
|
+
layoutRect.x !== this.lastRectX ||
|
|
130
|
+
layoutRect.y !== this.lastRectY ||
|
|
131
|
+
layoutRect.w !== this.lastRectW ||
|
|
132
|
+
layoutRect.h !== this.lastRectH
|
|
133
|
+
) {
|
|
134
|
+
this.lastRectX = layoutRect.x
|
|
135
|
+
this.lastRectY = layoutRect.y
|
|
136
|
+
this.lastRectW = layoutRect.w
|
|
137
|
+
this.lastRectH = layoutRect.h
|
|
138
|
+
this.onRect?.(layoutRect.x, layoutRect.y, layoutRect.w, layoutRect.h)
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
const child = this.child
|
|
126
142
|
if (!child) return
|
|
127
143
|
|
|
128
144
|
// Calculate max scroll offsets
|
|
129
|
-
const maxScrollY = Math.max(0, this.contentHeight -
|
|
130
|
-
const maxScrollX = Math.max(0, this.contentWidth -
|
|
145
|
+
const maxScrollY = Math.max(0, this.contentHeight - layoutRect.h)
|
|
146
|
+
const maxScrollX = Math.max(0, this.contentWidth - layoutRect.w)
|
|
131
147
|
|
|
132
148
|
// Start with the offset from props (controlled by useScroll)
|
|
133
149
|
let scrollY = this.offset
|
|
134
150
|
let scrollX = this.offsetX
|
|
135
151
|
|
|
136
|
-
// Sticky scroll logic:
|
|
152
|
+
// Sticky scroll logic (vertical):
|
|
137
153
|
// - If user manually scrolled away from end (offset < effectiveOffset), unstick
|
|
138
154
|
// - If at end (or was at end and content grew), stay stuck
|
|
139
|
-
if (this.sticky) {
|
|
155
|
+
if (this.sticky && (this.axis === "vertical" || this.axis === "both")) {
|
|
140
156
|
// Detect if user scrolled away (offset prop is less than where we rendered)
|
|
141
157
|
const userScrolledAway = this.offset < this.effectiveOffset - 1
|
|
142
158
|
|
|
@@ -149,8 +165,20 @@ export class ScrollHost extends SingleChildHost {
|
|
|
149
165
|
}
|
|
150
166
|
}
|
|
151
167
|
|
|
168
|
+
// Sticky scroll logic (horizontal)
|
|
169
|
+
if (this.sticky && (this.axis === "horizontal" || this.axis === "both")) {
|
|
170
|
+
const userScrolledAway = this.offsetX < this.effectiveOffsetX - 1
|
|
171
|
+
|
|
172
|
+
if (userScrolledAway) {
|
|
173
|
+
this.wasAtEndX = false
|
|
174
|
+
} else if (this.wasAtEndX) {
|
|
175
|
+
scrollX = maxScrollX
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
152
179
|
// Track if we're at end for next frame
|
|
153
180
|
this.wasAtEnd = scrollY >= maxScrollY - 1
|
|
181
|
+
this.wasAtEndX = scrollX >= maxScrollX - 1
|
|
154
182
|
|
|
155
183
|
// Clamp offsets
|
|
156
184
|
scrollY = Math.max(0, Math.min(maxScrollY, scrollY))
|
|
@@ -161,25 +189,28 @@ export class ScrollHost extends SingleChildHost {
|
|
|
161
189
|
if (scrollY !== this.effectiveOffset || scrollY !== this.offset) {
|
|
162
190
|
this.onEffectiveOffset?.(scrollY)
|
|
163
191
|
}
|
|
192
|
+
if (scrollX !== this.effectiveOffsetX || scrollX !== this.offsetX) {
|
|
193
|
+
this.onEffectiveOffsetX?.(scrollX)
|
|
194
|
+
}
|
|
164
195
|
this.effectiveOffset = scrollY
|
|
165
196
|
this.effectiveOffsetX = scrollX
|
|
166
197
|
|
|
167
198
|
// Handle alignment when content is smaller than viewport
|
|
168
199
|
if (this.align === "end") {
|
|
169
|
-
if (this.contentHeight <
|
|
200
|
+
if (this.contentHeight < layoutRect.h && (this.axis === "vertical" || this.axis === "both")) {
|
|
170
201
|
// Align to bottom
|
|
171
|
-
scrollY = -(
|
|
202
|
+
scrollY = -(layoutRect.h - this.contentHeight)
|
|
172
203
|
}
|
|
173
|
-
if (this.contentWidth <
|
|
204
|
+
if (this.contentWidth < layoutRect.w && (this.axis === "horizontal" || this.axis === "both")) {
|
|
174
205
|
// Align to right
|
|
175
|
-
scrollX = -(
|
|
206
|
+
scrollX = -(layoutRect.w - this.contentWidth)
|
|
176
207
|
}
|
|
177
208
|
}
|
|
178
209
|
|
|
179
210
|
// Layout child at offset position
|
|
180
211
|
const childRect: Rect = {
|
|
181
|
-
x:
|
|
182
|
-
y:
|
|
212
|
+
x: layoutRect.x - scrollX,
|
|
213
|
+
y: layoutRect.y - scrollY,
|
|
183
214
|
w: this.contentWidth,
|
|
184
215
|
h: this.contentHeight,
|
|
185
216
|
}
|
|
@@ -191,8 +222,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
191
222
|
const { x, y, w, h } = this.rect
|
|
192
223
|
|
|
193
224
|
// Fill background (inherit from parent if not explicitly set)
|
|
194
|
-
|
|
195
|
-
buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyleId)
|
|
225
|
+
fillRectWithInheritedBg(buffer, palette, { x, y, w, h }, this.bg, this.parent)
|
|
196
226
|
|
|
197
227
|
// Render children with clipping
|
|
198
228
|
buffer.withClip(x, y, w, h, () => {
|
|
@@ -254,9 +284,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
254
284
|
override updateProps(props: Record<string, unknown>): void {
|
|
255
285
|
super.updateProps(props)
|
|
256
286
|
// Scroll is greedy by default unless explicitly set to false
|
|
257
|
-
|
|
258
|
-
this.greedy = 1
|
|
259
|
-
}
|
|
287
|
+
this.applyGreedyDefault(props, 1)
|
|
260
288
|
if (props.axis !== undefined) this.axis = (props.axis as ScrollProps["axis"]) ?? "vertical"
|
|
261
289
|
if (props.offset !== undefined) this.offset = props.offset as number
|
|
262
290
|
if (props.offsetX !== undefined) this.offsetX = props.offsetX as number
|
|
@@ -267,6 +295,7 @@ export class ScrollHost extends SingleChildHost {
|
|
|
267
295
|
this.onContentSize = props.onContentSize as ScrollProps["onContentSize"]
|
|
268
296
|
this.onViewportSize = props.onViewportSize as ScrollProps["onViewportSize"]
|
|
269
297
|
this.onEffectiveOffset = props.onEffectiveOffset as ScrollProps["onEffectiveOffset"]
|
|
298
|
+
this.onEffectiveOffsetX = props.onEffectiveOffsetX as ScrollProps["onEffectiveOffsetX"]
|
|
270
299
|
this.onRect = props.onRect as ScrollProps["onRect"]
|
|
271
300
|
}
|
|
272
301
|
}
|
package/src/hosts/spacer.ts
CHANGED
|
@@ -37,9 +37,7 @@ export class SpacerHost extends BaseHost {
|
|
|
37
37
|
override updateProps(props: Record<string, unknown>): void {
|
|
38
38
|
super.updateProps(props)
|
|
39
39
|
// Spacer is greedy by default unless explicitly set to false
|
|
40
|
-
|
|
41
|
-
this.greedy = 1
|
|
42
|
-
}
|
|
40
|
+
this.applyGreedyDefault(props, 1)
|
|
43
41
|
if (props.minWidth !== undefined) this.minWidth = props.minWidth as number
|
|
44
42
|
if (props.minHeight !== undefined) this.minHeight = props.minHeight as number
|
|
45
43
|
}
|