diff --git a/deno.json b/deno.json index 1e00e4f..fd4aabb 100644 --- a/deno.json +++ b/deno.json @@ -2,6 +2,7 @@ "workspace": ["instance/", "instance/src/web/", "applications/*/web/", "applications/*/backend/", "applications/*/electron/", "uikit-solid"], "tasks": { "dev": "deno -A --watch instance/src/index.ts", + "start": "deno -A instance/src/index.ts", "dev-debug": "deno -A --inspect instance/src/index.ts", "build-web": "deno --cwd instance/src/web build", "uikit": "deno --cwd uikit-solid dev", diff --git a/instance/src/index.ts b/instance/src/index.ts index 29b2fbd..cfdf5e8 100644 --- a/instance/src/index.ts +++ b/instance/src/index.ts @@ -15,11 +15,11 @@ import NotificationsSystem from "./systems/notifications.ts"; import ReverseProxySystem from "./systems/reverseProxy.ts"; import { StringListApplicationSetting } from "./systems/settings/applicationSetting/stringListSetting.ts"; import SettingsSystem from "./systems/settings.ts"; +import TerminalUISystem from "./systems/terminalUI.ts"; import TRPCSystem from "./systems/trpc.ts"; import { createTRPCContext as createWorkspacesTRPCContext, workspacesRouter } from "./systems/trpcRouter.ts"; import UsersSystem from "./systems/users.ts"; import WebFrontendSystem from "./systems/webFrontend.ts"; -import TerminalUISystem from "./systems/terminalUI.ts"; export enum InstanceStatus { Online, diff --git a/instance/src/systems/terminalUI.ts b/instance/src/systems/terminalUI.ts index 2e34847..3bb5009 100644 --- a/instance/src/systems/terminalUI.ts +++ b/instance/src/systems/terminalUI.ts @@ -1,10 +1,10 @@ import type { Instance } from "../index.ts"; import System from "../system.ts"; -import TerminalPane from "./terminalUI/pane.ts"; -import { cursorTo } from "./terminalUI/stdout.ts"; import Terminal from "./terminalUI/terminal.ts"; import TerminalBorderView from "./terminalUI/views/borderView.ts"; +import TerminalContainerView from "./terminalUI/views/container.ts"; import TerminalCrossView from "./terminalUI/views/crossView.ts"; +import TerminalTextInputView from "./terminalUI/views/textInput.ts"; export default class TerminalUISystem extends System { constructor(instance: Instance) { @@ -17,25 +17,33 @@ export default class TerminalUISystem extends System { await super.startup(); const terminal = new Terminal(); - const testPane = new TerminalPane(terminal.activeScreen, { width: { unit: "%", value: 100 }, height: { unit: "%", value: 100 } }); - const borderView = new TerminalBorderView(); - borderView.childViews.push(new TerminalCrossView()); - // borderView.childViews.push(new TerminalCrossView()); - // borderView.childViews.push(new TerminalCrossView()); - // borderView.childViews.push(new TerminalCrossView()); - // borderView.childViews.push(new TerminalCrossView()); - // borderView.childViews.push(new TerminalCrossView()); + // biome-ignore format: TUI description + terminal.screen.childView.addChild( + new TerminalContainerView().setFlowDirection("y") + .addChild( + new TerminalBorderView() + .addChild(new TerminalContainerView().setFlowDirection("y") + .addChild( + new TerminalTextInputView() + .setPlaceholder("Placeholder Text") + ) + .addChild( + new TerminalContainerView().setFlowDirection("x") + .addChild(new TerminalCrossView()) + .addChild(new TerminalCrossView()) + ) + ) + ) + .addChild( + new TerminalBorderView() + .setDimensions({width: { unit: "%", value: 100 }, height: { unit: "px", value: 5 } }) + ) + ); - testPane.childViews.push(borderView); - - terminal.activeScreen.visiblePanes.push(testPane); - - await terminal.draw(); setInterval(async () => { await terminal.draw(); - await cursorTo(terminal.width, terminal.height); - }, 50); + }, 20); return true; } diff --git a/instance/src/systems/terminalUI/effect.ts b/instance/src/systems/terminalUI/effect.ts new file mode 100644 index 0000000..aed3aa0 --- /dev/null +++ b/instance/src/systems/terminalUI/effect.ts @@ -0,0 +1,194 @@ +export enum TerminalColor { + Black = 1, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, +} + +export default class TerminalEffect { + private foregroundColor?: TerminalColor; + private backgroundColor?: TerminalColor; + private bold?: boolean; + private underline?: boolean; + + setBold() { + this.bold = true; + + return this; + } + + setUnderline() { + this.underline = true; + + return this; + } + + setColor(color: TerminalColor) { + this.foregroundColor = color; + + return this; + } + + setBackgroundColor(color: TerminalColor) { + this.backgroundColor = color; + + return this; + } + + async _internal_apply(): Promise { + let outputString = ""; + + if (this.bold) outputString += "\x1b[1m"; + if (this.underline) outputString += "\x1b[4m"; + if (this.foregroundColor) { + switch (this.foregroundColor) { + case TerminalColor.Black: { + outputString += "\x1b[30m"; + break; + } + case TerminalColor.Red: { + outputString += "\x1b[31m"; + break; + } + case TerminalColor.Green: { + outputString += "\x1b[32m"; + break; + } + case TerminalColor.Yellow: { + outputString += "\x1b[33m"; + break; + } + case TerminalColor.Blue: { + outputString += "\x1b[34m"; + break; + } + case TerminalColor.Magenta: { + outputString += "\x1b[35m"; + break; + } + case TerminalColor.Cyan: { + outputString += "\x1b[36m"; + break; + } + case TerminalColor.White: { + outputString += "\x1b[37m"; + break; + } + case TerminalColor.BrightBlack: { + outputString += "\x1b[90m"; + break; + } + case TerminalColor.BrightRed: { + outputString += "\x1b[91m"; + break; + } + case TerminalColor.BrightGreen: { + outputString += "\x1b[92m"; + break; + } + case TerminalColor.BrightYellow: { + outputString += "\x1b[93m"; + break; + } + case TerminalColor.BrightBlue: { + outputString += "\x1b[94m"; + break; + } + case TerminalColor.BrightMagenta: { + outputString += "\x1b[95m"; + break; + } + case TerminalColor.BrightCyan: { + outputString += "\x1b[96m"; + break; + } + case TerminalColor.BrightWhite: { + outputString += "\x1b[97m"; + break; + } + } + } + if (this.backgroundColor) { + switch (this.backgroundColor) { + case TerminalColor.Black: { + outputString += "\x1b[40m"; + break; + } + case TerminalColor.Red: { + outputString += "\x1b[41m"; + break; + } + case TerminalColor.Green: { + outputString += "\x1b[42m"; + break; + } + case TerminalColor.Yellow: { + outputString += "\x1b[43m"; + break; + } + case TerminalColor.Blue: { + outputString += "\x1b[44m"; + break; + } + case TerminalColor.Magenta: { + outputString += "\x1b[45m"; + break; + } + case TerminalColor.Cyan: { + outputString += "\x1b[46m"; + break; + } + case TerminalColor.White: { + outputString += "\x1b[47m"; + break; + } + case TerminalColor.BrightBlack: { + outputString += "\x1b[100m"; + break; + } + case TerminalColor.BrightRed: { + outputString += "\x1b[101m"; + break; + } + case TerminalColor.BrightGreen: { + outputString += "\x1b[102m"; + break; + } + case TerminalColor.BrightYellow: { + outputString += "\x1b[103m"; + break; + } + case TerminalColor.BrightBlue: { + outputString += "\x1b[104m"; + break; + } + case TerminalColor.BrightMagenta: { + outputString += "\x1b[105m"; + break; + } + case TerminalColor.BrightCyan: { + outputString += "\x1b[106m"; + break; + } + case TerminalColor.BrightWhite: { + outputString += "\x1b[107m"; + break; + } + } + } + + return outputString; + } +} diff --git a/instance/src/systems/terminalUI/pane.ts b/instance/src/systems/terminalUI/pane.ts deleted file mode 100644 index ed9240f..0000000 --- a/instance/src/systems/terminalUI/pane.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type TerminalScreen from "./screen.ts"; -import TerminalSizableElement from "./sizableElement.ts"; -import type TerminalEvent from "./terminalEvent.ts"; -import type TerminalView from "./view.ts"; - -export default class TerminalPane extends TerminalSizableElement { - eventListeners: ((event: TerminalEvent) => void)[]; - childViews: TerminalView[]; - parent: TerminalScreen; - - constructor(screen: TerminalScreen, dimensions: TerminalPane["dimensions"]) { - super(); - - this.parent = screen; - this.dimensions = dimensions; - this.eventListeners = []; - this.childViews = []; - } - - override calculateSize(parentContentAbsoluteDimensions: { width: number; height: number }): void { - super.calculateSize(parentContentAbsoluteDimensions); - - for (const childView of this.childViews) { - childView.calculateSize(this.absoluteDimensions); - } - } - - async draw(drawOrigin: { x: number; y: number }) { - for (const childViews of this.childViews) { - await childViews.draw(drawOrigin); - } - } -} diff --git a/instance/src/systems/terminalUI/scalarValue.ts b/instance/src/systems/terminalUI/scalarValue.ts new file mode 100644 index 0000000..c39beb3 --- /dev/null +++ b/instance/src/systems/terminalUI/scalarValue.ts @@ -0,0 +1,101 @@ +import type TerminalSizableElement from "./sizableElement.ts"; + +enum TerminalScalarValueOperation { + Add, + Subtract, + Multiply, + Divide, + Floor, + Ceil, +} + +export enum TerminalScalarValueUnit { + Pixel, + PercentageOfOriginalValue, +} + +export default class TerminalScalarValue { + operationChain: [TerminalScalarValueOperation, number?, TerminalScalarValueUnit?][]; + + constructor() { + this.operationChain = []; + } + + add(value: number, unit: TerminalScalarValueUnit) { + this.operationChain.push([TerminalScalarValueOperation.Add, value, unit]); + return this; + } + subtract(value: number, unit: TerminalScalarValueUnit) { + this.operationChain.push([TerminalScalarValueOperation.Subtract, value, unit]); + return this; + } + multiply(value: number) { + this.operationChain.push([TerminalScalarValueOperation.Multiply, value]); + return this; + } + divide(value: number) { + this.operationChain.push([TerminalScalarValueOperation.Divide, value]); + return this; + } + floor() { + this.operationChain.push([TerminalScalarValueOperation.Floor]); + return this; + } + ceil() { + this.operationChain.push([TerminalScalarValueOperation.Ceil]); + return this; + } + + _internal_calculateFromSizableElement(sizableElement: TerminalSizableElement, valueType: "width" | "height"): number { + let val = sizableElement.absoluteDimensions[valueType]; + + for (const operation of this.operationChain) { + switch (operation[0]) { + case TerminalScalarValueOperation.Add: { + switch (operation[2]) { + case TerminalScalarValueUnit.Pixel: { + val += operation[1]!; + break; + } + case TerminalScalarValueUnit.PercentageOfOriginalValue: { + val += (val / 100) * operation[1]!; + break; + } + } + break; + } + case TerminalScalarValueOperation.Subtract: { + switch (operation[2]) { + case TerminalScalarValueUnit.Pixel: { + val -= operation[1]!; + break; + } + case TerminalScalarValueUnit.PercentageOfOriginalValue: { + val -= (val / 100) * operation[1]!; + break; + } + } + break; + } + case TerminalScalarValueOperation.Multiply: { + val *= operation[1]!; + break; + } + case TerminalScalarValueOperation.Divide: { + val /= operation[1]!; + break; + } + case TerminalScalarValueOperation.Floor: { + val = Math.floor(val); + break; + } + case TerminalScalarValueOperation.Ceil: { + val = Math.ceil(val); + break; + } + } + } + + return val; + } +} diff --git a/instance/src/systems/terminalUI/screen.ts b/instance/src/systems/terminalUI/screen.ts index 5024f6b..b9840d5 100644 --- a/instance/src/systems/terminalUI/screen.ts +++ b/instance/src/systems/terminalUI/screen.ts @@ -1,53 +1,157 @@ -import type TerminalPane from "./pane.ts"; -import { clearScreenDown, cursorTo } from "./stdout.ts"; +import type TerminalEffect from "./effect.ts"; import type Terminal from "./terminal.ts"; -import { TerminalEventType } from "./terminalEvent.ts"; +import type TerminalView from "./view.ts"; +import TerminalContainerView from "./views/container.ts"; export default class TerminalScreen { parentTerminal: Terminal; - contentBuffer: string[]; - visiblePanes: TerminalPane[]; - activePane: number; + private previousDrawBuffer: [string, string?][][]; + contentBuffer: [string, TerminalEffect?][]; + private cursorPosition: number; + visualCursorStopPosition: [number, number]; + _internal_isDrawingFrame: boolean; + width: number; + height: number; + childView: TerminalView; constructor(terminal: Terminal) { this.parentTerminal = terminal; - this.visiblePanes = []; + this.previousDrawBuffer = []; this.contentBuffer = []; - this.activePane = 0; + this.cursorPosition = 0; + this.visualCursorStopPosition = [0, 0]; + this._internal_isDrawingFrame = false; + this.width = process.stdout.columns; + this.height = process.stdout.rows; + this.childView = new TerminalContainerView(); } cursorTo(x: number, y?: number) { - return new Promise((resolve) => process.stdout.cursorTo(x, y, () => resolve())); + this.cursorPosition = x + (y !== undefined ? y * this.width : 0); } - write(text: string) { - return new Promise((resolve) => process.stdout.write(text, () => resolve())); + write(text: string, effect?: TerminalEffect) { + const strArray = text.split(""); + + for (let idx = 0; idx < strArray.length; idx++) { + this.contentBuffer[this.cursorPosition] = [strArray[idx], effect]; + this.cursorPosition++; + } } clearScreenDown() { + const originalCursorPosition = this.cursorPosition; + + while (true) { + if (!this.contentBuffer[this.cursorPosition]) break; + + this.contentBuffer[this.cursorPosition] = [" "]; + this.cursorPosition++; + } + + this.cursorPosition = originalCursorPosition; + } + + _rawCursorTo(x: number, y?: number) { + return new Promise((resolve) => process.stdout.cursorTo(x, y, () => resolve())); + } + + _rawWrite(text: string) { + const encoder = new TextEncoder(); + return Deno.stdout.write(encoder.encode(text)); + } + + _rawClearScreenDown() { return new Promise((resolve) => process.stdout.clearScreenDown(() => resolve())); } - async draw() { - let idx = 0; - for (const pane of this.visiblePanes) { - pane.calculateSize({ width: this.parentTerminal.width, height: this.parentTerminal.height }); - idx += 50; - await pane.draw({ x: idx, y: 0 }); - } + async onResize() { + this.width = process.stdout.columns; + this.height = process.stdout.rows; - function cursorTo(x: number, y?: number) { - return new Promise((resolve) => process.stdout.cursorTo(x, y, () => resolve())); - } + await this._rawWrite(`\u001b[?47h\u001b[?7l\u001b[3J`); + + this.contentBuffer = []; + this.previousDrawBuffer = []; - function write(text: string) { - return new Promise((resolve) => process.stdout.write(text, () => resolve())); + const entries = this.width * this.height; + + for (let i = 0; i < entries; i++) { + this.contentBuffer.push([" "]); } + } + + async draw() { + if (this._internal_isDrawingFrame) return; + this._internal_isDrawingFrame = true; + + this.childView.calculateSize({ width: this.width, height: this.height }, { width: this.width, height: this.height }); + await this.childView.draw(this, { x: 0, y: 0 }); + + let remBuf = this.contentBuffer; + + let i = 0; + + while (remBuf.length !== 0) { + const curBuf = remBuf.slice(0, this.width); + remBuf = remBuf.slice(this.width); + + // let lastChar: TerminalScreen["contentBuffer"][0] | undefined; + // for (const char of curBuf) { + // if (char[1] !== lastChar?.[1]) { + // await this._rawWrite(await char[1]!._internal_apply()); + // } + + // await this._rawWrite(char[0]); + + // lastChar = char; + // } + + const drawBuffer: [string, string?][] = []; + + for (const char of curBuf) { + if (drawBuffer[drawBuffer.length - 1]) { + if (drawBuffer[drawBuffer.length - 1]?.[1] === char?.[1]) { + drawBuffer[drawBuffer.length - 1][0] += char[0]; + } else { + drawBuffer.push([char?.[0], await char[1]?._internal_apply()]); + } + } else { + drawBuffer.push([char?.[0], await char[1]?._internal_apply()]); + } + } + + if ( + !this.previousDrawBuffer[i]?.every((element, idx) => { + const target = drawBuffer[idx]; + if (!element || !target) return false; + + return element[0] === target[0] && element[1] === target[1]; + }) + ) { + let toDraw = ""; + + for (const group of drawBuffer) { + if (group[1]) { + toDraw += `${group[1] + group[0]}\u001b[0m`; + } else { + toDraw += group[0]; + } + } + + await this._rawWrite(`\u001b[${i + 1};1f${toDraw}`); + } + + if (this.previousDrawBuffer[i]) { + this.previousDrawBuffer[i] = drawBuffer; + } else { + this.previousDrawBuffer.push(drawBuffer); + } - function clearScreenDown() { - return new Promise((resolve) => process.stdout.clearScreenDown(() => resolve())); + i++; } - await + await this._rawCursorTo(this.visualCursorStopPosition[0], this.visualCursorStopPosition[1]); + this._internal_isDrawingFrame = false; } } diff --git a/instance/src/systems/terminalUI/sizableElement.ts b/instance/src/systems/terminalUI/sizableElement.ts index b25e549..7e02622 100644 --- a/instance/src/systems/terminalUI/sizableElement.ts +++ b/instance/src/systems/terminalUI/sizableElement.ts @@ -1,7 +1,7 @@ export default abstract class TerminalSizableElement { dimensions: { - width: { value: number; unit: "%" | "px" }; - height: { value: number; unit: "%" | "px" }; + width: { value: number; unit: "%" | "rem%" | "px" }; + height: { value: number; unit: "%" | "rem%" | "px" }; minWidth?: { value: number; unit: "%" | "px" }; minHeight?: { value: number; unit: "%" | "px" }; maxWidth?: { value: number; unit: "%" | "px" }; @@ -65,8 +65,8 @@ export default abstract class TerminalSizableElement { width: number; height: number; } = { - width: 0, - height: 0, + width: 1, + height: 1, }; absoluteDimensions: { width: number; @@ -76,7 +76,16 @@ export default abstract class TerminalSizableElement { height: 1, }; - calculateSize(parentContentAbsoluteDimensions: { width: number; height: number }) { + setDimensions(dimensions: TerminalSizableElement["dimensions"]) { + this.dimensions = dimensions; + + return this; + } + + calculateSize( + parentContentAbsoluteDimensions: { width: number; height: number }, + parentContentRemainingAbsoluteDimensions: { width: number; height: number }, + ) { this.absoluteDimensions.width = 16; this.absoluteDimensions.height = 8; this.contentAbsoluteDimensions.width = 16; @@ -84,8 +93,7 @@ export default abstract class TerminalSizableElement { this.contentAbsoluteOffset.width = 0; this.contentAbsoluteOffset.height = 0; - // TODO: some maths here to calc the actual size - + // Dimensions // px if (this.dimensions.width.unit === "px") { this.absoluteDimensions.width = this.dimensions.width.value; @@ -102,14 +110,22 @@ export default abstract class TerminalSizableElement { this.absoluteDimensions.height = Math.floor((this.dimensions.height.value / 100) * parentContentAbsoluteDimensions.height); } + if (this.dimensions.width.unit === "rem%") { + this.absoluteDimensions.width = Math.floor((this.dimensions.width.value / 100) * parentContentRemainingAbsoluteDimensions.width); + } + if (this.dimensions.height.unit === "rem%") { + this.absoluteDimensions.height = Math.floor((this.dimensions.height.value / 100) * parentContentRemainingAbsoluteDimensions.height); + } + if (this.absoluteDimensions.width > parentContentAbsoluteDimensions.width) { - this.absoluteDimensions.width = parentContentAbsoluteDimensions.width; + this.absoluteDimensions.width = parentContentRemainingAbsoluteDimensions.width; } if (this.absoluteDimensions.height > parentContentAbsoluteDimensions.height) { - this.absoluteDimensions.height = parentContentAbsoluteDimensions.height; + this.absoluteDimensions.height = parentContentRemainingAbsoluteDimensions.height; } + // Content Offset // px if (this.contentOffset.width.unit === "px") { this.contentAbsoluteOffset.width = this.contentOffset.width.value; @@ -125,5 +141,26 @@ export default abstract class TerminalSizableElement { if (this.contentOffset.height.unit === "%") { this.contentAbsoluteOffset.height = Math.floor((this.contentOffset.height.value / 100) * parentContentAbsoluteDimensions.height); } + + // Content Dimensions + // px + if (this.contentDimensions.width.unit === "px") { + this.contentAbsoluteDimensions.width = this.contentDimensions.width.value; + } + if (this.contentDimensions.height.unit === "px") { + this.contentAbsoluteDimensions.height = this.contentDimensions.height.value; + } + + // % + if (this.contentDimensions.width.unit === "%") { + this.contentAbsoluteDimensions.width = Math.floor((this.contentDimensions.width.value / 100) * this.absoluteDimensions.width); + } + if (this.contentDimensions.height.unit === "%") { + this.contentAbsoluteDimensions.height = Math.floor((this.contentDimensions.height.value / 100) * this.absoluteDimensions.height); + } + + // Shrink the absolute content dimensions by the offset doubled + this.contentAbsoluteDimensions.width -= this.contentAbsoluteOffset.width * 2; + this.contentAbsoluteDimensions.height -= this.contentAbsoluteOffset.height * 2; } } diff --git a/instance/src/systems/terminalUI/stdout.ts b/instance/src/systems/terminalUI/stdout.ts deleted file mode 100644 index a80ed53..0000000 --- a/instance/src/systems/terminalUI/stdout.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function cursorTo(x: number, y?: number) { - return new Promise((resolve) => process.stdout.cursorTo(x, y, () => resolve())); -} - -export function write(text: string) { - return new Promise((resolve) => process.stdout.write(text, () => resolve())); -} - -export function clearScreenDown() { - return new Promise((resolve) => process.stdout.clearScreenDown(() => resolve())); -} diff --git a/instance/src/systems/terminalUI/terminal.ts b/instance/src/systems/terminalUI/terminal.ts index 054088c..b3eae81 100644 --- a/instance/src/systems/terminalUI/terminal.ts +++ b/instance/src/systems/terminalUI/terminal.ts @@ -1,30 +1,29 @@ import TerminalScreen from "./screen.ts"; -import { write } from "./stdout.ts"; export default class Terminal { - width: number; - height: number; - activeScreen: TerminalScreen; + screen: TerminalScreen; constructor() { - const windowSize = process.stdout.getWindowSize() || [80, 20]; - this.width = windowSize[0]; - this.height = windowSize[1]; - this.activeScreen = new TerminalScreen(this); + this.screen = new TerminalScreen(this); + this.screen.onResize(); - process.stdout.on("resize", () => { - const windowSize = process.stdout.getWindowSize() || [80, 20]; - this.width = windowSize[0]; - this.height = windowSize[1]; + process.stdout.on("resize", async () => { + await this.screen.onResize(); + }); + + // process.stdin.setRawMode(true); + + globalThis.addEventListener("unload", () => { + this.end(); }); } async draw() { - await write("\x1b[?7l"); - await this.activeScreen.draw(); + await this.screen.draw(); } - async end() { - await write("\x1b[?7h"); + end() { + this.screen._rawWrite(`\x1b[?47l`); + // process.stdin.setRawMode(false); } } diff --git a/instance/src/systems/terminalUI/view.ts b/instance/src/systems/terminalUI/view.ts index 86db9a9..33769d8 100644 --- a/instance/src/systems/terminalUI/view.ts +++ b/instance/src/systems/terminalUI/view.ts @@ -1,3 +1,4 @@ +import type TerminalScreen from "./screen.ts"; import TerminalSizableElement from "./sizableElement.ts"; import { type TerminalEventListener, TerminalEventType } from "./terminalEvent.ts"; @@ -21,21 +22,74 @@ export default abstract class TerminalView extends TerminalSizableElement { ]; } - override calculateSize(parentContentAbsoluteDimensions: { width: number; height: number }): void { - super.calculateSize(parentContentAbsoluteDimensions); + override calculateSize( + parentContentAbsoluteDimensions: { width: number; height: number }, + parentContentRemainingAbsoluteDimensions: { width: number; height: number }, + ): void { + super.calculateSize(parentContentAbsoluteDimensions, parentContentRemainingAbsoluteDimensions); + + const remainingContentAbsoluteDimensions = { ...this.contentAbsoluteDimensions }; + + let totalChildrenPercentageWidth = 0; + let totalChildrenPercentageHeight = 0; for (const childView of this.childViews) { - childView.calculateSize(this.contentAbsoluteDimensions); + if (this.childViewFlowDirection === "x") + if (childView.dimensions.width.unit === "%") { + totalChildrenPercentageWidth += childView.dimensions.width.value; + } + + if (this.childViewFlowDirection === "y") + if (childView.dimensions.height.unit === "%") { + totalChildrenPercentageHeight += childView.dimensions.height.value; + } + } + + for (const childView of this.childViews) { + if (this.childViewFlowDirection === "x") + if (childView.dimensions.width.unit === "%") { + childView.dimensions.width.value = (childView.dimensions.width.value / totalChildrenPercentageWidth) * 100; + } + if (this.childViewFlowDirection === "y") + if (childView.dimensions.height.unit === "%") { + childView.dimensions.height.value = (childView.dimensions.height.value / totalChildrenPercentageHeight) * 100; + } + + childView.calculateSize(this.contentAbsoluteDimensions, remainingContentAbsoluteDimensions); + if (this.childViewFlowDirection === "x") remainingContentAbsoluteDimensions.width -= childView.absoluteDimensions.width; + if (this.childViewFlowDirection === "y") remainingContentAbsoluteDimensions.height -= childView.absoluteDimensions.height; } } - async draw(parentDrawOrigin: { x: number; y: number }) { - let previousViewOrigin = { x: parentDrawOrigin.x + this.contentAbsoluteOffset.width, y: parentDrawOrigin.y + this.contentAbsoluteOffset.height }; + addChild(view: TerminalView) { + this.childViews.push(view); + + return this; + } + + async draw(screen: TerminalScreen, parentDrawOrigin: { x: number; y: number }) { + const previousViewOrigin = { x: parentDrawOrigin.x, y: parentDrawOrigin.y }; + + const currentContentTotalChildSize: { width: number; height: number } = { width: 0, height: 0 }; for (const childView of this.childViews) { - await childView.draw(previousViewOrigin); + if (this.childViewFlowDirection === "x") currentContentTotalChildSize.width += childView.absoluteDimensions.width; + if (this.childViewFlowDirection === "y") currentContentTotalChildSize.height += childView.absoluteDimensions.height; + + if (currentContentTotalChildSize.width > this.contentAbsoluteDimensions.width) { + screen.write(`Warning! Content Width Exceeds Remaining Space, ${currentContentTotalChildSize.width} > ${this.contentAbsoluteDimensions.width}`); + break; + } + + if (currentContentTotalChildSize.height > this.contentAbsoluteDimensions.height) { + screen.write(` [Warning! Content Height Exceeds Remaining Space, ${currentContentTotalChildSize.height} > ${this.contentAbsoluteDimensions.height}] `); + break; + } + + await childView.draw(screen, previousViewOrigin); if (this.childViewFlowDirection === "x") previousViewOrigin.x += childView.absoluteDimensions.width; if (this.childViewFlowDirection === "y") previousViewOrigin.y += childView.absoluteDimensions.height; + // screen.write(JSON.stringify({ parentDrawOrigin, type: childView.constructor.name })); } } } diff --git a/instance/src/systems/terminalUI/views/borderView.ts b/instance/src/systems/terminalUI/views/borderView.ts index 36d7be1..1cf7fa8 100644 --- a/instance/src/systems/terminalUI/views/borderView.ts +++ b/instance/src/systems/terminalUI/views/borderView.ts @@ -1,39 +1,32 @@ -import { cursorTo, write } from "../stdout.ts"; +import TerminalEffect, { TerminalColor } from "../effect.ts"; +import type TerminalScreen from "../screen.ts"; import TerminalView from "../view.ts"; export default class TerminalBorderView extends TerminalView { constructor() { super(); - this.dimensions.width = { unit: "%", value: 50 }; - this.dimensions.height = { unit: "%", value: 100 }; - this.contentOffset.width = { unit: "px", value: 0 }; - this.contentOffset.height = { unit: "px", value: 0 }; + this.dimensions.width = { unit: "rem%", value: 100 }; + this.dimensions.height = { unit: "rem%", value: 100 }; + this.contentDimensions.width = { unit: "%", value: 100 }; + this.contentDimensions.height = { unit: "%", value: 100 }; + this.contentOffset.width = { unit: "px", value: 1 }; + this.contentOffset.height = { unit: "px", value: 1 }; } - override async draw(drawOrigin: { x: number; y: number }): Promise { - await cursorTo(drawOrigin.x, drawOrigin.y); - await write(`┌${"─".repeat(this.absoluteDimensions.width - 2)}┐`); + override async draw(screen: TerminalScreen, drawOrigin: { x: number; y: number }): Promise { + screen.cursorTo(drawOrigin.x, drawOrigin.y); + screen.write(`┌${"─".repeat(this.absoluteDimensions.width - 2)}┐`, new TerminalEffect().setColor(TerminalColor.BrightBlack)); for (let i = 1; i < this.absoluteDimensions.height - 1; i++) { - await cursorTo(drawOrigin.x, drawOrigin.y + i); - if (i === this.absoluteDimensions.height - 2) { - await cursorTo(drawOrigin.x, drawOrigin.y + this.absoluteDimensions.height - 2); - } else { - await write(`│${"#".repeat(this.absoluteDimensions.width - 2)}│`); - } + screen.cursorTo(drawOrigin.x, drawOrigin.y + i); + screen.write(`│${" ".repeat(this.absoluteDimensions.width - 2)}│`, new TerminalEffect().setColor(TerminalColor.BrightBlack)); } - await write(`└${"─".repeat(this.absoluteDimensions.width - 2)}┘`); + screen.cursorTo(drawOrigin.x, this.absoluteDimensions.height); + screen.write(`└${"─".repeat(this.absoluteDimensions.width - 2)}┘`, new TerminalEffect().setColor(TerminalColor.BrightBlack)); - await super.draw({ x: drawOrigin.x + this.contentAbsoluteOffset.width, y: drawOrigin.y + this.contentAbsoluteOffset.height }); - } - - override calculateSize(parentContentAbsoluteDimensions: { width: number; height: number }): void { - super.calculateSize(parentContentAbsoluteDimensions); + // screen.write(JSON.stringify({ x: drawOrigin.x + this.contentAbsoluteOffset.width, y: drawOrigin.y + this.contentAbsoluteOffset.height })); - this.contentAbsoluteDimensions.width = this.absoluteDimensions.width - 2; - this.contentAbsoluteDimensions.height = this.absoluteDimensions.width - 2; - this.contentAbsoluteOffset.width = 1; - this.contentAbsoluteOffset.height = 1; + await super.draw(screen, { x: drawOrigin.x + this.contentAbsoluteOffset.width, y: drawOrigin.y + this.contentAbsoluteOffset.height }); } } diff --git a/instance/src/systems/terminalUI/views/container.ts b/instance/src/systems/terminalUI/views/container.ts new file mode 100644 index 0000000..c564e34 --- /dev/null +++ b/instance/src/systems/terminalUI/views/container.ts @@ -0,0 +1,25 @@ +import type TerminalScreen from "../screen.ts"; +import TerminalView from "../view.ts"; + +export default class TerminalContainerView extends TerminalView { + constructor() { + super(); + + this.dimensions.width = { unit: "rem%", value: 100 }; + this.dimensions.height = { unit: "rem%", value: 100 }; + this.contentDimensions.width = { unit: "%", value: 100 }; + this.contentDimensions.height = { unit: "%", value: 100 }; + this.contentOffset.width = { unit: "px", value: 0 }; + this.contentOffset.height = { unit: "px", value: 0 }; + } + + setFlowDirection(flowDirection: "x" | "y") { + this.childViewFlowDirection = flowDirection; + + return this; + } + + override async draw(screen: TerminalScreen, drawOrigin: { x: number; y: number }): Promise { + await super.draw(screen, { x: drawOrigin.x + this.contentAbsoluteOffset.width, y: drawOrigin.y + this.contentAbsoluteOffset.height }); + } +} diff --git a/instance/src/systems/terminalUI/views/crossView.ts b/instance/src/systems/terminalUI/views/crossView.ts index 6f4a372..f83e44f 100644 --- a/instance/src/systems/terminalUI/views/crossView.ts +++ b/instance/src/systems/terminalUI/views/crossView.ts @@ -1,4 +1,5 @@ -import { cursorTo, write } from "../stdout.ts"; +import TerminalEffect, { TerminalColor } from "../effect.ts"; +import type TerminalScreen from "../screen.ts"; import TerminalView from "../view.ts"; export default class TerminalCrossView extends TerminalView { @@ -7,22 +8,18 @@ export default class TerminalCrossView extends TerminalView { this.dimensions.width = { unit: "%", value: 100 }; this.dimensions.height = { unit: "%", value: 100 }; + this.contentDimensions.width = { unit: "%", value: 100 }; + this.contentDimensions.height = { unit: "%", value: 100 }; this.contentOffset.width = { unit: "px", value: 0 }; this.contentOffset.height = { unit: "px", value: 0 }; } - override async draw(drawOrigin: { x: number; y: number }): Promise { - /* - ▲ - │ - │ - ◀─────16x5─────▶ - │ - │ - ▼ - */ - - await cursorTo(drawOrigin.x, drawOrigin.y); + override async draw(screen: TerminalScreen, drawOrigin: { x: number; y: number }): Promise { + if (this.absoluteDimensions.width < 31 || this.absoluteDimensions.height < 3) { + screen.cursorTo(drawOrigin.x, drawOrigin.y); + screen.write("Too Small to display CrossView"); + return; + } enum VerticalAlignment { Even, @@ -52,65 +49,65 @@ export default class TerminalCrossView extends TerminalView { let labelTextOffsetX = 0; let labelTextOffsetY = 0; + screen.cursorTo(drawOrigin.x, drawOrigin.y); + if (horizontalAlignment === HorizontalAlignment.Odd) { - for (let i = 0; i < this.absoluteDimensions.height; i++) { - await cursorTo(drawOrigin.x, drawOrigin.y + i); - await write(`${" ".repeat((this.absoluteDimensions.width - 1) / 2)}│${" ".repeat((this.absoluteDimensions.width - 1) / 2)}`); + screen.write(`${" ".repeat((this.absoluteDimensions.width - 1) / 2)}▲${" ".repeat((this.absoluteDimensions.width - 1) / 2)}`); + + for (let i = 1; i < this.absoluteDimensions.height - 1; i++) { + screen.cursorTo(drawOrigin.x, drawOrigin.y + i); + screen.write(`${" ".repeat((this.absoluteDimensions.width - 1) / 2)}│${" ".repeat((this.absoluteDimensions.width - 1) / 2)}`); labelTextOffsetX = drawOrigin.x + (this.absoluteDimensions.width - 1) / 2 - (labelText.length % 2 === 0 ? labelText.length / 2 : (labelText.length - 1) / 2); } + + screen.cursorTo(drawOrigin.x, drawOrigin.y + (this.absoluteDimensions.height - 1)); + screen.write(`${" ".repeat((this.absoluteDimensions.width - 1) / 2)}▼${" ".repeat((this.absoluteDimensions.width - 1) / 2)}`); } else { - await write(`${" ".repeat(this.absoluteDimensions.width / 2 - 1)}▲${" ".repeat(this.absoluteDimensions.width / 2)}`); + screen.write(`${" ".repeat(this.absoluteDimensions.width / 2 - 1)}▲${" ".repeat(this.absoluteDimensions.width / 2)}`); - for (let i = 1; i < this.absoluteDimensions.height; i++) { - await cursorTo(drawOrigin.x, drawOrigin.y + i); - await write(`${" ".repeat(this.absoluteDimensions.width / 2 - 1)}│${" ".repeat(this.absoluteDimensions.width / 2)}`); + for (let i = 1; i < this.absoluteDimensions.height - 1; i++) { + screen.cursorTo(drawOrigin.x, drawOrigin.y + i); + screen.write(`${" ".repeat(this.absoluteDimensions.width / 2 - 1)}│${" ".repeat(this.absoluteDimensions.width / 2)}`); labelTextOffsetX = drawOrigin.x + this.absoluteDimensions.width / 2 - 1 - (labelText.length % 2 === 0 ? labelText.length / 2 : (labelText.length - 1) / 2); } - await cursorTo(drawOrigin.x, drawOrigin.y + this.absoluteDimensions.height - 1); - await write(`${" ".repeat(this.absoluteDimensions.width / 2 - 1)}▼${" ".repeat(this.absoluteDimensions.width / 2)}`); + screen.cursorTo(drawOrigin.x, drawOrigin.y + this.absoluteDimensions.height - 1); + screen.write(`${" ".repeat(this.absoluteDimensions.width / 2 - 1)}▼${" ".repeat(this.absoluteDimensions.width / 2)}`); } if (verticalAlignment === VerticalAlignment.Odd) { - await cursorTo(drawOrigin.x, drawOrigin.y + (this.absoluteDimensions.height - 1) / 2); - await write(`${"─".repeat(this.absoluteDimensions.width)}`); + screen.cursorTo(drawOrigin.x, drawOrigin.y + (this.absoluteDimensions.height - 1) / 2); + screen.write(`◀${"─".repeat(this.absoluteDimensions.width - 2)}▶`); labelTextOffsetY = drawOrigin.y + (this.absoluteDimensions.height - 1) / 2; } else { - await cursorTo(drawOrigin.x, drawOrigin.y + this.absoluteDimensions.height / 2 - 1); - await write(`${"─".repeat(this.absoluteDimensions.width)}`); + screen.cursorTo(drawOrigin.x, drawOrigin.y + this.absoluteDimensions.height / 2 - 1); + screen.write(`◀${"─".repeat(this.absoluteDimensions.width - 2)}▶`); labelTextOffsetY = drawOrigin.y + this.absoluteDimensions.height / 2 - 1; } - await cursorTo(labelTextOffsetX, labelTextOffsetY); - await write(labelText); + screen.cursorTo(labelTextOffsetX, labelTextOffsetY); + screen.write(labelText.split("x")[0], new TerminalEffect().setColor(TerminalColor.BrightWhite).setBold()); + screen.write("x", new TerminalEffect().setColor(TerminalColor.White)); + screen.write(labelText.split("x")[1], new TerminalEffect().setColor(TerminalColor.BrightWhite).setBold()); - // await write(`${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))} ▲${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))}`); + // screen.write(`${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))} ▲${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))}`); // for (let i = 1; i < this.absoluteDimensions.height; i++) { - // await cursorTo(drawOrigin.x, drawOrigin.y + i); + // screen.cursorTo(drawOrigin.x, drawOrigin.y + i); // if (i === Math.floor(this.absoluteDimensions.height / 2) - 1) { // const dashLen = Math.floor((this.absoluteDimensions.width - dimensionText.length) / 2) - 2; - // await write(`◀${"─".repeat(dashLen)}${dimensionText}${"─".repeat(dashLen)}▶`); + // screen.write(`◀${"─".repeat(dashLen)}${dimensionText}${"─".repeat(dashLen)}▶`); // } else { - // await write(`${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))} │${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))}`); + // screen.write(`${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))} │${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))}`); // } // } - // await cursorTo(drawOrigin.x, drawOrigin.y + this.absoluteDimensions.height); - // await write(`${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))} ▼${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))}`); - - await super.draw({ x: drawOrigin.x + this.contentAbsoluteOffset.width, y: drawOrigin.y + this.contentAbsoluteOffset.height }); - } - - override calculateSize(parentContentAbsoluteDimensions: { width: number; height: number }): void { - super.calculateSize(parentContentAbsoluteDimensions); + // screen.cursorTo(drawOrigin.x, drawOrigin.y + this.absoluteDimensions.height); + // screen.write(`${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))} ▼${" ".repeat(Math.floor((this.absoluteDimensions.width - 1) / 2))}`); - this.contentAbsoluteDimensions.width = this.absoluteDimensions.width - 2; - this.contentAbsoluteDimensions.height = this.absoluteDimensions.width - 2; - this.contentAbsoluteOffset.width = 1; - this.contentAbsoluteOffset.height = 1; + await super.draw(screen, { x: drawOrigin.x + this.contentAbsoluteOffset.width, y: drawOrigin.y + this.contentAbsoluteOffset.height }); } } diff --git a/instance/src/systems/terminalUI/views/textInput.ts b/instance/src/systems/terminalUI/views/textInput.ts new file mode 100644 index 0000000..007355d --- /dev/null +++ b/instance/src/systems/terminalUI/views/textInput.ts @@ -0,0 +1,37 @@ +import TerminalEffect from "../effect.ts"; +import type TerminalScreen from "../screen.ts"; +import TerminalView from "../view.ts"; + +export default class TerminalTextInputView extends TerminalView { + value: string; + private placeholder: string | undefined; + + constructor() { + super(); + + this.dimensions.width = { unit: "%", value: 100 }; + this.dimensions.height = { unit: "px", value: 1 }; + this.contentDimensions.width = { unit: "%", value: 100 }; + this.contentDimensions.height = { unit: "%", value: 100 }; + this.contentOffset.width = { unit: "px", value: 0 }; + this.contentOffset.height = { unit: "px", value: 0 }; + + this.value = ""; + this.placeholder = undefined; + } + + setPlaceholder(placeholderText: string | undefined) { + this.placeholder = placeholderText; + + return this; + } + + override async draw(screen: TerminalScreen, drawOrigin: { x: number; y: number }): Promise { + screen.cursorTo(drawOrigin.x, drawOrigin.y); + screen.write(`${" ".repeat(this.absoluteDimensions.width)}`, new TerminalEffect().setUnderline()); + screen.cursorTo(drawOrigin.x, drawOrigin.y); + if (this.value === "" && this.placeholder !== undefined) screen.write(`${this.placeholder}`, new TerminalEffect().setUnderline()); + + await super.draw(screen, { x: drawOrigin.x + this.contentAbsoluteOffset.width, y: drawOrigin.y + this.contentAbsoluteOffset.height }); + } +} diff --git a/uikit-solid b/uikit-solid index 23f4386..9496b1c 160000 --- a/uikit-solid +++ b/uikit-solid @@ -1 +1 @@ -Subproject commit 23f4386375e2c998be5da6986471092de870c7fa +Subproject commit 9496b1c69fdbf2bfe086c9ab6067fa242b1c3b1c