diff --git a/packages/ui/package.json b/packages/ui/package.json index b114b9b..923cb0a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -40,11 +40,9 @@ "typescript": "5.5.3" }, "dependencies": { - "@rbxts/charm": "^0.8.1", "@rbxts/services": "^1.5.5", "@rbxts/set-timeout": "^1.1.2", - "@rbxts/vide": "^0.5.0", - "@rbxts/vide-charm": "^0.1.1" + "@rbxts/vide": "^0.5.0" }, "peerDependencies": { "@rbxts/centurion": "workspace:^" diff --git a/packages/ui/src/app/centurion-app.tsx b/packages/ui/src/app/centurion-app.tsx index 5c23117..3253abf 100644 --- a/packages/ui/src/app/centurion-app.tsx +++ b/packages/ui/src/app/centurion-app.tsx @@ -1,18 +1,13 @@ import { CenturionClient } from "@rbxts/centurion"; import { UserInputService } from "@rbxts/services"; import Vide, { derive } from "@rbxts/vide"; -import { useAtom } from "@rbxts/vide-charm"; import { Suggestions, Terminal } from "../components"; import { Group } from "../components/ui/group"; import { Layer } from "../components/ui/layer"; import { useClient } from "../hooks/use-client"; import { useEvent } from "../hooks/use-event"; import { px, usePx } from "../hooks/use-px"; -import { - interfaceOptions, - interfaceVisible, - mouseOverInterface, -} from "../store"; +import { mouseOverInterface, options, visible } from "../store"; const MOUSE_INPUT_TYPES = new Set([ Enum.UserInputType.MouseButton1, @@ -24,20 +19,17 @@ export function CenturionApp(client: CenturionClient) { useClient(client); usePx(); - const options = useAtom(interfaceOptions); - const visible = useAtom(interfaceVisible); - const validKeys = derive(() => new Set(options().activationKeys)); useEvent(UserInputService.InputBegan, (input, gameProcessed) => { if (validKeys().has(input.KeyCode) && !gameProcessed) { - interfaceVisible(!visible()); + visible(!visible()); } else if ( options().hideOnLostFocus && MOUSE_INPUT_TYPES.has(input.UserInputType) && !mouseOverInterface() ) { - interfaceVisible(false); + visible(false); } }); diff --git a/packages/ui/src/app/index.ts b/packages/ui/src/app/index.ts index 6ee757e..5f1fdc1 100644 --- a/packages/ui/src/app/index.ts +++ b/packages/ui/src/app/index.ts @@ -4,7 +4,7 @@ import { ContentProvider, Players } from "@rbxts/services"; import { mount } from "@rbxts/vide"; import { DEFAULT_INTERFACE_OPTIONS } from "../constants/options"; import { DefaultPalette } from "../palette"; -import { interfaceOptions, interfaceVisible } from "../store"; +import { options as uiOptions, visible as uiVisible } from "../store"; import { InterfaceOptions } from "../types"; import { CenturionApp } from "./centurion-app"; @@ -18,7 +18,7 @@ export namespace CenturionUI { * @returns Whether the terminal UI is visible. */ export function isVisible() { - return interfaceVisible(); + return uiVisible(); } /** @@ -27,7 +27,7 @@ export namespace CenturionUI { * @param visible Whether the terminal UI should be visible. */ export function setVisible(visible: boolean) { - interfaceVisible(visible); + uiVisible(visible); } /** @@ -36,11 +36,11 @@ export namespace CenturionUI { * @param options The options to update. */ export function updateOptions(options: Partial) { - interfaceOptions((prev) => ({ + uiOptions({ ...DEFAULT_INTERFACE_OPTIONS, - ...prev, + ...uiOptions(), ...options, - })); + }); } /** diff --git a/packages/ui/src/components/history/history-line.tsx b/packages/ui/src/components/history/history-line.tsx index b16ff7a..0003d25 100644 --- a/packages/ui/src/components/history/history-line.tsx +++ b/packages/ui/src/components/history/history-line.tsx @@ -1,9 +1,8 @@ import { HistoryEntry } from "@rbxts/centurion"; import Vide, { Derivable, derive } from "@rbxts/vide"; -import { useAtom } from "@rbxts/vide-charm"; import { HISTORY_TEXT_SIZE } from "../../constants/text"; import { px } from "../../hooks/use-px"; -import { interfaceOptions } from "../../store"; +import { options } from "../../store"; import { Frame } from "../ui/frame"; import { Group } from "../ui/group"; import { Outline } from "../ui/outline"; @@ -18,8 +17,6 @@ interface HistoryLineProps { } export function HistoryLine({ data, size, position, order }: HistoryLineProps) { - const options = useAtom(interfaceOptions); - const date = derive(() => { const dateTime = DateTime.fromUnixTimestamp(data.sentAt).FormatLocalTime( "LT", diff --git a/packages/ui/src/components/history/history-list.tsx b/packages/ui/src/components/history/history-list.tsx index 7cb5d06..6f0e462 100644 --- a/packages/ui/src/components/history/history-list.tsx +++ b/packages/ui/src/components/history/history-list.tsx @@ -1,7 +1,6 @@ import Vide, { Derivable, derive, For, read } from "@rbxts/vide"; -import { useAtom } from "@rbxts/vide-charm"; import { px } from "../../hooks/use-px"; -import { interfaceOptions } from "../../store"; +import { options } from "../../store"; import { HistoryData, HistoryLineData } from "../../types"; import { ScrollingFrame } from "../ui/scrolling-frame"; import { HistoryLine } from "./history-line"; @@ -20,8 +19,6 @@ export function HistoryList({ position, maxHeight, }: HistoryListProps) { - const options = useAtom(interfaceOptions); - const height = derive(() => read(data).height - px(8)); const exceedsMaxHeight = derive( () => maxHeight !== undefined && height() > read(maxHeight), diff --git a/packages/ui/src/components/suggestions/badge.tsx b/packages/ui/src/components/suggestions/badge.tsx index cc0d50d..c617f22 100644 --- a/packages/ui/src/components/suggestions/badge.tsx +++ b/packages/ui/src/components/suggestions/badge.tsx @@ -1,7 +1,6 @@ import Vide, { Derivable, InstanceAttributes } from "@rbxts/vide"; -import { useAtom } from "@rbxts/vide-charm"; import { px } from "../../hooks/use-px"; -import { interfaceOptions } from "../../store"; +import { options } from "../../store"; import { Frame } from "../ui/frame"; import { Text } from "../ui/text"; @@ -20,8 +19,6 @@ interface BadgeProps { } export function Badge(props: BadgeProps) { - const options = useAtom(interfaceOptions); - return ( { return ArrayUtil.slice(read(suggestion)?.others ?? [], 0, MAX_SUGGESTIONS); }); diff --git a/packages/ui/src/components/suggestions/suggestions.tsx b/packages/ui/src/components/suggestions/suggestions.tsx index d945701..0d658a9 100644 --- a/packages/ui/src/components/suggestions/suggestions.tsx +++ b/packages/ui/src/components/suggestions/suggestions.tsx @@ -1,17 +1,12 @@ import { TextService } from "@rbxts/services"; import Vide, { cleanup, derive, source, spring } from "@rbxts/vide"; -import { useAtom } from "@rbxts/vide-charm"; import { SUGGESTION_TEXT_SIZE, SUGGESTION_TITLE_TEXT_SIZE, } from "../../constants/text"; import { px } from "../../hooks/use-px"; import { useTextBounds } from "../../hooks/use-text-bounds"; -import { - currentSuggestion, - currentTextPart, - interfaceOptions, -} from "../../store"; +import { currentSuggestion, currentTextPart, options } from "../../store"; import { Group } from "../ui/group"; import { MainSuggestion } from "./main-suggestion"; import { SuggestionList } from "./suggestion-list"; @@ -20,9 +15,6 @@ const MAX_SUGGESTION_WIDTH = 180; const PADDING = 8; export function Suggestions() { - const options = useAtom(interfaceOptions); - const textPart = useAtom(currentTextPart); - const suggestion = useAtom(currentSuggestion); const suggestionRef = source(); const offset = (boundsState: () => Vector2) => () => { @@ -31,13 +23,13 @@ export function Suggestions() { }; const titleBounds = useTextBounds({ - text: () => suggestion()?.title, + text: () => currentSuggestion()?.title, font: () => options().font.bold, size: () => px(SUGGESTION_TITLE_TEXT_SIZE), }); const descriptionBounds = useTextBounds({ - text: () => suggestion()?.description, + text: () => currentSuggestion()?.description, font: () => options().font.regular, size: () => px(SUGGESTION_TEXT_SIZE), width: () => px(MAX_SUGGESTION_WIDTH), @@ -45,9 +37,9 @@ export function Suggestions() { const typeBadgeBounds = useTextBounds({ text: () => { - const currentSuggestion = suggestion(); - if (currentSuggestion?.type === "command") return; - return currentSuggestion?.dataType; + const suggestion = currentSuggestion(); + if (suggestion?.type === "command") return; + return suggestion?.dataType; }, font: () => options().font.bold, size: () => px(SUGGESTION_TEXT_SIZE), @@ -55,9 +47,9 @@ export function Suggestions() { const errorBounds = useTextBounds({ text: () => { - const current = suggestion(); - if (current?.type === "command") return; - return current?.error; + const suggestion = currentSuggestion(); + if (suggestion?.type === "command") return; + return suggestion?.error; }, font: () => options().font.regular, size: () => px(SUGGESTION_TEXT_SIZE), @@ -70,7 +62,7 @@ export function Suggestions() { const listBounds = derive(() => { let width = 0; - const suggestions = suggestion()?.others ?? []; + const suggestions = currentSuggestion()?.others ?? []; for (const value of suggestions) { listBoundsParams.Text = value; const suggestionBounds = TextService.GetTextBoundsAsync(listBoundsParams); @@ -107,8 +99,8 @@ export function Suggestions() { return ( UDim2.fromOffset(windowSize().X.Offset, listBounds().Y), 0.3, diff --git a/packages/ui/src/components/terminal/terminal-text-field/suggestion.ts b/packages/ui/src/components/terminal/terminal-text-field/suggestion.ts index 92a29a6..851ab53 100644 --- a/packages/ui/src/components/terminal/terminal-text-field/suggestion.ts +++ b/packages/ui/src/components/terminal/terminal-text-field/suggestion.ts @@ -1,11 +1,9 @@ import { BaseRegistry } from "@rbxts/centurion"; import { - atNextPart, commandArgIndex, currentCommandPath, currentTextPart, terminalArgIndex, - terminalTextParts, } from "../../../store"; import { Suggestion } from "../../../types"; import { formatPartsAsPath, getArgumentNames } from "../command"; @@ -22,9 +20,9 @@ function replaceTextPart(text: string, ...suggestions: string[]) { export function completeCommand( registry: BaseRegistry, text: string, + textParts: string[], suggestion: string, ) { - const textParts = terminalTextParts(); const atNextPart = text.sub(-1) === " "; const pathParts = [...textParts]; if (!atNextPart) { @@ -64,14 +62,15 @@ export function completeArgument( export function getSuggestedText( registry: BaseRegistry, text: string, + textParts: string[], + atNextPart: boolean, suggestion?: Suggestion, ) { if (suggestion === undefined) return ""; let suggestionText: string; - const parts = terminalTextParts(); - if (parts.isEmpty() || suggestion.type === "command") { + if (textParts.isEmpty() || suggestion.type === "command") { suggestionText = suggestion.title; } else { const command = currentCommandPath(); @@ -79,7 +78,7 @@ export function getSuggestedText( if (command === undefined || argIndex === undefined) return ""; - if (atNextPart() && argIndex === commandArgIndex()) { + if (atNextPart && argIndex === commandArgIndex()) { suggestionText = suggestion.title; } else if (!suggestion.others.isEmpty()) { suggestionText = suggestion.others[0]; diff --git a/packages/ui/src/components/terminal/terminal-text-field/terminal-text-field.tsx b/packages/ui/src/components/terminal/terminal-text-field/terminal-text-field.tsx index 04d01d4..7eade48 100644 --- a/packages/ui/src/components/terminal/terminal-text-field/terminal-text-field.tsx +++ b/packages/ui/src/components/terminal/terminal-text-field/terminal-text-field.tsx @@ -1,16 +1,14 @@ -import { subscribe } from "@rbxts/charm"; import { UserInputService } from "@rbxts/services"; -import Vide, { cleanup, Derivable, source } from "@rbxts/vide"; -import { useAtom } from "@rbxts/vide-charm"; +import Vide, { Derivable, effect, source } from "@rbxts/vide"; import { useClient } from "../../../hooks/use-client"; import { useEvent } from "../../../hooks/use-event"; import { px } from "../../../hooks/use-px"; import { currentCommandPath, currentSuggestion, - interfaceOptions, - interfaceVisible, + options, terminalTextValid, + visible, } from "../../../store"; import { Frame } from "../../ui/frame"; import { Padding } from "../../ui/padding"; @@ -29,6 +27,8 @@ interface TerminalTextFieldProps { backgroundTransparency?: Derivable; onTextChange?: (text: string) => void; onSubmit?: (text: string) => void; + textParts: () => string[]; + atNextPart: () => boolean; } const TEXT_SIZE = 22; @@ -40,10 +40,10 @@ export function TerminalTextField({ backgroundTransparency, onTextChange, onSubmit, + textParts, + atNextPart, }: TerminalTextFieldProps) { const client = useClient(); - const options = useAtom(interfaceOptions); - const valid = useAtom(terminalTextValid); const ref = source(); const commandHistory = source([]); @@ -52,14 +52,13 @@ export function TerminalTextField({ let currentTextValue = ""; // Focus text field when terminal becomes visible - const visibleConnection = subscribe(interfaceVisible, (visible) => { - if (visible) { + effect(() => { + if (visible()) { ref()?.CaptureFocus(); } else { ref()?.ReleaseFocus(); } }); - cleanup(visibleConnection); const setText = (text: string) => { const textBox = ref(); @@ -95,12 +94,17 @@ export function TerminalTextField({ commandHistoryIndex(newIndex); }; - const suggestionConnection = subscribe(currentSuggestion, (suggestion) => { + effect(() => { suggestionText( - getSuggestedText(client.registry, ref()?.Text ?? "", suggestion), + getSuggestedText( + client.registry, + ref()?.Text ?? "", + textParts(), + atNextPart(), + currentSuggestion(), + ), ); }); - cleanup(suggestionConnection); useEvent(UserInputService.InputBegan, (input) => { const textBox = ref(); @@ -123,7 +127,14 @@ export function TerminalTextField({ const currentText = textBox.Text; if (commandPath === undefined) { - setText(completeCommand(client.registry, textBox.Text, suggestion.title)); + setText( + completeCommand( + client.registry, + textBox.Text, + textParts(), + suggestion.title, + ), + ); return; } @@ -158,7 +169,9 @@ export function TerminalTextField({ size={UDim2.fromScale(1, 1)} textSize={() => px(TEXT_SIZE)} textColor={() => { - return valid() ? options().palette.success : options().palette.error; + return terminalTextValid() + ? options().palette.success + : options().palette.error; }} textXAlignment="Left" placeholderText="Enter command..." diff --git a/packages/ui/src/components/terminal/terminal.tsx b/packages/ui/src/components/terminal/terminal.tsx index 17385d7..aae8f64 100644 --- a/packages/ui/src/components/terminal/terminal.tsx +++ b/packages/ui/src/components/terminal/terminal.tsx @@ -2,25 +2,23 @@ import { ArgumentOptions, RegistryPath } from "@rbxts/centurion"; import { ArrayUtil, ReadonlyDeep } from "@rbxts/centurion/out/shared/util/data"; import { splitString } from "@rbxts/centurion/out/shared/util/string"; import Vide, { derive, source, spring } from "@rbxts/vide"; -import { useAtom } from "@rbxts/vide-charm"; import { HISTORY_TEXT_SIZE } from "../../constants/text"; import { useClient } from "../../hooks/use-client"; import { useHistory } from "../../hooks/use-history"; import { px } from "../../hooks/use-px"; import { - atNextPart, commandArgIndex, currentCommandPath, currentSuggestion, currentTextPart, - interfaceOptions, mouseOverInterface, + options, terminalArgIndex, terminalText, - terminalTextParts, terminalTextValid, } from "../../store"; import { ArgumentSuggestion } from "../../types"; +import { isQuoteEnded, isQuoteStarted } from "../../utils/string"; import { HistoryList } from "../history"; import { Frame } from "../ui/frame"; import { Padding } from "../ui/padding"; @@ -34,10 +32,24 @@ const TRAILING_SPACE_PATTERN = "(%s+)$"; export function Terminal() { const client = useClient(); - const options = useAtom(interfaceOptions); const missingArgs = source([]); const history = useHistory(); + const terminalTextParts = derive(() => { + return splitString(terminalText(), " ", true); + }); + + const atNextPart = derive(() => { + const parts = terminalTextParts(); + const textPart = parts[parts.size() - 1] as string | undefined; + + let quoted = false; + if (textPart !== undefined) { + quoted = isQuoteStarted(textPart) && !isQuoteEnded(textPart); + } + return terminalText().sub(-1) === " " && !quoted; + }); + const terminalHeight = derive(() => { const padding = px.ceil(TEXT_FIELD_HEIGHT + 16); if (history().lines.isEmpty()) return padding; @@ -72,6 +84,8 @@ export function Terminal() { size={() => new UDim2(1, 0, 0, px(TEXT_FIELD_HEIGHT))} position={UDim2.fromScale(0, 1)} backgroundTransparency={() => options().backgroundTransparency ?? 0} + textParts={terminalTextParts} + atNextPart={atNextPart} onTextChange={(text) => { terminalText(text); terminalTextValid(false); diff --git a/packages/ui/src/components/ui/text-field.tsx b/packages/ui/src/components/ui/text-field.tsx index 842c15f..b98c973 100644 --- a/packages/ui/src/components/ui/text-field.tsx +++ b/packages/ui/src/components/ui/text-field.tsx @@ -1,6 +1,5 @@ import Vide, { Derivable, read } from "@rbxts/vide"; -import { useAtom } from "@rbxts/vide-charm"; -import { interfaceOptions } from "../../store"; +import { options } from "../../store"; import { TextProps } from "./text"; interface TextFieldProps extends TextProps { @@ -13,8 +12,6 @@ interface TextFieldProps extends TextProps { } export function TextField(props: TextFieldProps) { - const options = useAtom(interfaceOptions); - return ( @@ -22,8 +21,6 @@ export interface TextProps } export function Text(props: TextProps) { - const options = useAtom(interfaceOptions); - return ( read(props.font) ?? options().font.regular} diff --git a/packages/ui/src/hooks/use-history.ts b/packages/ui/src/hooks/use-history.ts index 0eeacb2..f7b34ad 100644 --- a/packages/ui/src/hooks/use-history.ts +++ b/packages/ui/src/hooks/use-history.ts @@ -1,9 +1,8 @@ import { HistoryEntry } from "@rbxts/centurion"; import { TextService } from "@rbxts/services"; import { cleanup, derive, source } from "@rbxts/vide"; -import { useAtom } from "@rbxts/vide-charm"; import { HISTORY_TEXT_SIZE } from "../constants/text"; -import { interfaceOptions } from "../store"; +import { options } from "../store"; import { HistoryData, HistoryLineData } from "../types"; import { useClient } from "./use-client"; import { useEvent } from "./use-event"; @@ -11,7 +10,6 @@ import { px } from "./use-px"; export function useHistory() { const client = useClient(); - const options = useAtom(interfaceOptions); const history = source(client.dispatcher.getHistory()); useEvent(client.dispatcher.historyUpdated, (entries) => diff --git a/packages/ui/src/store.ts b/packages/ui/src/store.ts index a4c46dd..597c45e 100644 --- a/packages/ui/src/store.ts +++ b/packages/ui/src/store.ts @@ -1,34 +1,19 @@ import { ImmutableRegistryPath } from "@rbxts/centurion"; -import { splitString } from "@rbxts/centurion/out/shared/util/string"; -import { atom, computed } from "@rbxts/charm"; +import { source } from "@rbxts/vide"; import { DEFAULT_INTERFACE_OPTIONS } from "./constants/options"; import { Suggestion } from "./types"; -import { isQuoteEnded, isQuoteStarted } from "./utils/string"; -export const interfaceVisible = atom(false); -export const interfaceOptions = atom(DEFAULT_INTERFACE_OPTIONS); -export const mouseOverInterface = atom(false); +export const visible = source(false); +export const options = source(DEFAULT_INTERFACE_OPTIONS); +export const mouseOverInterface = source(false); -export const currentCommandPath = atom( +export const currentCommandPath = source( undefined, ); -export const commandArgIndex = atom(undefined); -export const terminalArgIndex = atom(undefined); +export const commandArgIndex = source(undefined); +export const terminalArgIndex = source(undefined); -export const currentSuggestion = atom(undefined); -export const terminalText = atom(""); -export const terminalTextParts = computed(() => { - return splitString(terminalText(), " ", true); -}); -export const atNextPart = computed(() => { - const parts = terminalTextParts(); - const textPart = parts[parts.size() - 1] as string | undefined; - - let quoted = false; - if (textPart !== undefined) { - quoted = isQuoteStarted(textPart) && !isQuoteEnded(textPart); - } - return terminalText().sub(-1) === " " && !quoted; -}); -export const terminalTextValid = atom(false); -export const currentTextPart = atom(undefined); +export const currentSuggestion = source(undefined); +export const terminalText = source(""); +export const terminalTextValid = source(false); +export const currentTextPart = source(undefined); diff --git a/yarn.lock b/yarn.lock index 92634d7..14ed5af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1601,13 +1601,11 @@ __metadata: resolution: "@rbxts/centurion-ui@workspace:packages/ui" dependencies: "@rbxts/centurion": "workspace:^" - "@rbxts/charm": "npm:^0.8.1" "@rbxts/compiler-types": "npm:3.0.0-types.0" "@rbxts/services": "npm:^1.5.5" "@rbxts/set-timeout": "npm:^1.1.2" "@rbxts/types": "npm:^1.0.813" "@rbxts/vide": "npm:^0.5.0" - "@rbxts/vide-charm": "npm:^0.1.1" roblox-ts: "npm:3.0.0" shx: "npm:^0.3.4" typescript: "npm:5.5.3" @@ -1634,13 +1632,6 @@ __metadata: languageName: unknown linkType: soft -"@rbxts/charm@npm:0.8.1, @rbxts/charm@npm:^0.8.1": - version: 0.8.1 - resolution: "@rbxts/charm@npm:0.8.1" - checksum: 10c0/e82af9f8d01e677b5a1d64a2fdda8061cf502ffa4b03af7c7df5341746830f00ab9c1107977472dfb50f9a84b3fd9e31b3a0a33de75d88bf301ad2482b5842b7 - languageName: node - linkType: hard - "@rbxts/compiler-types@npm:3.0.0-types.0, @rbxts/compiler-types@npm:^3.0.0-types.0": version: 3.0.0-types.0 resolution: "@rbxts/compiler-types@npm:3.0.0-types.0" @@ -1742,17 +1733,6 @@ __metadata: languageName: node linkType: hard -"@rbxts/vide-charm@npm:^0.1.1": - version: 0.1.1 - resolution: "@rbxts/vide-charm@npm:0.1.1" - dependencies: - "@rbxts/charm": "npm:0.8.1" - peerDependencies: - "@rbxts/vide": "*" - checksum: 10c0/7c89d41c7ccaa3d2fda2d7441f3127e356ff7a9bd5daed70950de077c10f739b8744a9e9c8219e2f2d9f5bab772747570341e7c50a547ce206227bc5c1a14001 - languageName: node - linkType: hard - "@rbxts/vide@npm:^0.5.0": version: 0.5.0 resolution: "@rbxts/vide@npm:0.5.0"