diff --git a/package-lock.json b/package-lock.json index a80a1c8..6c6e2ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { - "name": "@rbxts/roact-blocks", - "version": "0.1.0", + "name": "@rbxts/zenui-core", + "version": "0.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@rbxts/roact-blocks", - "version": "0.1.0", - "license": "ISC", + "name": "@rbxts/zenui-core", + "version": "0.2.0", + "license": "MIT", "dependencies": { - "@rbxts/roact": "~1.4.2-ts" + "@rbxts/roact": "*", + "@rbxts/services": "^1.2.0", + "@rbxts/variant": "^1.0.1" }, "devDependencies": { "@rbxts/compiler-types": "^1.3.3-types.1", @@ -23,9 +25,6 @@ "eslint-plugin-roblox-ts": "^0.0.32", "prettier": "^2.6.2", "typescript": "^4.6.4" - }, - "peerDependencies": { - "@rbxts/roact": "~1.4.2-ts" } }, "node_modules/@eslint/eslintrc": { @@ -114,12 +113,22 @@ "resolved": "https://registry.npmjs.org/@rbxts/roact/-/roact-1.4.2-ts.0.tgz", "integrity": "sha512-IRJ4o0yv7bKmw0RVKi0KVYXmCjQjhnCYqIz1smVuglfxuC1KT6y8ItOJoTnbb8eY+MSf5j/GLV3fIvi2NcPm7w==" }, + "node_modules/@rbxts/services": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rbxts/services/-/services-1.2.0.tgz", + "integrity": "sha512-GEnAGgRz/oFEfBCjr+UChO7D49c/l+Wgr2OHMkURBKbVAqGg7hVQbEs9PUr+9BuVx/8OlsqZ/cvrUYbwAyd60A==" + }, "node_modules/@rbxts/types": { "version": "1.0.589", "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.589.tgz", "integrity": "sha512-dRZt8VJuh8nRNiMtsgjPpEKPr/3Bko2KsB/R9l5yPrdgU7d+uHDV6hdx77NW+q/9u017q8tt6GBL5CNniRk70g==", "dev": true }, + "node_modules/@rbxts/variant": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rbxts/variant/-/variant-1.0.1.tgz", + "integrity": "sha512-1F/fiKDnt/aXm+n0jLjBbugZUqy4INKIAGTx7aO9tE3jk66nuxw85JFn9CSXx7dP1zNYgjSHHF8IMd/JB/qEEg==" + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -2087,12 +2096,22 @@ "resolved": "https://registry.npmjs.org/@rbxts/roact/-/roact-1.4.2-ts.0.tgz", "integrity": "sha512-IRJ4o0yv7bKmw0RVKi0KVYXmCjQjhnCYqIz1smVuglfxuC1KT6y8ItOJoTnbb8eY+MSf5j/GLV3fIvi2NcPm7w==" }, + "@rbxts/services": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rbxts/services/-/services-1.2.0.tgz", + "integrity": "sha512-GEnAGgRz/oFEfBCjr+UChO7D49c/l+Wgr2OHMkURBKbVAqGg7hVQbEs9PUr+9BuVx/8OlsqZ/cvrUYbwAyd60A==" + }, "@rbxts/types": { "version": "1.0.589", "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.589.tgz", "integrity": "sha512-dRZt8VJuh8nRNiMtsgjPpEKPr/3Bko2KsB/R9l5yPrdgU7d+uHDV6hdx77NW+q/9u017q8tt6GBL5CNniRk70g==", "dev": true }, + "@rbxts/variant": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rbxts/variant/-/variant-1.0.1.tgz", + "integrity": "sha512-1F/fiKDnt/aXm+n0jLjBbugZUqy4INKIAGTx7aO9tE3jk66nuxw85JFn9CSXx7dP1zNYgjSHHF8IMd/JB/qEEg==" + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", diff --git a/package.json b/package.json index 11980e9..24204aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rbxts/zenui-core", - "version": "0.1.1", + "version": "0.2.0", "description": "", "main": "out/init.lua", "scripts": { @@ -10,7 +10,11 @@ "generate-barrels": "barrelsby --delete -c barrelsby.json", "yalc": "npm run prepublishOnly && yalc push" }, - "keywords": ["Roact", "TypeScript", "Components"], + "keywords": [ + "Roact", + "TypeScript", + "Components" + ], "author": "Vorlias", "license": "MIT", "types": "out/index.d.ts", @@ -35,6 +39,8 @@ "typescript": "^4.6.4" }, "dependencies": { - "@rbxts/roact": "*" + "@rbxts/roact": "*", + "@rbxts/services": "^1.2.0", + "@rbxts/variant": "^1.0.1" } } diff --git a/src/Layouts/ColumnView.tsx b/src/Layouts/ColumnView.tsx index 8529cd3..2d329ea 100644 --- a/src/Layouts/ColumnView.tsx +++ b/src/Layouts/ColumnView.tsx @@ -3,7 +3,8 @@ import { View } from "../Views/View"; export interface ColumnProps { Width?: UDim; - Alignment?: Roact.InferEnumNames | Enum.HorizontalAlignment; + VerticalAlignment?: Roact.InferEnumNames | Enum.VerticalAlignment; + HorizontalAlignment?: Roact.InferEnumNames | Enum.HorizontalAlignment; } /** * Represents a Column in a `` ({@link ColumnView}) @@ -53,8 +54,6 @@ export class ColumnView extends Roact.Component { } } - print(autoSizeCount, widthOffset, scaleOffset, "offsets", scaleOffset + autoSizeCount); - let idx = 0; const count = children.size(); for (const [key, child] of children) { @@ -71,8 +70,10 @@ export class ColumnView extends Roact.Component { AutomaticSize="Y" > {child} diff --git a/src/Utility/Padding.tsx b/src/Utility/Padding.tsx new file mode 100644 index 0000000..9429f8a --- /dev/null +++ b/src/Utility/Padding.tsx @@ -0,0 +1,145 @@ +type PaddingResult = Pick; +type PaddingOffset = { + [P in keyof PaddingResult]: number; +}; + +type PaddingAxisOffset = { PaddingHorizontal?: number; PaddingVertical?: number }; + +export type WidgetPadding = (Partial & PaddingAxisOffset) | number; +export type WidgetAxisPadding = PaddingAxisOffset | number; + +export function CalculatePaddingUDim2(padding: WidgetAxisPadding): UDim2 { + if (typeIs(padding, "number")) { + return new UDim2(0, padding, 0, padding); + } else if ("PaddingHorizontal" in padding || "PaddingVertical" in padding) { + const { PaddingHorizontal = 0, PaddingVertical = 0 } = padding; + return new UDim2(0, PaddingHorizontal, 0, PaddingVertical); + } + + throw `Invalid argument to CalculatePadding`; +} + +export function CalculatePadding(padding: WidgetPadding): Partial { + if (typeIs(padding, "number")) { + return { + PaddingBottom: new UDim(0, padding), + PaddingLeft: new UDim(0, padding), + PaddingRight: new UDim(0, padding), + PaddingTop: new UDim(0, padding), + }; + } else { + let padLeft = 0; + let padRight = 0; + let padTop = 0; + let padBottom = 0; + + if (padding.PaddingHorizontal !== undefined) { + padLeft += padding.PaddingHorizontal; + padRight += padding.PaddingHorizontal; + } + + if (padding.PaddingVertical !== undefined) { + padTop += padding.PaddingVertical; + padBottom += padding.PaddingVertical; + } + + if (padding.PaddingLeft !== undefined) padLeft += padding.PaddingLeft; + if (padding.PaddingRight !== undefined) padRight += padding.PaddingRight; + if (padding.PaddingTop !== undefined) padTop += padding.PaddingTop; + if (padding.PaddingBottom !== undefined) padBottom += padding.PaddingBottom; + + return { + PaddingBottom: new UDim(0, padBottom), + PaddingTop: new UDim(0, padTop), + PaddingLeft: new UDim(0, padLeft), + PaddingRight: new UDim(0, padRight), + }; + } +} + +import Roact from "@rbxts/roact"; +import variantModule, { isOfVariant, VariantOf } from "@rbxts/variant"; + +export const PaddingDim = variantModule({ + fromScale: (padding: Padding) => { + const { Left = 0, Right = 0, Top = 0, Bottom = 0, Vertical = 0, Horizontal = 0 } = padding; + + return { + Value: identity>({ + PaddingLeft: new UDim(Left + Horizontal, 0), + PaddingRight: new UDim(Right + Horizontal, 0), + PaddingTop: new UDim(Top + Vertical, 0), + PaddingBottom: new UDim(Bottom + Vertical, 0), + }), + }; + }, + fromOffset: (padding: Padding) => { + const { Left = 0, Right = 0, Top = 0, Bottom = 0, Vertical = 0, Horizontal = 0 } = padding; + + return { + Value: identity>({ + PaddingLeft: new UDim(0, Left + Horizontal), + PaddingRight: new UDim(0, Right + Horizontal), + PaddingTop: new UDim(0, Top + Vertical), + PaddingBottom: new UDim(0, Bottom + Vertical), + }), + }; + }, + from: (scale: Padding, offset: Padding) => { + const { Left = 0, Right = 0, Top = 0, Bottom = 0, Vertical = 0, Horizontal = 0 } = scale; + const { + Left: leftOffset = 0, + Right: rightOffset = 0, + Top: topOffset = 0, + Bottom: bottomOffset = 0, + Vertical: verticalOffset = 0, + Horizontal: horizontalOffset = 0, + } = offset; + + return { + Value: identity>({ + PaddingLeft: new UDim(Left + Horizontal, leftOffset + horizontalOffset), + PaddingRight: new UDim(Right + Horizontal, rightOffset + horizontalOffset), + PaddingTop: new UDim(Top + Vertical, topOffset + verticalOffset), + PaddingBottom: new UDim(Bottom + Vertical, bottomOffset + verticalOffset), + }), + }; + }, +}); +export type PaddingDim = VariantOf; + +export default interface Padding { + Left?: number; + Top?: number; + Right?: number; + Bottom?: number; + Vertical?: number; + Horizontal?: number; +} +export interface PaddingProps { + Padding: Padding | PaddingDim; + ForwardRef?: (rbx: UIPadding, padding: Padding) => void; +} +export default function Padding({ Padding: padding, ForwardRef: forwardRef }: PaddingProps) { + if (isOfVariant(padding, PaddingDim)) { + return ; + } + + const { Left = 0, Right = 0, Top = 0, Bottom = 0, Vertical = 0, Horizontal = 0 } = padding; + + return ( + { + forwardRef(ref, padding); + } + : undefined + } + PaddingBottom={new UDim(0, Bottom + Vertical)} + PaddingTop={new UDim(0, Top + Vertical)} + PaddingRight={new UDim(0, Right + Horizontal)} + PaddingLeft={new UDim(0, Left + Horizontal)} + /> + ); +} diff --git a/src/Views/ListView.tsx b/src/Views/ListView.tsx new file mode 100644 index 0000000..71728dd --- /dev/null +++ b/src/Views/ListView.tsx @@ -0,0 +1,112 @@ +import Roact, { InferEnumNames } from "@rbxts/roact"; +import Padding from "../Utility/Padding"; +import { View, ViewProps } from "./View"; + +export interface ListViewDefaultProps { + readonly FillDirection: InferEnumNames; + /** + * Automatically size to the direction of the `FillDirection` given + */ + readonly AutomaticSize: boolean; +} + +export interface ListViewProps extends Pick, ListViewDefaultProps { + readonly ItemPadding?: number | UDim; + readonly Size?: UDim2; + readonly HorizontalAlignment?: InferEnumNames | Enum.HorizontalAlignment; + readonly VerticalAlignment?: InferEnumNames | Enum.VerticalAlignment; + readonly Padding?: Padding; +} + +/** + * ### ZenUI::ListView + * + * A view that has an inbuilt {@link UIListLayout} that has the ability to automatically size. Also contains {@link Padding} support. + */ +export class ListView extends Roact.Component { + private size: Roact.Binding; + private setSize: Roact.BindingFunction; + + private padding: Roact.Binding; + private setPadding: Roact.BindingFunction; + + public static defaultProps: ListViewDefaultProps = { + FillDirection: "Vertical", + AutomaticSize: false, + }; + + public constructor(props: ListViewProps) { + super(props); + [this.size, this.setSize] = Roact.createBinding(new Vector2()); + [this.padding, this.setPadding] = Roact.createBinding(new Vector2()); + } + + public render(): Roact.Element | undefined { + const { + FillDirection, + AutomaticSize, + Size = new UDim2(1, 0, 1, 0), + Padding: padding, + ItemPadding, + } = this.props; + + const children = this.props[Roact.Children]; + if (children) { + for (const [k, child] of pairs(children)) { + if (child.component === "UIListLayout" || child.component === "UIGridLayout") { + warn("Duplicate UILayout '" + tostring(k) + "' in ListView - removed!"); + children.delete(k); + } + + if (child.component === "UIPadding" && padding !== undefined) { + warn("Duplicate UIPadding '" + tostring(k) + "' in ListView - removed!"); + children.delete(k); + } + } + } + + const binds = Roact.joinBindings({ size: this.size, padding: this.padding }); + + return ( + { + const { size, padding } = v; + + if (AutomaticSize) { + if (FillDirection === "Vertical") { + return new UDim2(Size.X.Scale, Size.X.Offset, 0, size.Y + padding.Y); + } else { + return new UDim2(0, size.X + padding.X, Size.Y.Scale, Size.Y.Offset); + } + } else { + return Size; + } + })} + > + { + this.setSize(listLayout.AbsoluteContentSize); + }, + }} + /> + {padding !== undefined && ( + { + const { Left = 0, Right = 0, Top = 0, Bottom = 0, Vertical = 0, Horizontal = 0 } = padding; + this.setPadding(new Vector2(Left + Right + Horizontal * 2, Top + Bottom + Vertical * 2)); + }} + /> + )} + {this.props[Roact.Children]} + + ); + } +} diff --git a/src/Views/View.tsx b/src/Views/View.tsx index ae7c2c7..f525948 100644 --- a/src/Views/View.tsx +++ b/src/Views/View.tsx @@ -5,6 +5,6 @@ export type ViewProps = Roact.JsxInstanceProperties; /** * A frame that's by default transparent, and size of `UDim2.fromScale(1, 1)` */ -export function View(props: ViewProps) { +export function View(props: Roact.PropsWithChildren) { return ; } diff --git a/src/Widgets/AutoSizedText.tsx b/src/Widgets/AutoSizedText.tsx new file mode 100644 index 0000000..d63f8d9 --- /dev/null +++ b/src/Widgets/AutoSizedText.tsx @@ -0,0 +1,47 @@ +import { TextService } from "@rbxts/services"; +import Roact, { InferEnumNames } from "@rbxts/roact"; + +interface AutoSizedTextProps + extends Partial> { + Text: string; + Limits?: Vector2; + MinTextSize?: number; + MaxTextSize?: number; + MinSize?: Vector2; + MaxSize?: Vector2; +} +export function AutoSizedText(props: AutoSizedTextProps) { + const { + Text, + TextColor3, + TextSize = 15, + Font = Enum.Font.SourceSans, + Limits, + Position, + AnchorPoint, + MinSize, + MinTextSize, + MaxTextSize, + } = props; + + const size = TextService.GetTextSize(Text, TextSize, Font, Limits ?? new Vector2(1000, 1000)); + const minX = MinSize?.X ?? 0; + + return ( + + + + ); +}