@effect-tui/core 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/anim.d.ts +4 -0
- package/dist/anim.d.ts.map +1 -0
- package/dist/anim.js +5 -0
- package/dist/anim.js.map +1 -0
- package/dist/ansi.d.ts +69 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +72 -0
- package/dist/ansi.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +18 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +247 -0
- package/dist/keys.js.map +1 -0
- package/dist/layout/linearStack.d.ts +17 -0
- package/dist/layout/linearStack.d.ts.map +1 -0
- package/dist/layout/linearStack.js +86 -0
- package/dist/layout/linearStack.js.map +1 -0
- package/dist/motion-value.d.ts +58 -0
- package/dist/motion-value.d.ts.map +1 -0
- package/dist/motion-value.js +250 -0
- package/dist/motion-value.js.map +1 -0
- package/dist/present/display.d.ts +58 -0
- package/dist/present/display.d.ts.map +1 -0
- package/dist/present/display.js +168 -0
- package/dist/present/display.js.map +1 -0
- package/dist/present/writers/fullscreen.d.ts +19 -0
- package/dist/present/writers/fullscreen.d.ts.map +1 -0
- package/dist/present/writers/fullscreen.js +55 -0
- package/dist/present/writers/fullscreen.js.map +1 -0
- package/dist/present/writers/inline.d.ts +20 -0
- package/dist/present/writers/inline.d.ts.map +1 -0
- package/dist/present/writers/inline.js +92 -0
- package/dist/present/writers/inline.js.map +1 -0
- package/dist/render/buffer.d.ts +31 -0
- package/dist/render/buffer.d.ts.map +1 -0
- package/dist/render/buffer.js +183 -0
- package/dist/render/buffer.js.map +1 -0
- package/dist/render/color-utils.d.ts +18 -0
- package/dist/render/color-utils.d.ts.map +1 -0
- package/dist/render/color-utils.js +58 -0
- package/dist/render/color-utils.js.map +1 -0
- package/dist/render/diff.d.ts +30 -0
- package/dist/render/diff.d.ts.map +1 -0
- package/dist/render/diff.js +83 -0
- package/dist/render/diff.js.map +1 -0
- package/dist/render/measure.d.ts +15 -0
- package/dist/render/measure.d.ts.map +1 -0
- package/dist/render/measure.js +65 -0
- package/dist/render/measure.js.map +1 -0
- package/dist/render/palette.d.ts +46 -0
- package/dist/render/palette.d.ts.map +1 -0
- package/dist/render/palette.js +108 -0
- package/dist/render/palette.js.map +1 -0
- package/dist/render/surface.d.ts +77 -0
- package/dist/render/surface.d.ts.map +1 -0
- package/dist/render/surface.js +198 -0
- package/dist/render/surface.js.map +1 -0
- package/dist/runtime/backend_node.d.ts +36 -0
- package/dist/runtime/backend_node.d.ts.map +1 -0
- package/dist/runtime/backend_node.js +66 -0
- package/dist/runtime/backend_node.js.map +1 -0
- package/dist/spring-physics.d.ts +36 -0
- package/dist/spring-physics.d.ts.map +1 -0
- package/dist/spring-physics.js +113 -0
- package/dist/spring-physics.js.map +1 -0
- package/dist/spring.d.ts +73 -0
- package/dist/spring.d.ts.map +1 -0
- package/dist/spring.js +136 -0
- package/dist/spring.js.map +1 -0
- package/dist/ui/containers/canvas.d.ts +13 -0
- package/dist/ui/containers/canvas.d.ts.map +1 -0
- package/dist/ui/containers/canvas.js +16 -0
- package/dist/ui/containers/canvas.js.map +1 -0
- package/dist/ui/containers/geometry-reader.d.ts +17 -0
- package/dist/ui/containers/geometry-reader.d.ts.map +1 -0
- package/dist/ui/containers/geometry-reader.js +24 -0
- package/dist/ui/containers/geometry-reader.js.map +1 -0
- package/dist/ui/containers/hstack.d.ts +12 -0
- package/dist/ui/containers/hstack.d.ts.map +1 -0
- package/dist/ui/containers/hstack.js +28 -0
- package/dist/ui/containers/hstack.js.map +1 -0
- package/dist/ui/containers/scroll.d.ts +28 -0
- package/dist/ui/containers/scroll.d.ts.map +1 -0
- package/dist/ui/containers/scroll.js +97 -0
- package/dist/ui/containers/scroll.js.map +1 -0
- package/dist/ui/containers/shared.d.ts +12 -0
- package/dist/ui/containers/shared.d.ts.map +1 -0
- package/dist/ui/containers/shared.js +19 -0
- package/dist/ui/containers/shared.js.map +1 -0
- package/dist/ui/containers/vstack.d.ts +12 -0
- package/dist/ui/containers/vstack.d.ts.map +1 -0
- package/dist/ui/containers/vstack.js +28 -0
- package/dist/ui/containers/vstack.js.map +1 -0
- package/dist/ui/containers/zstack.d.ts +14 -0
- package/dist/ui/containers/zstack.d.ts.map +1 -0
- package/dist/ui/containers/zstack.js +36 -0
- package/dist/ui/containers/zstack.js.map +1 -0
- package/dist/ui/core/geometry-store.d.ts +22 -0
- package/dist/ui/core/geometry-store.d.ts.map +1 -0
- package/dist/ui/core/geometry-store.js +29 -0
- package/dist/ui/core/geometry-store.js.map +1 -0
- package/dist/ui/core/geometry.d.ts +34 -0
- package/dist/ui/core/geometry.d.ts.map +1 -0
- package/dist/ui/core/geometry.js +14 -0
- package/dist/ui/core/geometry.js.map +1 -0
- package/dist/ui/core/view.d.ts +25 -0
- package/dist/ui/core/view.d.ts.map +1 -0
- package/dist/ui/core/view.js +34 -0
- package/dist/ui/core/view.js.map +1 -0
- package/dist/ui/index.d.ts +44 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +39 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/inlinetext.d.ts +24 -0
- package/dist/ui/inlinetext.d.ts.map +1 -0
- package/dist/ui/inlinetext.js +131 -0
- package/dist/ui/inlinetext.js.map +1 -0
- package/dist/ui/install.d.ts +22 -0
- package/dist/ui/install.d.ts.map +1 -0
- package/dist/ui/install.js +66 -0
- package/dist/ui/install.js.map +1 -0
- package/dist/ui/markdown.d.ts +40 -0
- package/dist/ui/markdown.d.ts.map +1 -0
- package/dist/ui/markdown.js +351 -0
- package/dist/ui/markdown.js.map +1 -0
- package/dist/ui/modifiers/border.d.ts +33 -0
- package/dist/ui/modifiers/border.d.ts.map +1 -0
- package/dist/ui/modifiers/border.js +82 -0
- package/dist/ui/modifiers/border.js.map +1 -0
- package/dist/ui/modifiers/fill.d.ts +14 -0
- package/dist/ui/modifiers/fill.d.ts.map +1 -0
- package/dist/ui/modifiers/fill.js +25 -0
- package/dist/ui/modifiers/fill.js.map +1 -0
- package/dist/ui/modifiers/frame.d.ts +23 -0
- package/dist/ui/modifiers/frame.d.ts.map +1 -0
- package/dist/ui/modifiers/frame.js +54 -0
- package/dist/ui/modifiers/frame.js.map +1 -0
- package/dist/ui/modifiers/offset.d.ts +15 -0
- package/dist/ui/modifiers/offset.d.ts.map +1 -0
- package/dist/ui/modifiers/offset.js +21 -0
- package/dist/ui/modifiers/offset.js.map +1 -0
- package/dist/ui/modifiers/opacity.d.ts +15 -0
- package/dist/ui/modifiers/opacity.d.ts.map +1 -0
- package/dist/ui/modifiers/opacity.js +95 -0
- package/dist/ui/modifiers/opacity.js.map +1 -0
- package/dist/ui/modifiers/padding.d.ts +20 -0
- package/dist/ui/modifiers/padding.d.ts.map +1 -0
- package/dist/ui/modifiers/padding.js +36 -0
- package/dist/ui/modifiers/padding.js.map +1 -0
- package/dist/ui/modifiers/styled.d.ts +14 -0
- package/dist/ui/modifiers/styled.d.ts.map +1 -0
- package/dist/ui/modifiers/styled.js +26 -0
- package/dist/ui/modifiers/styled.js.map +1 -0
- package/dist/ui/primitives/rectangle.d.ts +15 -0
- package/dist/ui/primitives/rectangle.d.ts.map +1 -0
- package/dist/ui/primitives/rectangle.js +23 -0
- package/dist/ui/primitives/rectangle.js.map +1 -0
- package/dist/ui/primitives/spacer.d.ts +13 -0
- package/dist/ui/primitives/spacer.d.ts.map +1 -0
- package/dist/ui/primitives/spacer.js +16 -0
- package/dist/ui/primitives/spacer.js.map +1 -0
- package/dist/ui/primitives/text.d.ts +15 -0
- package/dist/ui/primitives/text.d.ts.map +1 -0
- package/dist/ui/primitives/text.js +79 -0
- package/dist/ui/primitives/text.js.map +1 -0
- package/dist/ui/primitives/wrapped-text.d.ts +30 -0
- package/dist/ui/primitives/wrapped-text.d.ts.map +1 -0
- package/dist/ui/primitives/wrapped-text.js +117 -0
- package/dist/ui/primitives/wrapped-text.js.map +1 -0
- package/dist/ui/shinytext.d.ts +66 -0
- package/dist/ui/shinytext.d.ts.map +1 -0
- package/dist/ui/shinytext.js +99 -0
- package/dist/ui/shinytext.js.map +1 -0
- package/dist/ui/text/layout.d.ts +35 -0
- package/dist/ui/text/layout.d.ts.map +1 -0
- package/dist/ui/text/layout.js +102 -0
- package/dist/ui/text/layout.js.map +1 -0
- package/dist/ui/textinput.d.ts +140 -0
- package/dist/ui/textinput.d.ts.map +1 -0
- package/dist/ui/textinput.js +402 -0
- package/dist/ui/textinput.js.map +1 -0
- package/dist/ui/view-constructors.d.ts +72 -0
- package/dist/ui/view-constructors.d.ts.map +1 -0
- package/dist/ui/view-constructors.js +74 -0
- package/dist/ui/view-constructors.js.map +1 -0
- package/package.json +57 -0
- package/src/anim.ts +5 -0
- package/src/ansi.ts +83 -0
- package/src/index.ts +21 -0
- package/src/keys.ts +302 -0
- package/src/layout/linearStack.ts +115 -0
- package/src/motion-value.ts +335 -0
- package/src/present/display.ts +206 -0
- package/src/present/writers/fullscreen.ts +58 -0
- package/src/present/writers/inline.ts +101 -0
- package/src/render/buffer.ts +200 -0
- package/src/render/color-utils.ts +60 -0
- package/src/render/diff.ts +95 -0
- package/src/render/measure.ts +74 -0
- package/src/render/palette.ts +113 -0
- package/src/render/surface.ts +238 -0
- package/src/runtime/backend_node.ts +80 -0
- package/src/spring-physics.ts +151 -0
- package/src/spring.ts +234 -0
- package/src/ui/__snapshots__/wrappedtext.test.ts.snap +57 -0
- package/src/ui/containers/canvas.ts +18 -0
- package/src/ui/containers/geometry-reader.ts +32 -0
- package/src/ui/containers/hstack.ts +33 -0
- package/src/ui/containers/scroll.ts +106 -0
- package/src/ui/containers/shared.ts +27 -0
- package/src/ui/containers/vstack.ts +34 -0
- package/src/ui/containers/zstack.ts +37 -0
- package/src/ui/core/geometry-store.ts +42 -0
- package/src/ui/core/geometry.ts +30 -0
- package/src/ui/core/view.ts +49 -0
- package/src/ui/index.ts +84 -0
- package/src/ui/inlinetext.ts +135 -0
- package/src/ui/install.ts +110 -0
- package/src/ui/markdown.test.ts +74 -0
- package/src/ui/markdown.ts +388 -0
- package/src/ui/modifiers/border.ts +100 -0
- package/src/ui/modifiers/fill.ts +28 -0
- package/src/ui/modifiers/frame.ts +74 -0
- package/src/ui/modifiers/offset.ts +23 -0
- package/src/ui/modifiers/opacity.ts +93 -0
- package/src/ui/modifiers/padding.ts +53 -0
- package/src/ui/modifiers/styled.ts +31 -0
- package/src/ui/primitives/rectangle.ts +25 -0
- package/src/ui/primitives/spacer.ts +18 -0
- package/src/ui/primitives/text.ts +85 -0
- package/src/ui/primitives/wrapped-text.ts +131 -0
- package/src/ui/shinytext.ts +159 -0
- package/src/ui/text/layout.ts +119 -0
- package/src/ui/textinput.ts +496 -0
- package/src/ui/view-constructors.ts +96 -0
- package/src/ui/wrappedtext.test.ts +138 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/* shinytext.ts — reusable shining text component */
|
|
2
|
+
|
|
3
|
+
import type { Palette, Surface } from "../render/surface.js"
|
|
4
|
+
import { Colors } from "../render/surface.js"
|
|
5
|
+
import type { Rect } from "./core/geometry.js"
|
|
6
|
+
import { Step, type Stepper } from "../anim.js"
|
|
7
|
+
import { View } from "./core/view.js"
|
|
8
|
+
|
|
9
|
+
export type ShineTextOptions = {
|
|
10
|
+
/**
|
|
11
|
+
* Center position of the shine in character coordinates (can be fractional).
|
|
12
|
+
* 0 = before first char, length-1 = last char. Values outside are fine for overscan.
|
|
13
|
+
*/
|
|
14
|
+
center?: number
|
|
15
|
+
/**
|
|
16
|
+
* Alternative to center: normalized progress 0..1 across the text.
|
|
17
|
+
* If provided, takes precedence and maps to center internally.
|
|
18
|
+
*/
|
|
19
|
+
progress?: number
|
|
20
|
+
/** Gaussian falloff spread; larger = wider shine. Default 2.25 */
|
|
21
|
+
spread?: number
|
|
22
|
+
/** Base gray level (0..23). Default 12 */
|
|
23
|
+
baseGray?: number
|
|
24
|
+
/** Max additional gray levels added at peak (0..23). Default 8 */
|
|
25
|
+
boost?: number
|
|
26
|
+
/** Bold threshold on intensity (0..1). Default 0.75 */
|
|
27
|
+
boldThreshold?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ShinyText extends View {
|
|
31
|
+
constructor(
|
|
32
|
+
readonly text: string,
|
|
33
|
+
readonly opts?: ShineTextOptions,
|
|
34
|
+
) {
|
|
35
|
+
super()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private getChars(): string[] {
|
|
39
|
+
return [...this.text]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected measureContent(maxW: number, _maxH: number) {
|
|
43
|
+
const w = Math.min(maxW, this.getChars().length)
|
|
44
|
+
return { w, h: 1 }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected renderContent(s: Surface, pal: Palette, rect: Rect) {
|
|
48
|
+
const chars = this.getChars()
|
|
49
|
+
const n = Math.min(rect.w, chars.length)
|
|
50
|
+
if (rect.h <= 0 || n <= 0) return
|
|
51
|
+
|
|
52
|
+
const spread = this.opts?.spread ?? 2.25
|
|
53
|
+
const base = Math.max(0, Math.min(23, (this.opts?.baseGray ?? 12) | 0))
|
|
54
|
+
const boost = Math.max(0, Math.min(23, (this.opts?.boost ?? 8) | 0))
|
|
55
|
+
const boldTh = this.opts?.boldThreshold ?? 0.75
|
|
56
|
+
|
|
57
|
+
const progress = this.opts?.progress
|
|
58
|
+
const center = progress != null ? progress * (chars.length - 1) : (this.opts?.center ?? 0)
|
|
59
|
+
|
|
60
|
+
const styleFor = (i: number) => {
|
|
61
|
+
const d = (i - center) / Math.max(1e-3, spread)
|
|
62
|
+
const intensity = Math.exp(-d * d)
|
|
63
|
+
const level = Math.max(0, Math.min(23, base + Math.round(intensity * boost)))
|
|
64
|
+
const bold = intensity > boldTh
|
|
65
|
+
return pal.id({ fg: Colors.gray(level), bold })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Emit minimal runs per style id
|
|
69
|
+
const y = rect.y
|
|
70
|
+
let x = rect.x
|
|
71
|
+
let runStyle = -1
|
|
72
|
+
let runStart = x
|
|
73
|
+
let runText = ""
|
|
74
|
+
const flush = () => {
|
|
75
|
+
if (runText.length > 0) {
|
|
76
|
+
const maxW = Math.max(0, rect.x + rect.w - runStart)
|
|
77
|
+
if (maxW > 0) s.drawText(runStart, y, runText, runStyle, maxW)
|
|
78
|
+
runText = ""
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (let i = 0; i < n; i++) {
|
|
82
|
+
const ch = chars[i]
|
|
83
|
+
const id = styleFor(i)
|
|
84
|
+
if (id !== runStyle) {
|
|
85
|
+
flush()
|
|
86
|
+
runStyle = id
|
|
87
|
+
runStart = x
|
|
88
|
+
}
|
|
89
|
+
runText += ch
|
|
90
|
+
x++
|
|
91
|
+
}
|
|
92
|
+
flush()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Optional builder contribution for the View object
|
|
97
|
+
export type ViewShinyTextExt = {
|
|
98
|
+
shineText(text: string, opts?: ShineTextOptions): View
|
|
99
|
+
}
|
|
100
|
+
export const viewShinyText: ViewShinyTextExt = {
|
|
101
|
+
shineText(text: string, opts?: ShineTextOptions): View {
|
|
102
|
+
return new ShinyText(text, opts)
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ----------------------------------------------------------------------------
|
|
107
|
+
// Stateful convenience API (init/tick/setSpeed + node factory)
|
|
108
|
+
// ----------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
export type ShinyTextModel = {
|
|
111
|
+
text: string
|
|
112
|
+
step: Stepper
|
|
113
|
+
speedMs: number
|
|
114
|
+
overscan: number
|
|
115
|
+
shine?: Omit<ShineTextOptions, "center" | "progress">
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type InitShinyOptions = {
|
|
119
|
+
speedMs?: number
|
|
120
|
+
overscan?: number
|
|
121
|
+
shine?: Omit<ShineTextOptions, "center" | "progress">
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function initShinyText(text: string, opts?: InitShinyOptions): ShinyTextModel {
|
|
125
|
+
const speedMs = Math.max(1, (opts?.speedMs ?? 40) | 0)
|
|
126
|
+
const overscan = Math.max(0, (opts?.overscan ?? 6) | 0)
|
|
127
|
+
const width = [...text].length
|
|
128
|
+
const step = Step.init(width + overscan * 2, speedMs, 0)
|
|
129
|
+
return { text, step, speedMs, overscan, shine: opts?.shine }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function tickShinyText(state: ShinyTextModel, nowMs: number): ShinyTextModel {
|
|
133
|
+
const step = Step.tick(state.step, nowMs)
|
|
134
|
+
return { ...state, step }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function setShinySpeed(state: ShinyTextModel, speedMs: number): ShinyTextModel {
|
|
138
|
+
const ms = Math.max(1, speedMs | 0)
|
|
139
|
+
const step = { ...state.step, intervalMs: ms }
|
|
140
|
+
return { ...state, speedMs: ms, step }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Create a ShinyText node from state. Use with chainers like .frame/.padding. */
|
|
144
|
+
export function shinyText(state: ShinyTextModel): View {
|
|
145
|
+
const center = state.step.index - state.overscan
|
|
146
|
+
return new ShinyText(state.text, { ...state.shine, center })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Merged namespace to expose a cohesive API: ShinyText.make(), ShinyText.tick(), ShinyText.setSpeed(), ShinyText.view()
|
|
150
|
+
export namespace ShinyText {
|
|
151
|
+
export type Model = ShinyTextModel
|
|
152
|
+
export type Options = ShineTextOptions
|
|
153
|
+
export type MakeOptions = InitShinyOptions
|
|
154
|
+
|
|
155
|
+
export const make = initShinyText
|
|
156
|
+
export const tick = tickShinyText
|
|
157
|
+
export const setSpeed = setShinySpeed
|
|
158
|
+
export const view = shinyText
|
|
159
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* text/layout.ts — Shared text layout utilities for wrapping and caret mapping
|
|
2
|
+
*
|
|
3
|
+
* Single source of truth for:
|
|
4
|
+
* - Computing wrapped visual lines (first-line width vs subsequent)
|
|
5
|
+
* - Mapping between grapheme cursor index and visual caret coordinates
|
|
6
|
+
* - Mapping from visual cell X back to cursor index (for vertical nav)
|
|
7
|
+
*
|
|
8
|
+
* These helpers mirror renderer behavior by delegating to render/measure.ts
|
|
9
|
+
* (displayWidth, graphemes, sliceByWidth), ensuring edit-time and render-time
|
|
10
|
+
* wrapping boundaries are identical.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { displayWidth, graphemes, sliceByWidth } from "../../render/measure.js"
|
|
14
|
+
|
|
15
|
+
export type WrappedLine = { text: string; graphemes: string[]; start: number; width: number }
|
|
16
|
+
export type Wrapped = { lines: Array<WrappedLine> }
|
|
17
|
+
|
|
18
|
+
export type WrapOptions = {
|
|
19
|
+
widthFirst: number
|
|
20
|
+
widthOther: number
|
|
21
|
+
wordWrap: boolean
|
|
22
|
+
breakWords: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Compute wrapped lines with widths and grapheme start offsets (soft + hard breaks). */
|
|
26
|
+
export function wrapText(value: string, opts: WrapOptions): Wrapped {
|
|
27
|
+
const lines: Array<{ graphemes: string[]; start: number; width: number }> = []
|
|
28
|
+
const widthFor = (isFirstVisual: boolean) => Math.max(0, isFirstVisual ? opts.widthFirst : opts.widthOther)
|
|
29
|
+
|
|
30
|
+
let gOffset = 0 // grapheme index from start of whole value
|
|
31
|
+
let isFirstVisual = true
|
|
32
|
+
|
|
33
|
+
// Split logical lines by explicit newlines
|
|
34
|
+
const logicals = value.split("\n")
|
|
35
|
+
for (let li = 0; li < logicals.length; li++) {
|
|
36
|
+
const logical = logicals[li]
|
|
37
|
+
let rest = logical
|
|
38
|
+
// Even for empty logical lines, emit one visual line (empty)
|
|
39
|
+
if (rest.length === 0) {
|
|
40
|
+
lines.push({ graphemes: [], start: gOffset, width: 0 })
|
|
41
|
+
if (li < logicals.length - 1) gOffset += 1 // the "\n" grapheme
|
|
42
|
+
isFirstVisual = false
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
while (rest.length > 0) {
|
|
46
|
+
const maxW = widthFor(isFirstVisual)
|
|
47
|
+
const { text } = sliceByWidth(rest, maxW)
|
|
48
|
+
const used = text.length > 0 ? text : rest.slice(0, 1) // ensure progress even when maxW == 0
|
|
49
|
+
const usedGs = graphemes(used)
|
|
50
|
+
const w = displayWidth(used)
|
|
51
|
+
lines.push({ graphemes: usedGs, start: gOffset, width: w })
|
|
52
|
+
gOffset += usedGs.length
|
|
53
|
+
rest = rest.slice(used.length)
|
|
54
|
+
isFirstVisual = false
|
|
55
|
+
if (used.length === 0) break
|
|
56
|
+
}
|
|
57
|
+
if (li < logicals.length - 1) gOffset += 1 // account for newline between logical segments
|
|
58
|
+
}
|
|
59
|
+
return { lines: lines.map((l) => ({ ...l, text: l.graphemes.join("") })) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Locate the visual line and column within that line for a grapheme cursor. */
|
|
63
|
+
export function findVisualPos(wrap: Wrapped, cursor: number): { lineIdx: number; colInLine: number } {
|
|
64
|
+
for (let i = 0; i < wrap.lines.length; i++) {
|
|
65
|
+
const ln = wrap.lines[i]
|
|
66
|
+
const start = ln.start
|
|
67
|
+
// Use next line's start as upper bound (accounts for newline gaps)
|
|
68
|
+
const nextStart = i + 1 < wrap.lines.length ? wrap.lines[i + 1].start : Infinity
|
|
69
|
+
|
|
70
|
+
// Cursor in range [start, nextStart) belongs to this line
|
|
71
|
+
if (cursor >= start && cursor < nextStart) {
|
|
72
|
+
// For empty lines, cursor must be exactly at start
|
|
73
|
+
if (ln.graphemes.length === 0) {
|
|
74
|
+
return { lineIdx: i, colInLine: 0 }
|
|
75
|
+
}
|
|
76
|
+
const col = Math.min(cursor - start, ln.graphemes.length)
|
|
77
|
+
return { lineIdx: i, colInLine: col }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Fallback: end of last line
|
|
81
|
+
const last = wrap.lines[wrap.lines.length - 1]
|
|
82
|
+
return { lineIdx: wrap.lines.length - 1, colInLine: last.graphemes.length }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Sum display widths of graphemes up to a column to get visual cell X. */
|
|
86
|
+
export function cellXWithinLine(wrap: Wrapped, lineIdx: number, colInLine: number): number {
|
|
87
|
+
const ln = wrap.lines[lineIdx]
|
|
88
|
+
const gs = ln.graphemes
|
|
89
|
+
let x = 0
|
|
90
|
+
for (let i = 0; i < Math.min(colInLine, gs.length); i++) x += displayWidth(gs[i])
|
|
91
|
+
return x
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Map a visual cell X back to a cursor index on a given line (nearest-left by centers). */
|
|
95
|
+
export function cursorFromCellX(wrap: Wrapped, lineIdx: number, x: number): number {
|
|
96
|
+
const ln = wrap.lines[lineIdx]
|
|
97
|
+
const gs = ln.graphemes
|
|
98
|
+
let acc = 0
|
|
99
|
+
for (let i = 0; i < gs.length; i++) {
|
|
100
|
+
const w = displayWidth(gs[i])
|
|
101
|
+
const center = acc + w / 2
|
|
102
|
+
if (x < center) return ln.start + i
|
|
103
|
+
acc += w
|
|
104
|
+
}
|
|
105
|
+
return ln.start + gs.length
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Map cursor index to absolute caret coordinates using provided anchors. */
|
|
109
|
+
export function caretXYFromCursor(
|
|
110
|
+
wrap: Wrapped,
|
|
111
|
+
cursor: number,
|
|
112
|
+
anchors: { xFirst: number; xOther: number },
|
|
113
|
+
): { x: number; y: number } {
|
|
114
|
+
const { lineIdx, colInLine } = findVisualPos(wrap, cursor)
|
|
115
|
+
const baseX = lineIdx === 0 ? anchors.xFirst : anchors.xOther
|
|
116
|
+
const x = baseX + cellXWithinLine(wrap, lineIdx, colInLine)
|
|
117
|
+
const y = lineIdx
|
|
118
|
+
return { x, y }
|
|
119
|
+
}
|