finish basic tui for accesscontrol, add scroll for tui

This commit is contained in:
2025-10-12 20:23:08 +08:00
parent 1f85ef6aa2
commit bd8e1f9b8d
13 changed files with 1503 additions and 335 deletions

View File

@@ -25,6 +25,32 @@ export interface StyleProps {
textColor?: number;
/** Background color */
backgroundColor?: number;
/** Width - can be a number (fixed), "full" (100% of parent), or "screen" (terminal width) */
width?: number | "full" | "screen";
/** Height - can be a number (fixed), "full" (100% of parent), or "screen" (terminal height) */
height?: number | "full" | "screen";
}
/**
* Scroll properties for scroll containers
*/
export interface ScrollProps {
/** Current horizontal scroll position */
scrollX: number;
/** Current vertical scroll position */
scrollY: number;
/** Maximum horizontal scroll (content width - viewport width) */
maxScrollX: number;
/** Maximum vertical scroll (content height - viewport height) */
maxScrollY: number;
/** Content dimensions */
contentWidth: number;
contentHeight: number;
/** Whether to show scrollbars */
showScrollbar?: boolean;
/** Viewport dimensions (visible area) */
viewportWidth: number;
viewportHeight: number;
}
/**
@@ -48,18 +74,21 @@ export interface BaseProps {
/**
* UIObject node type
*/
export type UIObjectType =
| "div"
| "label"
| "button"
| "input"
export type UIObjectType =
| "div"
| "label"
| "button"
| "input"
| "form"
| "h1"
| "h2"
| "h3"
| "for"
| "show"
| "fragment";
| "switch"
| "match"
| "fragment"
| "scroll-container";
/**
* UIObject represents a node in the UI tree
@@ -68,44 +97,47 @@ export type UIObjectType =
export class UIObject {
/** Type of the UI object */
type: UIObjectType;
/** Props passed to the component */
props: Record<string, unknown>;
/** Children UI objects */
children: UIObject[];
/** Parent UI object */
parent?: UIObject;
/** Computed layout after flexbox calculation */
layout?: ComputedLayout;
/** Layout properties parsed from class string */
layoutProps: LayoutProps;
/** Style properties parsed from class string */
styleProps: StyleProps;
/** Whether this component is currently mounted */
mounted: boolean;
/** Cleanup functions to call when unmounting */
cleanupFns: (() => void)[];
/** For text nodes - the text content (can be reactive) */
textContent?: string | Accessor<string>;
/** Event handlers */
handlers: Record<string, ((...args: unknown[]) => void) | undefined>;
/** For input text components - cursor position */
cursorPos?: number;
/** For scroll containers - scroll state */
scrollProps?: ScrollProps;
constructor(
type: UIObjectType,
props: Record<string, unknown> = {},
children: UIObject[] = []
children: UIObject[] = [],
) {
this.type = type;
this.props = props;
@@ -115,45 +147,60 @@ export class UIObject {
this.mounted = false;
this.cleanupFns = [];
this.handlers = {};
// Parse layout and styles from class prop
this.parseClassNames();
// Extract event handlers
this.extractHandlers();
// Initialize cursor position for text inputs
if (type === "input" && props.type !== "checkbox") {
this.cursorPos = 0;
}
// Initialize scroll properties for scroll containers
if (type === "scroll-container") {
this.scrollProps = {
scrollX: 0,
scrollY: 0,
maxScrollX: 0,
maxScrollY: 0,
contentWidth: 0,
contentHeight: 0,
showScrollbar: props.showScrollbar !== false,
viewportWidth: (props.width as number) ?? 10,
viewportHeight: (props.height as number) ?? 10,
};
}
}
/**
* Map color name to ComputerCraft colors API value
*
*
* @param colorName - The color name from class (e.g., "white", "red")
* @returns The color value from colors API, or undefined if invalid
*/
private parseColor(colorName: string): number | undefined {
const colorMap: Record<string, number> = {
"white": colors.white,
"orange": colors.orange,
"magenta": colors.magenta,
"lightBlue": colors.lightBlue,
"yellow": colors.yellow,
"lime": colors.lime,
"pink": colors.pink,
"gray": colors.gray,
"lightGray": colors.lightGray,
"cyan": colors.cyan,
"purple": colors.purple,
"blue": colors.blue,
"brown": colors.brown,
"green": colors.green,
"red": colors.red,
"black": colors.black,
white: colors.white,
orange: colors.orange,
magenta: colors.magenta,
lightBlue: colors.lightBlue,
yellow: colors.yellow,
lime: colors.lime,
pink: colors.pink,
gray: colors.gray,
lightGray: colors.lightGray,
cyan: colors.cyan,
purple: colors.purple,
blue: colors.blue,
brown: colors.brown,
green: colors.green,
red: colors.red,
black: colors.black,
};
return colorMap[colorName];
}
@@ -164,8 +211,8 @@ export class UIObject {
const className = this.props.class as string | undefined;
if (className === undefined) return;
const classes = className.split(" ").filter(c => c.length > 0);
const classes = className.split(" ").filter((c) => c.length > 0);
for (const cls of classes) {
// Flex direction
if (cls === "flex-row") {
@@ -173,7 +220,7 @@ export class UIObject {
} else if (cls === "flex-col") {
this.layoutProps.flexDirection = "column";
}
// Justify content
else if (cls === "justify-start") {
this.layoutProps.justifyContent = "start";
@@ -184,7 +231,7 @@ export class UIObject {
} else if (cls === "justify-between") {
this.layoutProps.justifyContent = "between";
}
// Align items
else if (cls === "items-start") {
this.layoutProps.alignItems = "start";
@@ -193,7 +240,7 @@ export class UIObject {
} else if (cls === "items-end") {
this.layoutProps.alignItems = "end";
}
// Text color (text-<color>)
else if (cls.startsWith("text-")) {
const colorName = cls.substring(5); // Remove "text-" prefix
@@ -202,7 +249,7 @@ export class UIObject {
this.styleProps.textColor = color;
}
}
// Background color (bg-<color>)
else if (cls.startsWith("bg-")) {
const colorName = cls.substring(3); // Remove "bg-" prefix
@@ -211,8 +258,38 @@ export class UIObject {
this.styleProps.backgroundColor = color;
}
}
// Width sizing (w-<size>)
else if (cls.startsWith("w-")) {
const sizeValue = cls.substring(2); // Remove "w-" prefix
if (sizeValue === "full") {
this.styleProps.width = "full";
} else if (sizeValue === "screen") {
this.styleProps.width = "screen";
} else {
const numValue = tonumber(sizeValue);
if (numValue !== undefined) {
this.styleProps.width = numValue;
}
}
}
// Height sizing (h-<size>)
else if (cls.startsWith("h-")) {
const sizeValue = cls.substring(2); // Remove "h-" prefix
if (sizeValue === "full") {
this.styleProps.height = "full";
} else if (sizeValue === "screen") {
this.styleProps.height = "screen";
} else {
const numValue = tonumber(sizeValue);
if (numValue !== undefined) {
this.styleProps.height = numValue;
}
}
}
}
// Set defaults
if (this.type === "div") {
this.layoutProps.flexDirection ??= "row";
@@ -226,7 +303,11 @@ export class UIObject {
*/
private extractHandlers(): void {
for (const [key, value] of pairs(this.props)) {
if (typeof key === "string" && key.startsWith("on") && typeof value === "function") {
if (
typeof key === "string" &&
key.startsWith("on") &&
typeof value === "function"
) {
this.handlers[key] = value as (...args: unknown[]) => void;
}
}
@@ -257,7 +338,7 @@ export class UIObject {
mount(): void {
if (this.mounted) return;
this.mounted = true;
// Mount all children
for (const child of this.children) {
child.mount();
@@ -270,12 +351,12 @@ export class UIObject {
unmount(): void {
if (!this.mounted) return;
this.mounted = false;
// Unmount all children first
for (const child of this.children) {
child.unmount();
}
// Run cleanup functions
for (const cleanup of this.cleanupFns) {
try {
@@ -293,6 +374,75 @@ export class UIObject {
onCleanup(fn: () => void): void {
this.cleanupFns.push(fn);
}
/**
* Scroll the container by the given amount
* @param deltaX - Horizontal scroll delta
* @param deltaY - Vertical scroll delta
*/
scrollBy(deltaX: number, deltaY: number): void {
if (this.type !== "scroll-container" || !this.scrollProps) return;
const newScrollX = Math.max(
0,
Math.min(this.scrollProps.maxScrollX, this.scrollProps.scrollX + deltaX),
);
const newScrollY = Math.max(
0,
Math.min(this.scrollProps.maxScrollY, this.scrollProps.scrollY + deltaY),
);
this.scrollProps.scrollX = newScrollX;
this.scrollProps.scrollY = newScrollY;
}
/**
* Scroll to a specific position
* @param x - Horizontal scroll position
* @param y - Vertical scroll position
*/
scrollTo(x: number, y: number): void {
if (this.type !== "scroll-container" || !this.scrollProps) return;
this.scrollProps.scrollX = Math.max(
0,
Math.min(this.scrollProps.maxScrollX, x),
);
this.scrollProps.scrollY = Math.max(
0,
Math.min(this.scrollProps.maxScrollY, y),
);
}
/**
* Update scroll bounds based on content size
* @param contentWidth - Total content width
* @param contentHeight - Total content height
*/
updateScrollBounds(contentWidth: number, contentHeight: number): void {
if (this.type !== "scroll-container" || !this.scrollProps) return;
this.scrollProps.contentWidth = contentWidth;
this.scrollProps.contentHeight = contentHeight;
this.scrollProps.maxScrollX = Math.max(
0,
contentWidth - this.scrollProps.viewportWidth,
);
this.scrollProps.maxScrollY = Math.max(
0,
contentHeight - this.scrollProps.viewportHeight,
);
// Clamp current scroll position to new bounds
this.scrollProps.scrollX = Math.max(
0,
Math.min(this.scrollProps.maxScrollX, this.scrollProps.scrollX),
);
this.scrollProps.scrollY = Math.max(
0,
Math.min(this.scrollProps.maxScrollY, this.scrollProps.scrollY),
);
}
}
/**