Skip to content

Commit

Permalink
refactor(ui): switch to TextBounds for UI sizing
Browse files Browse the repository at this point in the history
The UI no longer uses GetTextBoundsAsync to size the UI based on its
contents.

This should be more performant and fix various issues, the main issue
being that the UI was not sized correctly (elements would overflow) -
this was very easy to reproduce on higher (1440p+) resolutions from my
testing, but it's probably still possible to reproduce on lower
resolutions such as 1080p.

Text preloading (previously used to avoid issues with GetTextBounds) has
also been removed; the UI should resize properly once the font has been
loaded.
  • Loading branch information
paradoxuum committed Dec 25, 2024
1 parent c982672 commit 25b0068
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 291 deletions.
23 changes: 0 additions & 23 deletions packages/ui/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import { InterfaceOptions } from "../types";
import { CenturionApp } from "./centurion-app";

export namespace CenturionUI {
const MAX_PRELOAD_ATTEMPTS = 3;
const PRELOAD_ATTEMPT_INTERVAL = 3;

/**
* Returns whether the terminal UI is visible.
*
Expand Down Expand Up @@ -61,26 +58,6 @@ export namespace CenturionUI {
}
updateOptions(options);

// Attempt to preload font
task.spawn(() => {
const fontFamily = (
options.font?.regular ?? DEFAULT_INTERFACE_OPTIONS.font.regular
).Family;

let attempts = 0;
while (attempts < MAX_PRELOAD_ATTEMPTS) {
ContentProvider.PreloadAsync([fontFamily], (_, status) => {
if (status === Enum.AssetFetchStatus.Success) {
attempts = MAX_PRELOAD_ATTEMPTS;
}
});

if (attempts === MAX_PRELOAD_ATTEMPTS) break;
task.wait(PRELOAD_ATTEMPT_INTERVAL);
attempts++;
}
});

mount(
() => CenturionApp(client),
Players.LocalPlayer.WaitForChild("PlayerGui"),
Expand Down
15 changes: 10 additions & 5 deletions packages/ui/src/components/history/history-line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ import { TextField } from "../ui/text-field";

interface HistoryLineProps {
data: HistoryEntry;
size?: Derivable<UDim2>;
position?: Derivable<UDim2>;
order?: Derivable<number>;
}

export function HistoryLine({ data, size, position, order }: HistoryLineProps) {
export function HistoryLine({ data, position, order }: HistoryLineProps) {
const date = derive(() => {
const dateTime = DateTime.fromUnixTimestamp(data.sentAt).FormatLocalTime(
"LT",
Expand All @@ -27,7 +26,12 @@ export function HistoryLine({ data, size, position, order }: HistoryLineProps) {
});

return (
<Group size={size} position={position} layoutOrder={order}>
<Group
automaticSize="Y"
size={UDim2.fromScale(1, 0)}
position={position}
layoutOrder={order}
>
<Frame
backgroundColor={() => options().palette.surface}
size={() => UDim2.fromOffset(px(76), px(HISTORY_TEXT_SIZE + 4))}
Expand Down Expand Up @@ -55,9 +59,10 @@ export function HistoryLine({ data, size, position, order }: HistoryLineProps) {
</Frame>

<TextField
automaticSize="Y"
anchor={new Vector2(1, 0)}
size={() => new UDim2(1, -px(84), 1, 0)}
position={UDim2.fromScale(1, 0)}
size={() => new UDim2(1, -px(84), 0, 0)}
position={() => new UDim2(1, 0, 0, px(2))}
text={data.text}
textSize={() => px(HISTORY_TEXT_SIZE)}
textColor={() => {
Expand Down
55 changes: 33 additions & 22 deletions packages/ui/src/components/history/history-list.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,65 @@
import Vide, { Derivable, derive, For, read } from "@rbxts/vide";
import { HistoryEntry } from "@rbxts/centurion";
import Vide, {
Derivable,
derive,
effect,
For,
read,
source,
} from "@rbxts/vide";
import { HISTORY_TEXT_SIZE } from "../../constants/text";
import { px } from "../../hooks/use-px";
import { options } from "../../store";
import { HistoryData, HistoryLineData } from "../../types";
import { ScrollingFrame } from "../ui/scrolling-frame";
import { HistoryLine } from "./history-line";

interface HistoryListProps {
data: Derivable<HistoryData>;
entries: Derivable<HistoryEntry[]>;
size?: Derivable<UDim2>;
position?: Derivable<UDim2>;
maxHeight?: Derivable<number>;
scrollingEnabled?: Derivable<boolean>;
onContentSizeChanged?: (size: Vector2) => void;
}

export function HistoryList({
data,
entries,
size,
position,
maxHeight,
scrollingEnabled,
onContentSizeChanged,
}: HistoryListProps) {
const height = derive(() => read(data).height - px(8));
const exceedsMaxHeight = derive(
() => maxHeight !== undefined && height() > read(maxHeight),
);
const ref = source<ScrollingFrame>();

return (
<ScrollingFrame
automaticCanvasSize="Y"
size={size}
position={position}
canvasSize={() => UDim2.fromOffset(0, height())}
canvasPosition={() => new Vector2(0, height())}
canvasSize={new UDim2()}
action={ref}
scrollBarColor={() => options().palette.subtext}
scrollBarThickness={() => (exceedsMaxHeight() ? 10 : 0)}
scrollingEnabled={() => exceedsMaxHeight()}
scrollingEnabled={scrollingEnabled}
scrollBarThickness={() => (read(scrollingEnabled) ? 10 : 0)}
scrollingDirection="Y"
native={{
AbsoluteCanvasSizeChanged: (rbx) => {
const frame = ref();
if (frame === undefined) return;
frame.CanvasPosition = new Vector2(0, rbx.Y);
},
}}
>
<For each={() => read(data).lines}>
{(line: HistoryLineData, index: () => number) => {
return (
<HistoryLine
size={new UDim2(1, 0, 0, line.height)}
data={line.entry}
order={index}
/>
);
<For each={() => read(entries)}>
{(entry: HistoryEntry, index: () => number) => {
return <HistoryLine data={entry} order={index} />;
}}
</For>

<uilistlayout
Padding={() => new UDim(0, px(8))}
SortOrder="LayoutOrder"
AbsoluteContentSizeChanged={(rbx) => onContentSizeChanged?.(rbx)}
/>
</ScrollingFrame>
);
Expand Down
157 changes: 101 additions & 56 deletions packages/ui/src/components/suggestions/main-suggestion.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Vide, { Derivable, read, spring } from "@rbxts/vide";
import Vide, { Derivable, derive, read, source, spring } from "@rbxts/vide";
import {
SUGGESTION_TEXT_SIZE,
SUGGESTION_TITLE_TEXT_SIZE,
Expand All @@ -9,34 +9,58 @@ import { Suggestion } from "../../types";
import { Frame } from "../ui/frame";
import { Padding } from "../ui/padding";
import { Text } from "../ui/text";
import { Badge } from "./badge";
import { highlightMatching } from "./util";

export interface MainSuggestionProps {
suggestion: Derivable<Suggestion | undefined>;
currentText?: Derivable<string | undefined>;
size: Derivable<UDim2>;
titleSize: Derivable<UDim2>;
descriptionSize: Derivable<UDim2>;
badgeSize: Derivable<UDim2>;
errorSize: Derivable<UDim2>;
action?: (instance: Frame) => void;
onSizeChanged?: (size: UDim2) => void;
}

const MAX_WIDTH = 180;

export function MainSuggestion({
suggestion,
currentText,
size,
titleSize,
descriptionSize,
badgeSize,
errorSize,
action,
onSizeChanged,
}: MainSuggestionProps) {
const titleBounds = source(new Vector2());
const descriptionBounds = source(new Vector2());
const errorBounds = source(new Vector2());
const badgeBounds = source(new Vector2());
const errorText = derive(() => {
const currentSuggestion = read(suggestion);
return currentSuggestion?.type === "argument"
? currentSuggestion.error
: undefined;
});

const windowSize = derive(() => {
if (read(suggestion) === undefined) {
const size = new UDim2();
onSizeChanged?.(size);
return size;
}

const titleSize = read(titleBounds);
const descriptionSize = read(descriptionBounds);
const errorSize = read(errorBounds);

const width =
math.max(titleSize.X, descriptionSize.X, errorSize.X) + badgeBounds().X;
const height = titleSize.Y + descriptionSize.Y + errorSize.Y;

const size = UDim2.fromOffset(width + px(8) * 2, height + px(8) * 2);
onSizeChanged?.(size);
return size;
});

return (
<Frame
action={action}
size={size}
size={spring(windowSize, 0.2)}
backgroundColor={() => options().palette.background}
backgroundTransparency={() => options().backgroundTransparency ?? 0}
cornerRadius={() => new UDim(0, px(8))}
Expand All @@ -47,34 +71,6 @@ export function MainSuggestion({
>
<Padding all={() => new UDim(0, px(8))} />

<Badge
color={() => options().palette.highlight}
text={() => {
const currentSuggestion = read(suggestion);
return currentSuggestion !== undefined &&
currentSuggestion.type === "argument"
? currentSuggestion.dataType
: "";
}}
textColor={() => options().palette.surface}
textSize={() => px(SUGGESTION_TEXT_SIZE)}
visible={() => {
const currentSuggestion = read(suggestion);
return (
currentSuggestion !== undefined &&
currentSuggestion.type === "argument"
);
}}
anchor={new Vector2(1, 0)}
position={UDim2.fromScale(1, 0)}
size={spring(() => {
return UDim2.fromOffset(
read(badgeSize).X.Offset + px(4),
px(SUGGESTION_TITLE_TEXT_SIZE),
);
}, 0.2)}
/>

<Text
text={() => {
const currentSuggestion = read(suggestion);
Expand All @@ -92,7 +88,12 @@ export function MainSuggestion({
textYAlignment="Top"
font={() => options().font.bold}
richText
size={titleSize}
size={() =>
UDim2.fromOffset(titleBounds().X, px(SUGGESTION_TITLE_TEXT_SIZE))
}
native={{
TextBoundsChanged: (bounds) => titleBounds(bounds),
}}
/>

<Text
Expand All @@ -104,31 +105,75 @@ export function MainSuggestion({
textWrapped
richText
position={() => UDim2.fromOffset(0, px(SUGGESTION_TITLE_TEXT_SIZE))}
size={descriptionSize}
size={() =>
UDim2.fromOffset(
px(MAX_WIDTH),
math.max(descriptionBounds().Y, px(SUGGESTION_TEXT_SIZE)),
)
}
native={{
TextBoundsChanged: (rbx) => descriptionBounds(rbx),
}}
/>

<Text
text={() => {
const currentSuggestion = read(suggestion);
return currentSuggestion !== undefined &&
currentSuggestion.type === "argument"
? (currentSuggestion.error ?? "")
: "";
}}
text={() => errorText() ?? ""}
textColor={() => options().palette.error}
textSize={() => px(SUGGESTION_TEXT_SIZE)}
textTransparency={spring(() => {
const currentSuggestion = read(suggestion);
return currentSuggestion?.type === "argument" &&
currentSuggestion.error !== undefined
? 0
: 1;
return errorText() !== undefined ? 0 : 1;
}, 0.2)}
textXAlignment="Left"
textWrapped
automaticSize="Y"
anchor={new Vector2(0, 1)}
position={UDim2.fromScale(0, 1)}
size={errorSize}
size={() => UDim2.fromOffset(px(MAX_WIDTH), 0)}
native={{
TextBoundsChanged: (bounds) => {
errorBounds(errorText() !== undefined ? bounds : Vector2.zero);
},
}}
/>

<Frame
backgroundColor={() => options().palette.highlight}
cornerRadius={() => new UDim(0, px(4))}
clipsDescendants
visible={() => {
const currentSuggestion = read(suggestion);
return (
currentSuggestion !== undefined &&
currentSuggestion.type === "argument"
);
}}
anchor={new Vector2(1, 0)}
position={UDim2.fromScale(1, 0)}
size={spring(() => {
return UDim2.fromOffset(
badgeBounds().X + px(4),
px(SUGGESTION_TITLE_TEXT_SIZE),
);
}, 0.2)}
>
<Text
text={() => {
const currentSuggestion = read(suggestion);
return currentSuggestion !== undefined &&
currentSuggestion.type === "argument"
? currentSuggestion.dataType
: "";
}}
textColor={() => options().palette.surface}
textSize={() => px(SUGGESTION_TEXT_SIZE)}
textXAlignment="Center"
font={() => options().font.bold}
size={UDim2.fromScale(1, 1)}
native={{
TextBoundsChanged: (bounds) => badgeBounds(bounds),
}}
/>
</Frame>
</Frame>
);
}
Loading

0 comments on commit 25b0068

Please sign in to comment.