mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-04 19:27:50 +08:00
move package and add tab component for tui famework
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@ node_modules
|
||||
event/
|
||||
build/
|
||||
reference/
|
||||
src/*/*.md
|
||||
|
||||
QWEN.md
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||
sync-path := if os_family() == "windows" {
|
||||
"C:\\Users\\sikongjueluo\\AppData\\Roaming\\CraftOS-PC\\computer\\0\\user\\"
|
||||
} else {
|
||||
"/home/sikongjueluo/.local/share/craftos-pc/computer/0/user/"
|
||||
}
|
||||
|
||||
build: build-autocraft build-accesscontrol build-test build-example sync
|
||||
|
||||
@@ -16,4 +21,4 @@ build-example:
|
||||
pnpm tstl -p ./tsconfig.tuiExample.json
|
||||
|
||||
sync:
|
||||
cp -r "./build/*" "C:\\Users\\sikongjueluo\\AppData\\Roaming\\CraftOS-PC\\computer\\0\\user\\"
|
||||
rsync --delete -r "./build/" "{{sync-path}}"
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
{
|
||||
packages = with pkgs; [
|
||||
pnpm
|
||||
craftos-pc
|
||||
qwen-code
|
||||
];
|
||||
|
||||
# https://devenv.sh/languages/
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"@jackmacwindows/lua-types": "^2.13.2",
|
||||
"@jackmacwindows/typescript-to-lua": "^1.28.1",
|
||||
"@sikongjueluo/advanced-peripherals-types": "file:types/advanced-peripherals",
|
||||
"@sikongjueluo/toml2lua-types": "file:types/toml2lua",
|
||||
"@sikongjueluo/dkjson-types": "file:types/dkjson",
|
||||
"@sikongjueluo/dkjson-types": "file:thirdparty/dkjson",
|
||||
"@sikongjueluo/toml2lua-types": "file:thirdparty/toml2lua",
|
||||
"@typescript-to-lua/language-extensions": "^1.19.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.7.2",
|
||||
|
||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -27,11 +27,11 @@ importers:
|
||||
specifier: file:types/advanced-peripherals
|
||||
version: file:types/advanced-peripherals
|
||||
'@sikongjueluo/dkjson-types':
|
||||
specifier: file:types/dkjson
|
||||
version: file:types/dkjson
|
||||
specifier: file:thirdparty/dkjson
|
||||
version: file:thirdparty/dkjson
|
||||
'@sikongjueluo/toml2lua-types':
|
||||
specifier: file:types/toml2lua
|
||||
version: file:types/toml2lua
|
||||
specifier: file:thirdparty/toml2lua
|
||||
version: file:thirdparty/toml2lua
|
||||
'@typescript-to-lua/language-extensions':
|
||||
specifier: ^1.19.0
|
||||
version: 1.19.0
|
||||
@@ -132,11 +132,11 @@ packages:
|
||||
'@sikongjueluo/advanced-peripherals-types@file:types/advanced-peripherals':
|
||||
resolution: {directory: types/advanced-peripherals, type: directory}
|
||||
|
||||
'@sikongjueluo/dkjson-types@file:types/dkjson':
|
||||
resolution: {directory: types/dkjson, type: directory}
|
||||
'@sikongjueluo/dkjson-types@file:thirdparty/dkjson':
|
||||
resolution: {directory: thirdparty/dkjson, type: directory}
|
||||
|
||||
'@sikongjueluo/toml2lua-types@file:types/toml2lua':
|
||||
resolution: {directory: types/toml2lua, type: directory}
|
||||
'@sikongjueluo/toml2lua-types@file:thirdparty/toml2lua':
|
||||
resolution: {directory: thirdparty/toml2lua, type: directory}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
@@ -689,9 +689,9 @@ snapshots:
|
||||
|
||||
'@sikongjueluo/advanced-peripherals-types@file:types/advanced-peripherals': {}
|
||||
|
||||
'@sikongjueluo/dkjson-types@file:types/dkjson': {}
|
||||
'@sikongjueluo/dkjson-types@file:thirdparty/dkjson': {}
|
||||
|
||||
'@sikongjueluo/toml2lua-types@file:types/toml2lua': {}
|
||||
'@sikongjueluo/toml2lua-types@file:thirdparty/toml2lua': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
|
||||
538
src/lib/ccTUI.ts
538
src/lib/ccTUI.ts
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
// Import required types from the ComputerCraft environment
|
||||
import { CharEvent, KeyEvent, pullEventAs } from "./event";
|
||||
import { CharEvent, KeyEvent, pullEventAs, TimerEvent } from "./event";
|
||||
import { CCLog, DAY } from "./ccLog";
|
||||
|
||||
/**
|
||||
@@ -38,24 +38,34 @@ class Signal<T = void> {
|
||||
}
|
||||
|
||||
abstract class UIObject {
|
||||
private objectName: string;
|
||||
readonly objectName: string;
|
||||
private parent?: UIObject;
|
||||
private children: Record<string, UIObject> = {};
|
||||
private log?: CCLog;
|
||||
|
||||
constructor(name: string) {
|
||||
public log?: CCLog;
|
||||
|
||||
constructor(name: string, parent?: UIObject, log?: CCLog) {
|
||||
this.objectName = name;
|
||||
this.parent = parent;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public setParent(parent: UIObject) {
|
||||
this.parent = parent;
|
||||
this.log ??= parent.log;
|
||||
}
|
||||
|
||||
public addChild(child: UIObject) {
|
||||
this.children[child.objectName] = child;
|
||||
}
|
||||
|
||||
public removeChild(child: UIObject) {}
|
||||
public removeChild(child: UIObject) {
|
||||
Object.entries(this.children).forEach(([key, value]) => {
|
||||
if (value === child) {
|
||||
delete this.children[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +85,8 @@ abstract class UIComponent extends UIObject {
|
||||
public onKeyPress = new Signal<KeyEvent>();
|
||||
public onMouseClick = new Signal<{ x: number; y: number }>();
|
||||
|
||||
constructor(x: number, y: number, width = 0, height = 0) {
|
||||
constructor(objectName: string, x: number, y: number, width = 0, height = 0) {
|
||||
super(objectName);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
@@ -85,11 +96,20 @@ abstract class UIComponent extends UIObject {
|
||||
// Render the component to the terminal
|
||||
abstract render(): void;
|
||||
|
||||
// Handle input events
|
||||
// Handle events
|
||||
// Key
|
||||
abstract handleKeyInput(event: KeyEvent): void;
|
||||
handleKeyInput(_event: KeyEvent): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// Char
|
||||
abstract handleCharInput(event: CharEvent): void;
|
||||
handleCharInput(_event: CharEvent): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
handleTimerTrigger(_event: TimerEvent): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// Get/set focus for the component
|
||||
focus(): void {
|
||||
@@ -151,13 +171,14 @@ class TextLabel extends UIComponent {
|
||||
private bgColor: number;
|
||||
|
||||
constructor(
|
||||
objectName: string,
|
||||
x: number,
|
||||
y: number,
|
||||
text: string,
|
||||
textColor: number = colors.white,
|
||||
bgColor: number = colors.black,
|
||||
) {
|
||||
super(x, y, text.length, 1);
|
||||
super(objectName, x, y, text.length, 1);
|
||||
this.text = text;
|
||||
this.textColor = textColor;
|
||||
this.bgColor = bgColor;
|
||||
@@ -180,14 +201,6 @@ class TextLabel extends UIComponent {
|
||||
term.setCursorPos(originalX, originalY);
|
||||
}
|
||||
|
||||
handleKeyInput(_event: KeyEvent): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
handleCharInput(_event: CharEvent): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
setText(newText: string): void {
|
||||
this.text = newText;
|
||||
this.width = newText.length; // Update width based on new text
|
||||
@@ -213,6 +226,7 @@ class InputField extends UIComponent {
|
||||
public onTextChanged = new Signal<string>();
|
||||
|
||||
constructor(
|
||||
objectName: string,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
@@ -221,7 +235,7 @@ class InputField extends UIComponent {
|
||||
maxLength = 50,
|
||||
password = false,
|
||||
) {
|
||||
super(x, y, width, 1);
|
||||
super(objectName, x, y, width, 1);
|
||||
this.value = value;
|
||||
this.placeholder = placeholder;
|
||||
this.maxLength = maxLength;
|
||||
@@ -286,7 +300,7 @@ class InputField extends UIComponent {
|
||||
this.onKeyPress.emit(event);
|
||||
|
||||
const key = event.key;
|
||||
log.debug(`[${InputField.name}]: Get key ${keys.getName(key)}`);
|
||||
this.log?.debug(`[${InputField.name}]: Get key ${keys.getName(key)}`);
|
||||
|
||||
// Handle backspace
|
||||
if (key === keys.backspace) {
|
||||
@@ -334,7 +348,7 @@ class InputField extends UIComponent {
|
||||
if (!this.focused) return;
|
||||
|
||||
const character = event.character;
|
||||
log.debug(`[${InputField.name}]: Get character ${character}`);
|
||||
this.log?.debug(`[${InputField.name}]: Get character ${character}`);
|
||||
|
||||
this.value += character;
|
||||
this.cursorPos++;
|
||||
@@ -385,13 +399,14 @@ class OptionSelector extends UIComponent {
|
||||
public onSelectionChanged = new Signal<{ index: number; value: string }>();
|
||||
|
||||
constructor(
|
||||
objectName: string,
|
||||
x: number,
|
||||
y: number,
|
||||
options: string[],
|
||||
prompt = "Select:",
|
||||
initialIndex = 0,
|
||||
) {
|
||||
super(x, y, 0, 1); // Width will be calculated dynamically
|
||||
super(objectName, x, y, 0, 1); // Width will be calculated dynamically
|
||||
this.options = options;
|
||||
this.currentIndex = initialIndex;
|
||||
this.prompt = prompt;
|
||||
@@ -454,10 +469,6 @@ class OptionSelector extends UIComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleCharInput(_event: CharEvent): void {
|
||||
//Do nothing
|
||||
}
|
||||
|
||||
private previousOption(): void {
|
||||
this.currentIndex =
|
||||
(this.currentIndex - 1 + this.options.length) % this.options.length;
|
||||
@@ -509,6 +520,430 @@ class OptionSelector extends UIComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab component that allows switching between different pages
|
||||
* Similar to QT's TabWidget, currently implementing horizontal tabs only
|
||||
*/
|
||||
class TabWidget extends UIComponent {
|
||||
// Tab data structure - simple array of tab names
|
||||
private tabs: string[];
|
||||
private currentIndex: number;
|
||||
// Tracks visible range of tabs to handle overflow scenarios
|
||||
private firstVisibleIndex: number;
|
||||
private lastVisibleIndex: number;
|
||||
|
||||
// Signal emitted when the current tab changes
|
||||
public onTabChanged = new Signal<{ index: number; name: string }>();
|
||||
|
||||
/**
|
||||
* Creates a new TabWidget component
|
||||
* @param objectName Unique name for the component
|
||||
* @param x X position on the terminal
|
||||
* @param y Y position on the terminal
|
||||
* @param width Width of the tab widget
|
||||
* @param tabNames Initial list of tab names
|
||||
* @param initialIndex Index of the initially selected tab (default: 0)
|
||||
*/
|
||||
constructor(
|
||||
objectName: string,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
tabNames: string[],
|
||||
initialIndex = 0,
|
||||
) {
|
||||
super(objectName, x, y, width, 1);
|
||||
|
||||
// Initialize tabs as simple string array
|
||||
this.tabs = [...tabNames];
|
||||
this.currentIndex = Math.max(
|
||||
0,
|
||||
Math.min(initialIndex, tabNames.length - 1),
|
||||
);
|
||||
this.firstVisibleIndex = 0;
|
||||
this.lastVisibleIndex = -1;
|
||||
|
||||
// Calculate which tabs can be displayed based on available width
|
||||
this.updateVisibleRange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the range of visible tabs based on available width
|
||||
* This method ensures the current tab is always visible and calculates
|
||||
* which other tabs can fit in the available space
|
||||
*/
|
||||
private updateVisibleRange(): void {
|
||||
// If no tabs exist, nothing to update
|
||||
if (this.tabs.length === 0) {
|
||||
this.firstVisibleIndex = 0;
|
||||
this.lastVisibleIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate visible tabs range based on current position
|
||||
this.calculateVisibleTabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates visible tabs based on current position and available width
|
||||
* Follows the new core rendering logic
|
||||
*/
|
||||
private calculateVisibleTabs(): void {
|
||||
if (this.tabs.length === 0) {
|
||||
this.firstVisibleIndex = 0;
|
||||
this.lastVisibleIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with all tabs and build the complete string
|
||||
let fullString = "| ";
|
||||
for (let i = 0; i < this.tabs.length; i++) {
|
||||
if (i > 0) {
|
||||
fullString += " | ";
|
||||
}
|
||||
fullString += this.tabs[i];
|
||||
}
|
||||
fullString += " |";
|
||||
|
||||
// If the full string fits, show all tabs
|
||||
if (fullString.length <= this.width) {
|
||||
this.firstVisibleIndex = 0;
|
||||
this.lastVisibleIndex = this.tabs.length - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the range that can fit around the current tab
|
||||
this.firstVisibleIndex = this.currentIndex;
|
||||
this.lastVisibleIndex = this.currentIndex;
|
||||
|
||||
// Try to expand left and right alternately
|
||||
while (
|
||||
this.firstVisibleIndex > 0 ||
|
||||
this.lastVisibleIndex < this.tabs.length - 1
|
||||
) {
|
||||
let expanded = false;
|
||||
|
||||
// Try expanding left first
|
||||
if (this.firstVisibleIndex > 0) {
|
||||
const newTestString =
|
||||
"| " +
|
||||
this.tabs[this.firstVisibleIndex - 1] +
|
||||
" | " +
|
||||
this.tabs
|
||||
.slice(this.firstVisibleIndex, this.lastVisibleIndex + 1)
|
||||
.join(" | ") +
|
||||
" |";
|
||||
|
||||
if (newTestString.length <= this.width) {
|
||||
this.firstVisibleIndex--;
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Try expanding right
|
||||
if (this.lastVisibleIndex < this.tabs.length - 1) {
|
||||
const newTestString =
|
||||
"| " +
|
||||
this.tabs
|
||||
.slice(this.firstVisibleIndex, this.lastVisibleIndex + 1)
|
||||
.join(" | ") +
|
||||
" | " +
|
||||
this.tabs[this.lastVisibleIndex + 1] +
|
||||
" |";
|
||||
|
||||
if (newTestString.length <= this.width) {
|
||||
this.lastVisibleIndex++;
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If no expansion was possible, break
|
||||
if (!expanded) break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the tab widget to the terminal
|
||||
* Follows the new core rendering logic:
|
||||
* 1. Build complete string with all visible tabs
|
||||
* 2. Calculate what can be displayed
|
||||
* 3. Replace indicators based on hidden tabs
|
||||
* 4. Determine highlight range and render
|
||||
*/
|
||||
render(): void {
|
||||
if (!this.visible) return;
|
||||
|
||||
const [originalX, originalY] = term.getCursorPos();
|
||||
|
||||
// Move cursor to the position of the tab widget
|
||||
term.setCursorPos(this.x, this.y);
|
||||
|
||||
if (this.tabs.length === 0) {
|
||||
// Fill with spaces if no tabs
|
||||
term.setTextColor(colors.white);
|
||||
term.setBackgroundColor(colors.black);
|
||||
term.write(" ".repeat(this.width));
|
||||
term.setCursorPos(originalX, originalY);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 1: Build complete string for visible tabs with "| " at start and " |" at end
|
||||
let displayString = "| ";
|
||||
for (let i = this.firstVisibleIndex; i <= this.lastVisibleIndex; i++) {
|
||||
if (i > this.firstVisibleIndex) {
|
||||
displayString += " | ";
|
||||
}
|
||||
displayString += this.tabs[i];
|
||||
}
|
||||
displayString += " |";
|
||||
|
||||
// Step 2: Check if the string fits, if not, truncate with "..."
|
||||
if (displayString.length > this.width) {
|
||||
// Need to truncate - find where to cut and add "..."
|
||||
const maxLength = this.width - 3; // Reserve space for "..."
|
||||
if (maxLength > 0) {
|
||||
// Find the last complete tab that can fit
|
||||
let cutPosition = maxLength;
|
||||
// Try to cut at a tab boundary if possible
|
||||
let lastPipePos = -1;
|
||||
for (let i = cutPosition; i >= 0; i--) {
|
||||
if (displayString.substring(i, i + 3) === " | ") {
|
||||
lastPipePos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastPipePos > 2) {
|
||||
// Make sure we don't cut before the first tab
|
||||
cutPosition = lastPipePos;
|
||||
}
|
||||
displayString = displayString.substring(0, cutPosition) + "...";
|
||||
} else {
|
||||
displayString = "...";
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Replace boundary indicators based on hidden tabs
|
||||
if (this.firstVisibleIndex > 0) {
|
||||
// Left side has hidden tabs - replace "| " with "< "
|
||||
displayString = "< " + displayString.substring(2);
|
||||
}
|
||||
|
||||
if (this.lastVisibleIndex < this.tabs.length - 1) {
|
||||
// Right side has hidden tabs - replace " |" with " >"
|
||||
if (displayString.endsWith(" |")) {
|
||||
displayString =
|
||||
displayString.substring(0, displayString.length - 2) + " >";
|
||||
} else if (displayString.endsWith("...")) {
|
||||
// If we have "...", just ensure we show ">"
|
||||
displayString =
|
||||
displayString.substring(0, displayString.length - 3) + " >";
|
||||
}
|
||||
}
|
||||
|
||||
// Pad to maintain consistent width
|
||||
while (displayString.length < this.width) {
|
||||
displayString += " ";
|
||||
}
|
||||
|
||||
// Ensure we don't exceed the width
|
||||
if (displayString.length > this.width) {
|
||||
displayString = displayString.substring(0, this.width);
|
||||
}
|
||||
|
||||
// Step 4: Find current tab position for highlighting
|
||||
let currentTabStart = -1;
|
||||
let currentTabEnd = -1;
|
||||
|
||||
if (
|
||||
this.currentIndex >= this.firstVisibleIndex &&
|
||||
this.currentIndex <= this.lastVisibleIndex
|
||||
) {
|
||||
// Calculate position of current tab in display string
|
||||
let searchPos = 2; // Start after "| " or "< "
|
||||
|
||||
// Find current tab position by iterating through visible tabs
|
||||
for (let i = this.firstVisibleIndex; i <= this.lastVisibleIndex; i++) {
|
||||
if (i > this.firstVisibleIndex) {
|
||||
searchPos += 3; // " | " separator
|
||||
}
|
||||
|
||||
if (i === this.currentIndex) {
|
||||
currentTabStart = searchPos;
|
||||
|
||||
// Find the end of the current tab
|
||||
const tabName = this.tabs[i];
|
||||
const remainingString = displayString.substring(searchPos);
|
||||
|
||||
// Check if the tab is fully displayed or truncated
|
||||
if (remainingString.startsWith(tabName)) {
|
||||
// Tab is fully displayed
|
||||
currentTabEnd = searchPos + tabName.length;
|
||||
} else {
|
||||
// Tab might be truncated, find where it ends
|
||||
const nextSeparatorPos = remainingString.indexOf(" |");
|
||||
const nextIndicatorPos = remainingString.indexOf(" >");
|
||||
const ellipsisPos = remainingString.indexOf("...");
|
||||
|
||||
let endPos = remainingString.length;
|
||||
if (nextSeparatorPos >= 0)
|
||||
endPos = Math.min(endPos, nextSeparatorPos);
|
||||
if (nextIndicatorPos >= 0)
|
||||
endPos = Math.min(endPos, nextIndicatorPos);
|
||||
if (ellipsisPos >= 0) endPos = Math.min(endPos, ellipsisPos);
|
||||
|
||||
currentTabEnd = searchPos + endPos;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
searchPos += this.tabs[i].length;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Render with highlighting
|
||||
term.setTextColor(colors.white);
|
||||
term.setBackgroundColor(colors.black);
|
||||
|
||||
if (currentTabStart >= 0 && currentTabEnd > currentTabStart) {
|
||||
// Render text before current tab
|
||||
if (currentTabStart > 0) {
|
||||
term.write(displayString.substring(0, currentTabStart));
|
||||
}
|
||||
|
||||
// Render current tab with highlighting
|
||||
term.setTextColor(colors.yellow);
|
||||
term.setBackgroundColor(colors.gray);
|
||||
term.write(displayString.substring(currentTabStart, currentTabEnd));
|
||||
|
||||
// Reset colors and render remaining text
|
||||
term.setTextColor(colors.white);
|
||||
term.setBackgroundColor(colors.black);
|
||||
term.write(displayString.substring(currentTabEnd));
|
||||
} else {
|
||||
// No highlighting needed, render entire string
|
||||
term.write(displayString);
|
||||
}
|
||||
|
||||
// Restore original cursor position
|
||||
term.setCursorPos(originalX, originalY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles key input events for the tab widget
|
||||
* Supports left/right arrow keys to switch between tabs
|
||||
* @param event The key event to handle
|
||||
*/
|
||||
handleKeyInput(event: KeyEvent): void {
|
||||
if (!this.focused) return;
|
||||
|
||||
this.onKeyPress.emit(event);
|
||||
|
||||
const key = event.key;
|
||||
|
||||
// Handle left arrow to move to previous visible tab
|
||||
if (key === keys.left && this.canMoveToPreviousTab()) {
|
||||
this.moveToPreviousTab();
|
||||
}
|
||||
// Handle right arrow to move to next visible tab
|
||||
else if (key === keys.right && this.canMoveToNextTab()) {
|
||||
this.moveToNextTab();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is a previous tab available to move to
|
||||
* @returns True if there's a previous tab, false otherwise
|
||||
*/
|
||||
private canMoveToPreviousTab(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to the previous tab
|
||||
*/
|
||||
private moveToPreviousTab(): void {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
this.updateVisibleRange();
|
||||
this.onTabChanged.emit({
|
||||
index: this.currentIndex,
|
||||
name: this.tabs[this.currentIndex],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is a next tab available to move to
|
||||
* @returns True if there's a next tab, false otherwise
|
||||
*/
|
||||
private canMoveToNextTab(): boolean {
|
||||
return this.currentIndex < this.tabs.length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to the next tab
|
||||
*/
|
||||
private moveToNextTab(): void {
|
||||
if (this.currentIndex < this.tabs.length - 1) {
|
||||
this.currentIndex++;
|
||||
this.updateVisibleRange();
|
||||
this.onTabChanged.emit({
|
||||
index: this.currentIndex,
|
||||
name: this.tabs[this.currentIndex],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index of the currently selected tab
|
||||
* @returns The index of the current tab
|
||||
*/
|
||||
getCurrentTabIndex(): number {
|
||||
return this.currentIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the currently selected tab
|
||||
* @returns The name of the current tab
|
||||
*/
|
||||
getCurrentTabName(): string {
|
||||
if (this.currentIndex >= 0 && this.currentIndex < this.tabs.length) {
|
||||
return this.tabs[this.currentIndex];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currently selected tab by index
|
||||
* @param index The index of the tab to select
|
||||
*/
|
||||
setCurrentTabIndex(index: number): void {
|
||||
if (index >= 0 && index < this.tabs.length) {
|
||||
this.currentIndex = index;
|
||||
this.updateVisibleRange();
|
||||
this.onTabChanged.emit({
|
||||
index: this.currentIndex,
|
||||
name: this.tabs[this.currentIndex],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the list of tabs with new tab names
|
||||
* @param tabNames The new list of tab names
|
||||
*/
|
||||
setTabNames(tabNames: string[]): void {
|
||||
this.tabs = [...tabNames];
|
||||
|
||||
// Ensure current index is within bounds
|
||||
if (this.currentIndex >= this.tabs.length) {
|
||||
this.currentIndex = Math.max(0, this.tabs.length - 1);
|
||||
}
|
||||
|
||||
this.updateVisibleRange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base Window class to manage UI components
|
||||
*/
|
||||
@@ -516,7 +951,7 @@ class UIWindow {
|
||||
private components: UIComponent[] = [];
|
||||
private focusedComponentIndex = -1;
|
||||
|
||||
addComponent(component: UIComponent, manager: GlobalManager): void {
|
||||
addComponent(component: UIComponent): void {
|
||||
this.components.push(component);
|
||||
}
|
||||
|
||||
@@ -566,6 +1001,12 @@ class UIWindow {
|
||||
}
|
||||
}
|
||||
|
||||
handleTimerTrigger(event: TimerEvent) {
|
||||
for (const component of this.components) {
|
||||
component.handleTimerTrigger(event);
|
||||
}
|
||||
}
|
||||
|
||||
setFocus(index: number): void {
|
||||
// Unfocus current component
|
||||
if (
|
||||
@@ -613,22 +1054,16 @@ class UIWindow {
|
||||
*/
|
||||
class TUIApplication {
|
||||
private log = new CCLog(`TUI.log`, false, DAY);
|
||||
private manager: GlobalManager;
|
||||
|
||||
private window: UIWindow;
|
||||
private running = false;
|
||||
private keyEvent?: KeyEvent;
|
||||
private charEvent?: CharEvent;
|
||||
|
||||
constructor() {
|
||||
this.window = new UIWindow();
|
||||
this.manager = {
|
||||
log: this.log,
|
||||
};
|
||||
}
|
||||
|
||||
addComponent(component: UIComponent): void {
|
||||
this.window.addComponent(component, this.manager);
|
||||
this.window.addComponent(component);
|
||||
}
|
||||
|
||||
run(): void {
|
||||
@@ -653,7 +1088,7 @@ class TUIApplication {
|
||||
|
||||
stop(): void {
|
||||
this.running = false;
|
||||
this.manager.log.close();
|
||||
this.log.close();
|
||||
}
|
||||
|
||||
mainLoop(): void {
|
||||
@@ -670,24 +1105,36 @@ class TUIApplication {
|
||||
keyLoop(): void {
|
||||
while (this.running) {
|
||||
// Handle input events
|
||||
this.keyEvent = pullEventAs(KeyEvent, "key");
|
||||
this.manager.log.debug(
|
||||
`[${TUIApplication.name}]: Get Key Event: ${textutils.serialise(this.keyEvent ?? {})}`,
|
||||
const keyEvent = pullEventAs(KeyEvent, "key");
|
||||
this.log.debug(
|
||||
`[${TUIApplication.name}]: Get Key Event: ${textutils.serialise(keyEvent ?? {})}`,
|
||||
);
|
||||
if (this.keyEvent == undefined) continue;
|
||||
this.window.handleKeyInput(this.keyEvent);
|
||||
if (keyEvent == undefined) continue;
|
||||
this.window.handleKeyInput(keyEvent);
|
||||
}
|
||||
}
|
||||
|
||||
charLoop(): void {
|
||||
while (this.running) {
|
||||
// Handle input events
|
||||
this.charEvent = pullEventAs(CharEvent, "char");
|
||||
this.manager.log.debug(
|
||||
`[${TUIApplication.name}]: Get Char Event: ${textutils.serialise(this.charEvent ?? {})}`,
|
||||
const charEvent = pullEventAs(CharEvent, "char");
|
||||
this.log.debug(
|
||||
`[${TUIApplication.name}]: Get Char Event: ${textutils.serialise(charEvent ?? {})}`,
|
||||
);
|
||||
if (this.charEvent == undefined) continue;
|
||||
this.window.handleCharInput(this.charEvent);
|
||||
if (charEvent == undefined) continue;
|
||||
this.window.handleCharInput(charEvent);
|
||||
}
|
||||
}
|
||||
|
||||
timerLoop(): void {
|
||||
while (this.running) {
|
||||
// Handle events
|
||||
const timerEvent = pullEventAs(TimerEvent, "timer");
|
||||
this.log.debug(
|
||||
`[${TUIApplication.name}]: Get Timer Event: ${textutils.serialise(timerEvent ?? {})}`,
|
||||
);
|
||||
if (timerEvent == undefined) continue;
|
||||
this.window.handleTimerTrigger(timerEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,6 +1150,7 @@ export {
|
||||
TextLabel,
|
||||
InputField,
|
||||
OptionSelector,
|
||||
TabWidget,
|
||||
UIWindow,
|
||||
TUIApplication,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TextLabel,
|
||||
InputField,
|
||||
OptionSelector,
|
||||
TabWidget,
|
||||
} from "../lib/ccTUI";
|
||||
|
||||
// Create the main application
|
||||
@@ -17,6 +18,7 @@ const [termWidth, _termHeight] = term.getSize();
|
||||
|
||||
// Create UI components
|
||||
const title = new TextLabel(
|
||||
"LabelTitle",
|
||||
Math.floor(termWidth / 2) - 10,
|
||||
2,
|
||||
"CC TUI Framework Demo",
|
||||
@@ -24,16 +26,27 @@ const title = new TextLabel(
|
||||
colors.black,
|
||||
);
|
||||
|
||||
const label1 = new TextLabel(5, 5, "Enter your name:");
|
||||
const label1 = new TextLabel("Label1", 5, 5, "Enter your name:");
|
||||
|
||||
const inputField = new InputField(5, 6, 30, "", "Type here...");
|
||||
const inputField = new InputField("LabelInput", 5, 6, 30, "", "Type here...");
|
||||
|
||||
const optionLabel = new TextLabel(5, 8, "Select an option:");
|
||||
const optionLabel = new TextLabel("LableOption", 5, 8, "Select an option:");
|
||||
|
||||
const options = ["Option 1", "Option 2", "Option 3", "Option 4"];
|
||||
const optionSelector = new OptionSelector(5, 9, options, "Choose:", 0);
|
||||
const optionSelector = new OptionSelector(
|
||||
"OptionSelector",
|
||||
5,
|
||||
9,
|
||||
options,
|
||||
"Choose:",
|
||||
0,
|
||||
);
|
||||
|
||||
const statusLabel = new TextLabel(5, 11, "Status: Ready");
|
||||
const statusLabel = new TextLabel("LableStatus", 5, 11, "Status: Ready");
|
||||
|
||||
// Create tab widget with sample tabs - using longer tab names for testing
|
||||
const tabNames = ["Home", "Settings", "User Profile", "Messages", "About Us", "Documentation", "Advanced Settings", "Account Management"];
|
||||
const tabWidget = new TabWidget("TabWidget", 5, 3, 50, tabNames, 0);
|
||||
|
||||
// Add components to the application
|
||||
app.addComponent(title);
|
||||
@@ -42,9 +55,10 @@ app.addComponent(inputField);
|
||||
app.addComponent(optionLabel);
|
||||
app.addComponent(optionSelector);
|
||||
app.addComponent(statusLabel);
|
||||
app.addComponent(tabWidget);
|
||||
|
||||
// Set focus to the input field initially
|
||||
app.getWindow().setFocusFor(optionSelector);
|
||||
app.getWindow().setFocusFor(tabWidget);
|
||||
|
||||
// Connect events
|
||||
optionSelector.onSelectionChanged.connect((data) => {
|
||||
@@ -61,6 +75,12 @@ inputField.onTextChanged.connect((value) => {
|
||||
}
|
||||
});
|
||||
|
||||
tabWidget.onTabChanged.connect((data) => {
|
||||
statusLabel.setText(
|
||||
`Status: Tab changed to ${data?.name} (index: ${data?.index})`,
|
||||
);
|
||||
});
|
||||
|
||||
// Run the application
|
||||
try {
|
||||
print("Starting CC TUI Demo. Press Ctrl+T to quit.");
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
{
|
||||
"name": "@jackmacwindows/cc-types",
|
||||
"version": "1.0.1",
|
||||
"description": "TypeScript type definitions for CraftOS modules.",
|
||||
"types": "index.d.ts",
|
||||
"files": ["./*.d.ts", "./audio", "./image", "./shell"],
|
||||
"author": "JackMacWindows",
|
||||
"license": "MIT"
|
||||
}
|
||||
"name": "@jackmacwindows/cc-types",
|
||||
"version": "1.0.1",
|
||||
"description": "TypeScript type definitions for CraftOS modules.",
|
||||
"types": "index.d.ts",
|
||||
"files": [
|
||||
"./*.d.ts",
|
||||
"./audio",
|
||||
"./image",
|
||||
"./shell"
|
||||
],
|
||||
"author": "JackMacWindows",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user