mirror of
https://github.com/anomalyco/opentui.git
synced 2026-09-19 01:26:03 +08:00
Make it so you can now write text anywhere using solid (#42)
Add "ghost" text nodes if the text parent is not a text renderable Handle cleanup of text nodes Handle anchoring to text nodes properly Fix bug where text is not updating properly Let me know if you want me to clean up this code, its a bit messy
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opentui/core",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"dependencies": {
|
||||
"jimp": "1.6.0",
|
||||
"yoga-layout": "3.2.1",
|
||||
@@ -19,12 +19,12 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@dimforge/rapier2d-simd-compat": "^0.17.3",
|
||||
"@opentui/core-darwin-arm64": "0.1.5",
|
||||
"@opentui/core-darwin-x64": "0.1.5",
|
||||
"@opentui/core-linux-arm64": "0.1.5",
|
||||
"@opentui/core-linux-x64": "0.1.5",
|
||||
"@opentui/core-win32-arm64": "0.1.5",
|
||||
"@opentui/core-win32-x64": "0.1.5",
|
||||
"@opentui/core-darwin-arm64": "0.1.6",
|
||||
"@opentui/core-darwin-x64": "0.1.6",
|
||||
"@opentui/core-linux-arm64": "0.1.6",
|
||||
"@opentui/core-linux-x64": "0.1.6",
|
||||
"@opentui/core-win32-arm64": "0.1.6",
|
||||
"@opentui/core-win32-x64": "0.1.6",
|
||||
"bun-webgpu": "0.1.3",
|
||||
"planck": "^1.4.2",
|
||||
"three": "0.177.0",
|
||||
@@ -35,8 +35,9 @@
|
||||
},
|
||||
"packages/react": {
|
||||
"name": "@opentui/react",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"dependencies": {
|
||||
"@opentui/core": "workspace:*",
|
||||
"react-reconciler": "^0.32.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -45,14 +46,13 @@
|
||||
"prettier": "3.6.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": "workspace:*",
|
||||
"react": ">=19.0.0",
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
"packages/solid": {
|
||||
"name": "@opentui/solid",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"dependencies": {
|
||||
"@opentui/core": "workspace:*",
|
||||
},
|
||||
@@ -199,17 +199,17 @@
|
||||
|
||||
"@opentui/core": ["@opentui/core@workspace:packages/core"],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5/JQdjeko52fjyrJ9SRRz9W0aCUPC9okGFUJkVEoIxmypC5qsOpG+ExVz5HdtX4iEC9HI2GvLPd5FwLquABUeA=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-H1B1smKYfr1E1ovCKJz3N+uYBX4ZK37FOqrpmwgtjdCzp3V+1lHP/dmM+tX6MWgFUP2qyMJ4CfRPpGJrW2jakQ=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1GTjLSsuP3rXhJJf9eDk4gMBtt1FSchfjsudfL4mlUQwRSXuJUTdtiJnl6frZZ0Q9quLraigLnQ0FsBU5VgKFg=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-CNqu/UGpciPh95OKboE5V9V2zkWXJRcfkPOgw5+Y+FLyf4z3v1+2vztXZJihz+FcufVDARy3uLS9kYispX0MFg=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-j0TFLhDY3NJ6C6Dua3sVHXFC4dP2s4HI1k3j2lYaSIYF99wp7PJxv8dJchBGFeb88yRuAmxnTJRmptj5POk9oA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-s8c40QcFZlTiFRMYGhEMXzO5D8SH2GaNtmFkDDPIKn58ZxTTG2rOinbEv96fCMaKNgMEfZ70MLc2evyeL3vW8A=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-/rSgJmPJqxvCS1zQyEdgQgJeWhZ35e5yBsRjsrGajUoFGMUqnttCT/rSqNlpmWlmE8KEYkDdpQ+o8nPT9aUHYg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kvhuvYejVp7p+H4TyMPz5izKafh7qHOQhoEEtBGQDpbzMH/9rLiRKKFCRH3bpP9VWGVb6QTEipoGwdTjGzOSqQ=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-2GYlTfxpPJifSSFX51q4qiigb0K5Fz+lKvV2H5jbnktowkaCto1d1UnTFnagx3fiahTNuV3AB4bilVYMj3hWyg=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-yeoV/KYLipJLbdRn3IU39Zwcv4Y7CJyrm9oAjUKoocpzImH8jZJW663qmTW06yTi7NMVkwfTnRV2YmTMjxF+gQ=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-l7o4rFZ/UKhdzOceEAa6hsIEYDXqQRFlklTSWbzDCPnAKbiQVTHjOc11f+meiaKBHN5lE7EOpQDJxm2V4u1Bcw=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-1emGMn880UbnaU2n4Dv78iuLpUSc8kAstImFN8k5zgBOIsOOkelyvCA8Y8A5aB2otPsNm5o6ueAJpucfiWA0ng=="],
|
||||
|
||||
"@opentui/react": ["@opentui/react@workspace:packages/react"],
|
||||
|
||||
|
||||
@@ -142,8 +142,8 @@ export class TextRenderable extends Renderable {
|
||||
const changed = this.selectionHelper.reevaluateSelection(this.width, this.height)
|
||||
if (changed) {
|
||||
this.syncSelectionToTextBuffer()
|
||||
this.needsUpdate()
|
||||
}
|
||||
this.needsUpdate()
|
||||
}
|
||||
|
||||
private setupMeasureFunc(): void {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createSignal, onCleanup, onMount } from "solid-js";
|
||||
export default function TextStyleScene() {
|
||||
const [counter, setCounter] = createSignal(0);
|
||||
|
||||
let interval: NodeJS.Timer;
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
onMount(() => {
|
||||
interval = setInterval(() => {
|
||||
@@ -24,6 +24,10 @@ export default function TextStyleScene() {
|
||||
<text>
|
||||
And {bold("chunk arrays")} work {fg("#ff8c00")("as welll")}!! {italic(underline(`${counter()}`))}
|
||||
</text>
|
||||
You do not need to have a text node {counter()} as a parent when dealing with text {counter()}
|
||||
<box>
|
||||
{counter()} Mix in some {bold("more text")} {counter()}
|
||||
</box>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* @refresh skip */
|
||||
import {
|
||||
GroupRenderable,
|
||||
InputRenderable,
|
||||
InputRenderableEvents,
|
||||
Renderable,
|
||||
@@ -16,16 +15,20 @@ import { createRenderer } from "solid-js/universal";
|
||||
import { elements, type Element } from "./elements";
|
||||
import { getNextId } from "./utils/id-counter";
|
||||
|
||||
const GHOST_NODE_TAG = "text-ghost" as const;
|
||||
|
||||
class TextNode {
|
||||
id: string;
|
||||
chunk: TextChunk;
|
||||
parent?: Renderable;
|
||||
textParent?: TextRenderable;
|
||||
|
||||
constructor(chunk: TextChunk) {
|
||||
this.id = getNextId("text-node");
|
||||
this.chunk = chunk;
|
||||
}
|
||||
}
|
||||
|
||||
const ChunkToTextNodeMap = new WeakMap<TextChunk, TextNode>();
|
||||
|
||||
type DomNode = Renderable | TextNode;
|
||||
@@ -34,43 +37,106 @@ const log = (...args: any[]) => {
|
||||
console.log("[Reconciler]", ...args);
|
||||
};
|
||||
|
||||
function getOrCreateTextGhostNode(parent: Renderable, anchor?: DomNode | null): TextRenderable {
|
||||
if (anchor instanceof TextNode && anchor.textParent) {
|
||||
// prepend text to anchor
|
||||
return anchor.textParent;
|
||||
}
|
||||
const children = parent.getChildren();
|
||||
|
||||
if (anchor instanceof Renderable) {
|
||||
const anchorIndex = children.findIndex((el) => el.id === anchor.id);
|
||||
const beforeAnchor = children[anchorIndex - 1];
|
||||
if (beforeAnchor instanceof TextRenderable && beforeAnchor.id.startsWith(GHOST_NODE_TAG)) {
|
||||
// append text to previous
|
||||
return beforeAnchor;
|
||||
}
|
||||
}
|
||||
|
||||
const lastChild = children.at(-1);
|
||||
if (lastChild instanceof TextRenderable && lastChild.id.startsWith(GHOST_NODE_TAG)) {
|
||||
// Append text to last child if exists
|
||||
return lastChild;
|
||||
}
|
||||
|
||||
// Create a new ghost node
|
||||
const ghostNode = new TextRenderable(getNextId(GHOST_NODE_TAG), {});
|
||||
|
||||
_insertNode(parent, ghostNode, anchor);
|
||||
|
||||
return ghostNode;
|
||||
}
|
||||
|
||||
function insertTextNode(parent: DomNode, node: TextNode, anchor?: DomNode | null): void {
|
||||
if (!(parent instanceof Renderable)) {
|
||||
console.warn("Attaching text node to parent text node, impossible");
|
||||
return;
|
||||
}
|
||||
log("Inserting text node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
||||
|
||||
let textParent: TextRenderable;
|
||||
// get parent text renderable
|
||||
if (!(parent instanceof TextRenderable)) {
|
||||
textParent = getOrCreateTextGhostNode(parent, anchor);
|
||||
} else {
|
||||
textParent = parent;
|
||||
}
|
||||
|
||||
node.textParent = textParent;
|
||||
const styledText = textParent.content;
|
||||
|
||||
if (anchor && anchor instanceof TextNode) {
|
||||
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
||||
if (anchorIndex == -1) {
|
||||
console.log("anchor not found");
|
||||
return;
|
||||
}
|
||||
styledText.insert(node.chunk, anchorIndex);
|
||||
} else {
|
||||
const firstChunk = textParent.content.chunks[0];
|
||||
// Handles the default unlinked chunk
|
||||
if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
|
||||
styledText.replace(node.chunk, firstChunk);
|
||||
} else {
|
||||
styledText.insert(node.chunk);
|
||||
}
|
||||
}
|
||||
textParent.content = styledText;
|
||||
node.parent = parent;
|
||||
return;
|
||||
}
|
||||
|
||||
function removeTextNode(parent: DomNode, node: TextNode): void {
|
||||
if (!(parent instanceof Renderable)) {
|
||||
// cleanup orphaned node
|
||||
ChunkToTextNodeMap.delete(node.chunk);
|
||||
return;
|
||||
}
|
||||
if (parent === node.textParent && parent instanceof TextRenderable) {
|
||||
ChunkToTextNodeMap.delete(node.chunk);
|
||||
const styledText = parent.content;
|
||||
styledText.remove(node.chunk);
|
||||
parent.content = styledText;
|
||||
} else if (node.textParent) {
|
||||
// check to remove ghost node
|
||||
ChunkToTextNodeMap.delete(node.chunk);
|
||||
const styledText = node.textParent.content;
|
||||
styledText.remove(node.chunk);
|
||||
|
||||
if (styledText.chunks.length > 0) {
|
||||
node.textParent.content = styledText;
|
||||
} else {
|
||||
node.parent?.remove(node.textParent.id);
|
||||
node.textParent.destroyRecursively();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _insertNode(parent: DomNode, node: DomNode, anchor?: DomNode | null): void {
|
||||
log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
||||
|
||||
if (node instanceof TextNode) {
|
||||
// Text nodes
|
||||
if (!(parent instanceof TextRenderable)) {
|
||||
console.warn(`Cannot insert text:"${node.chunk.plainText}" unless wrapped with a <text> element.`);
|
||||
const ghostNode = new GroupRenderable(getNextId("ghost-group"), {});
|
||||
// Sets display to contents so that it doesn't affect layout
|
||||
ghostNode.getLayoutNode().yogaNode.setDisplay(2);
|
||||
|
||||
if (parent instanceof Renderable) {
|
||||
parent.add(ghostNode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const styledText = parent.content;
|
||||
|
||||
if (anchor && anchor instanceof TextNode) {
|
||||
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
||||
if (anchorIndex == -1) {
|
||||
console.log("anchor not found");
|
||||
return;
|
||||
}
|
||||
styledText.insert(node.chunk, anchorIndex);
|
||||
} else {
|
||||
const firstChunk = parent.content.chunks[0];
|
||||
// Handles the default unlinked chunk
|
||||
if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
|
||||
styledText.replace(node.chunk, firstChunk);
|
||||
} else {
|
||||
styledText.insert(node.chunk);
|
||||
}
|
||||
}
|
||||
parent.content = styledText;
|
||||
node.parent = parent;
|
||||
return;
|
||||
return insertTextNode(parent, node, anchor);
|
||||
}
|
||||
|
||||
// Renderable nodes
|
||||
@@ -79,7 +145,12 @@ function _insertNode(parent: DomNode, node: DomNode, anchor?: DomNode | null): v
|
||||
}
|
||||
|
||||
if (anchor) {
|
||||
const anchorIndex = parent.getChildren().findIndex((el) => el.id === anchor.id);
|
||||
const anchorIndex = parent.getChildren().findIndex((el) => {
|
||||
if (anchor instanceof TextNode) {
|
||||
return el.id === anchor.textParent?.id;
|
||||
}
|
||||
return el.id === anchor.id;
|
||||
});
|
||||
parent.add(node, anchorIndex);
|
||||
} else {
|
||||
parent.add(node);
|
||||
@@ -88,14 +159,12 @@ function _insertNode(parent: DomNode, node: DomNode, anchor?: DomNode | null): v
|
||||
|
||||
function _removeNode(parent: DomNode, node: DomNode): void {
|
||||
log("Removing node:", node.id, "from parent:", parent.id);
|
||||
if (parent instanceof TextRenderable && node instanceof TextNode) {
|
||||
ChunkToTextNodeMap.delete(node.chunk);
|
||||
const styledText = parent.content;
|
||||
styledText.remove(node.chunk);
|
||||
parent.content = styledText;
|
||||
} else if (parent instanceof Renderable && node instanceof Renderable) {
|
||||
node.destroyRecursively();
|
||||
if (node instanceof TextNode) {
|
||||
return removeTextNode(parent, node);
|
||||
}
|
||||
if (parent instanceof Renderable && node instanceof Renderable) {
|
||||
parent.remove(node.id);
|
||||
node.destroyRecursively();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,15 +214,15 @@ export const {
|
||||
plainText: value,
|
||||
};
|
||||
|
||||
const parent = textNode.parent;
|
||||
if (!parent) {
|
||||
const textParent = textNode.textParent;
|
||||
if (!textParent) {
|
||||
log("No parent found for text node:", textNode.id);
|
||||
return;
|
||||
}
|
||||
if (parent instanceof TextRenderable) {
|
||||
const styledText = parent.content;
|
||||
if (textParent instanceof TextRenderable) {
|
||||
const styledText = textParent.content;
|
||||
styledText.replace(newChunk, textNode.chunk);
|
||||
parent.content = styledText;
|
||||
textParent.content = styledText;
|
||||
|
||||
textNode.chunk = newChunk;
|
||||
ChunkToTextNodeMap.set(newChunk, textNode);
|
||||
|
||||
Reference in New Issue
Block a user