diff --git a/.vscode/settings.json b/.vscode/settings.json index cb5b1534..8af0e9a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,68 +1,69 @@ -{ - "cSpell.words": [ - "ADB's", - "adbd", - "allowlist", - "arraybuffer", - "autorun", - "brotli", - "Callout", - "Cascadia", - "CLSE", - "CNXN", - "Deserialization", - "Embedder", - "fluentui", - "genymobile", - "Genymobile's", - "getprop", - "griffel", - "keyof", - "laggy", - "localabstract", - "lstat", - "luma", - "mitm", - "Nalu", - "opendir", - "PKCS", - "runtimes", - "Scrcpy", - "sendrecv", - "sideload", - "streamsaver", - "struct", - "struct's", - "tcpip", - "tinyh", - "transferables", - "tsbuildinfo", - "typeof", - "webadb", - "webcodecs", - "websockify", - "webusb", - "wifi", - "wirelessly", - "WRTE", - "yume", - "zstd" - ], - "editor.tabSize": 4, - "jest.rootPath": "packages/struct", - "jest.showCoverageOnLoad": true, - "markdown.extension.toc.levels": "2..6", - "files.associations": { - "*.mdx": "markdown", - "*.json": "jsonc" - }, - "prettier.tabWidth": 4, - "json.schemas": [ - { - "fileMatch": [ - "version-policies.json" - ], - "url": "https://developer.microsoft.com/json-schemas/rush/v5/version-policies.schema.json" - } - ] -} +{ + "cSpell.words": [ + "ADB's", + "adbd", + "allowlist", + "arraybuffer", + "autorun", + "brotli", + "Callout", + "Cascadia", + "CLSE", + "CNXN", + "Deserialization", + "Embedder", + "fluentui", + "genymobile", + "Genymobile's", + "getprop", + "griffel", + "keyof", + "laggy", + "localabstract", + "Logcat", + "lstat", + "luma", + "mitm", + "Nalu", + "opendir", + "PKCS", + "runtimes", + "Scrcpy", + "sendrecv", + "sideload", + "streamsaver", + "struct", + "struct's", + "tcpip", + "tinyh", + "transferables", + "tsbuildinfo", + "typeof", + "webadb", + "webcodecs", + "websockify", + "webusb", + "wifi", + "wirelessly", + "WRTE", + "yume", + "zstd" + ], + "editor.tabSize": 4, + "jest.rootPath": "packages/struct", + "jest.showCoverageOnLoad": true, + "markdown.extension.toc.levels": "2..6", + "files.associations": { + "*.mdx": "markdown", + "*.json": "jsonc" + }, + "prettier.tabWidth": 4, + "json.schemas": [ + { + "fileMatch": [ + "version-policies.json" + ], + "url": "https://developer.microsoft.com/json-schemas/rush/v5/version-policies.schema.json" + } + ] +} diff --git a/apps/demo/package.json b/apps/demo/package.json index 55c2c9d0..3ce92ef7 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -1,49 +1,49 @@ -{ - "name": "demo", - "version": "0.1.0", - "private": true, - "scripts": { - "postinstall": "fetch-scrcpy-server 1.24", - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@fluentui/react": "^8.67.2", - "@fluentui/react-file-type-icons": "^8.6.6", - "@fluentui/react-hooks": "^8.5.4", - "@fluentui/react-icons": "^2.0.166-rc.3", - "@fluentui/style-utilities": "^8.6.5", - "@griffel/react": "^1.0.3", - "@yume-chan/adb": "^0.0.15", - "@yume-chan/adb-backend-direct-sockets": "^0.0.9", - "@yume-chan/adb-backend-webusb": "^0.0.15", - "@yume-chan/adb-backend-ws": "^0.0.9", - "@yume-chan/adb-credential-web": "^0.0.15", - "@yume-chan/android-bin": "^0.0.15", - "@yume-chan/async": "^2.1.4", - "@yume-chan/event": "^0.0.15", - "@yume-chan/scrcpy": "^0.0.15", - "@yume-chan/struct": "^0.0.15", - "mobx": "^6.5.0", - "mobx-react-lite": "^3.3.0", - "next": "12.1.5", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "streamsaver": "^2.0.5", - "xterm": "^4.18.0", - "xterm-addon-fit": "^0.5.0", - "xterm-addon-search": "^0.8.2", - "xterm-addon-webgl": "^0.11.4" - }, - "devDependencies": { - "@mdx-js/loader": "^1.6.22", - "@next/mdx": "^11.1.2", - "@types/react": "17.0.27", - "eslint": "8.8.0", - "eslint-config-next": "12.1.5", - "source-map-loader": "^3.0.1", - "typescript": "4.7.0-beta" - } -} +{ + "name": "demo", + "version": "0.1.0", + "private": true, + "scripts": { + "postinstall": "fetch-scrcpy-server 1.24", + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@fluentui/react": "^8.67.2", + "@fluentui/react-file-type-icons": "^8.6.6", + "@fluentui/react-hooks": "^8.5.4", + "@fluentui/react-icons": "^2.0.166-rc.3", + "@fluentui/style-utilities": "^8.6.5", + "@griffel/react": "^1.0.3", + "@yume-chan/adb": "^0.0.15", + "@yume-chan/adb-backend-direct-sockets": "^0.0.9", + "@yume-chan/adb-backend-webusb": "^0.0.15", + "@yume-chan/adb-backend-ws": "^0.0.9", + "@yume-chan/adb-credential-web": "^0.0.15", + "@yume-chan/android-bin": "^0.0.15", + "@yume-chan/async": "^2.1.4", + "@yume-chan/event": "^0.0.15", + "@yume-chan/scrcpy": "^0.0.15", + "@yume-chan/struct": "^0.0.15", + "mobx": "^6.5.0", + "mobx-react-lite": "^3.3.0", + "next": "12.1.5", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "streamsaver": "^2.0.5", + "xterm": "^4.18.0", + "xterm-addon-fit": "^0.5.0", + "xterm-addon-search": "^0.8.2", + "xterm-addon-webgl": "^0.11.4" + }, + "devDependencies": { + "@mdx-js/loader": "^1.6.22", + "@next/mdx": "^11.1.2", + "@types/react": "17.0.27", + "eslint": "8.8.0", + "eslint-config-next": "12.1.5", + "source-map-loader": "^3.0.1", + "typescript": "4.7.1-rc" + } +} diff --git a/apps/demo/src/components/grid.tsx b/apps/demo/src/components/grid.tsx index e8272d71..ae3e8ded 100644 --- a/apps/demo/src/components/grid.tsx +++ b/apps/demo/src/components/grid.tsx @@ -1,320 +1,320 @@ -import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; -import { ComponentType, CSSProperties, useLayoutEffect, useMemo, useState } from "react"; -import { useCallbackRef, withDisplayName } from "../utils"; -import { ResizeObserver, Size } from './resize-observer'; - -const useClasses = makeStyles({ - container: { - display: 'flex', - flexDirection: 'column', - ...shorthands.overflow('hidden'), - }, - header: { - position: 'relative', - }, - body: { - position: 'relative', - flexGrow: 1, - height: 0, - ...shorthands.overflow('auto'), - }, - placeholder: { - // make horizontal scrollbar visible - minHeight: '1px', - }, - row: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - willChange: 'transform', - }, - cell: { - position: 'absolute', - top: 0, - left: 0, - willChange: 'transform', - }, -}); - -export interface GridCellProps { - className: string; - style: CSSProperties; - rowIndex: number; - columnIndex: number; -} - -export interface GridCellWrapperProps { - CellComponent: ComponentType; - rowIndex: number; - rowHeight: number; - columnIndex: number; - columnWidth: number; - columnOffset: number; -} - -const GridCellWrapper = withDisplayName('GridCellWrapper')(({ - CellComponent, - rowIndex, - rowHeight, - columnIndex, - columnWidth, - columnOffset, -}: GridCellWrapperProps) => { - const classes = useClasses(); - - const styles = useMemo(() => ({ - width: columnWidth, - height: rowHeight, - transform: `translateX(${columnOffset}px)`, - }), [rowHeight, columnWidth, columnOffset]); - - return ( - - ); -}); - -export interface GridRowProps { - className: string; - style: CSSProperties; - rowIndex: number; - children: React.ReactNode; -} - -export interface GridColumn { - width: number; - minWidth?: number; - maxWidth?: number; - flexGrow?: number; - flexShrink?: number; - CellComponent: ComponentType; -} - -interface GridRowWrapperProps { - RowComponent: ComponentType; - rowIndex: number; - rowHeight: number; - columns: (GridColumn & { offset: number; })[]; -} - -const GridRowWrapper = withDisplayName('GridRowWrapper')(({ - RowComponent, - rowIndex, - rowHeight, - columns, -}: GridRowWrapperProps) => { - const classes = useClasses(); - - const styles = useMemo(() => ({ - height: rowHeight, - transform: `translateY(${rowIndex * rowHeight}px)`, - }), [rowIndex, rowHeight]); - - return ( - - {columns.map((column, columnIndex) => ( - - ))} - - ); -}); - -export interface GridHeaderProps { - className: string; - columnIndex: number; - style: CSSProperties; -} - -export interface GridProps { - className?: string; - rowCount: number; - rowHeight: number; - columns: GridColumn[]; - HeaderComponent: ComponentType; - RowComponent: ComponentType; -} - -export const Grid = withDisplayName('Grid')(({ - className, - rowCount, - rowHeight, - columns, - HeaderComponent, - RowComponent, -}: GridProps) => { - const classes = useClasses(); - - const [scrollLeft, setScrollLeft] = useState(0); - const [scrollTop, setScrollTop] = useState(0); - - const [bodyRef, setBodyRef] = useState(null); - const [bodySize, setBodySize] = useState({ width: 0, height: 0 }); - - const handleScroll = useCallbackRef(() => { - if (bodyRef) { - setScrollLeft(bodyRef.scrollLeft); - setScrollTop(bodyRef.scrollTop); - } - }); - - useLayoutEffect(() => { - if (bodyRef) { - setScrollLeft(bodyRef.scrollLeft); - setScrollTop(bodyRef.scrollTop); - } - }, [bodyRef]); - - const rowRange = useMemo(() => { - const start = Math.min(rowCount, Math.floor(scrollTop / rowHeight)); - const end = Math.min(rowCount, Math.ceil((scrollTop + bodySize.height) / rowHeight)); - return { start, end, offset: scrollTop - start * rowHeight }; - }, [scrollTop, bodySize.height, rowCount, rowHeight]); - - const columnMetadata = useMemo(() => { - if (bodySize.width === 0) { - return { - columns: [], - totalWidth: 0, - }; - } - - const result = []; - let requestedWidth = 0; - let columnsCanGrow = []; - let totalFlexGrow = 0; - let columnsCanShrink = []; - let totalFlexShrink = 0; - for (const column of columns) { - const copy = { ...column, offset: 0 }; - result.push(copy); - - requestedWidth += copy.width; - - if (copy.flexGrow !== undefined) { - columnsCanGrow.push(copy); - totalFlexGrow += copy.flexGrow; - } - - if (copy.flexShrink !== 0) { - if (copy.flexShrink === undefined) { - copy.flexShrink = 1; - } - if (copy.minWidth === undefined) { - copy.minWidth = 0; - } - columnsCanShrink.push(copy); - totalFlexShrink += copy.flexShrink; - } - } - - let extraWidth = bodySize.width - requestedWidth; - while (extraWidth > 1 && columnsCanGrow.length > 0) { - const growPerRatio = extraWidth / totalFlexGrow; - columnsCanGrow = columnsCanGrow.filter(column => { - let canGrowFurther = true; - const initialWidth = column.width; - column.width += column.flexGrow! * growPerRatio; - if (column.maxWidth !== undefined && column.width > column.maxWidth) { - column.width = column.maxWidth; - canGrowFurther = false; - } - extraWidth -= (column.width - initialWidth); - return canGrowFurther; - }); - } - - while (extraWidth < -1 && columnsCanShrink.length > 0) { - const shrinkPerRatio = -extraWidth / totalFlexShrink; - columnsCanShrink = columnsCanShrink.filter(column => { - let canShrinkFurther = true; - const initialWidth = column.width; - column.width -= column.flexShrink! * shrinkPerRatio; - if (column.width < column.minWidth!) { - column.width = column.minWidth!; - canShrinkFurther = false; - } - extraWidth += (initialWidth - column.width); - return canShrinkFurther; - }); - } - - let offset = 0; - for (const column of result) { - column.offset = offset; - offset += column.width; - } - - return { - columns: result, - totalWidth: offset, - }; - }, [columns, bodySize.width]); - - const headers = useMemo(() => ( - columnMetadata.columns.map((column, index) => ( - - )) - ), [columnMetadata, HeaderComponent, classes, rowHeight]); - - const headerStyle = useMemo(() => ({ - height: rowHeight, - transform: `translateX(-${scrollLeft}px)`, - }), [rowHeight, scrollLeft]); - - const placeholder = useMemo(() => ( -
- ), [classes, columnMetadata, rowCount, rowHeight]); - - return ( -
-
- {headers} -
-
- - {placeholder} - {Array.from( - { length: rowRange.end - rowRange.start }, - (_, rowIndex) => ( - - ) - )} -
-
- ); -}); +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import { ComponentType, CSSProperties, useLayoutEffect, useMemo, useState } from "react"; +import { useStableCallback, withDisplayName } from "../utils"; +import { ResizeObserver, Size } from './resize-observer'; + +const useClasses = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + ...shorthands.overflow('hidden'), + }, + header: { + position: 'relative', + }, + body: { + position: 'relative', + flexGrow: 1, + height: 0, + ...shorthands.overflow('auto'), + }, + placeholder: { + // make horizontal scrollbar visible + minHeight: '1px', + }, + row: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + willChange: 'transform', + }, + cell: { + position: 'absolute', + top: 0, + left: 0, + willChange: 'transform', + }, +}); + +export interface GridCellProps { + className: string; + style: CSSProperties; + rowIndex: number; + columnIndex: number; +} + +export interface GridCellWrapperProps { + CellComponent: ComponentType; + rowIndex: number; + rowHeight: number; + columnIndex: number; + columnWidth: number; + columnOffset: number; +} + +const GridCellWrapper = withDisplayName('GridCellWrapper')(({ + CellComponent, + rowIndex, + rowHeight, + columnIndex, + columnWidth, + columnOffset, +}: GridCellWrapperProps) => { + const classes = useClasses(); + + const styles = useMemo(() => ({ + width: columnWidth, + height: rowHeight, + transform: `translateX(${columnOffset}px)`, + }), [rowHeight, columnWidth, columnOffset]); + + return ( + + ); +}); + +export interface GridRowProps { + className: string; + style: CSSProperties; + rowIndex: number; + children: React.ReactNode; +} + +export interface GridColumn { + width: number; + minWidth?: number; + maxWidth?: number; + flexGrow?: number; + flexShrink?: number; + CellComponent: ComponentType; +} + +interface GridRowWrapperProps { + RowComponent: ComponentType; + rowIndex: number; + rowHeight: number; + columns: (GridColumn & { offset: number; })[]; +} + +const GridRowWrapper = withDisplayName('GridRowWrapper')(({ + RowComponent, + rowIndex, + rowHeight, + columns, +}: GridRowWrapperProps) => { + const classes = useClasses(); + + const styles = useMemo(() => ({ + height: rowHeight, + transform: `translateY(${rowIndex * rowHeight}px)`, + }), [rowIndex, rowHeight]); + + return ( + + {columns.map((column, columnIndex) => ( + + ))} + + ); +}); + +export interface GridHeaderProps { + className: string; + columnIndex: number; + style: CSSProperties; +} + +export interface GridProps { + className?: string; + rowCount: number; + rowHeight: number; + columns: GridColumn[]; + HeaderComponent: ComponentType; + RowComponent: ComponentType; +} + +export const Grid = withDisplayName('Grid')(({ + className, + rowCount, + rowHeight, + columns, + HeaderComponent, + RowComponent, +}: GridProps) => { + const classes = useClasses(); + + const [scrollLeft, setScrollLeft] = useState(0); + const [scrollTop, setScrollTop] = useState(0); + + const [bodyRef, setBodyRef] = useState(null); + const [bodySize, setBodySize] = useState({ width: 0, height: 0 }); + + const handleScroll = useStableCallback(() => { + if (bodyRef) { + setScrollLeft(bodyRef.scrollLeft); + setScrollTop(bodyRef.scrollTop); + } + }); + + useLayoutEffect(() => { + if (bodyRef) { + setScrollLeft(bodyRef.scrollLeft); + setScrollTop(bodyRef.scrollTop); + } + }, [bodyRef]); + + const rowRange = useMemo(() => { + const start = Math.min(rowCount, Math.floor(scrollTop / rowHeight)); + const end = Math.min(rowCount, Math.ceil((scrollTop + bodySize.height) / rowHeight)); + return { start, end, offset: scrollTop - start * rowHeight }; + }, [scrollTop, bodySize.height, rowCount, rowHeight]); + + const columnMetadata = useMemo(() => { + if (bodySize.width === 0) { + return { + columns: [], + totalWidth: 0, + }; + } + + const result = []; + let requestedWidth = 0; + let columnsCanGrow = []; + let totalFlexGrow = 0; + let columnsCanShrink = []; + let totalFlexShrink = 0; + for (const column of columns) { + const copy = { ...column, offset: 0 }; + result.push(copy); + + requestedWidth += copy.width; + + if (copy.flexGrow !== undefined) { + columnsCanGrow.push(copy); + totalFlexGrow += copy.flexGrow; + } + + if (copy.flexShrink !== 0) { + if (copy.flexShrink === undefined) { + copy.flexShrink = 1; + } + if (copy.minWidth === undefined) { + copy.minWidth = 0; + } + columnsCanShrink.push(copy); + totalFlexShrink += copy.flexShrink; + } + } + + let extraWidth = bodySize.width - requestedWidth; + while (extraWidth > 1 && columnsCanGrow.length > 0) { + const growPerRatio = extraWidth / totalFlexGrow; + columnsCanGrow = columnsCanGrow.filter(column => { + let canGrowFurther = true; + const initialWidth = column.width; + column.width += column.flexGrow! * growPerRatio; + if (column.maxWidth !== undefined && column.width > column.maxWidth) { + column.width = column.maxWidth; + canGrowFurther = false; + } + extraWidth -= (column.width - initialWidth); + return canGrowFurther; + }); + } + + while (extraWidth < -1 && columnsCanShrink.length > 0) { + const shrinkPerRatio = -extraWidth / totalFlexShrink; + columnsCanShrink = columnsCanShrink.filter(column => { + let canShrinkFurther = true; + const initialWidth = column.width; + column.width -= column.flexShrink! * shrinkPerRatio; + if (column.width < column.minWidth!) { + column.width = column.minWidth!; + canShrinkFurther = false; + } + extraWidth += (initialWidth - column.width); + return canShrinkFurther; + }); + } + + let offset = 0; + for (const column of result) { + column.offset = offset; + offset += column.width; + } + + return { + columns: result, + totalWidth: offset, + }; + }, [columns, bodySize.width]); + + const headers = useMemo(() => ( + columnMetadata.columns.map((column, index) => ( + + )) + ), [columnMetadata, HeaderComponent, classes, rowHeight]); + + const headerStyle = useMemo(() => ({ + height: rowHeight, + transform: `translateX(-${scrollLeft}px)`, + }), [rowHeight, scrollLeft]); + + const placeholder = useMemo(() => ( +
+ ), [classes, columnMetadata, rowCount, rowHeight]); + + return ( +
+
+ {headers} +
+
+ + {placeholder} + {Array.from( + { length: rowRange.end - rowRange.start }, + (_, rowIndex) => ( + + ) + )} +
+
+ ); +}); diff --git a/apps/demo/src/components/resize-observer.tsx b/apps/demo/src/components/resize-observer.tsx index e5d5a939..bf8516cc 100644 --- a/apps/demo/src/components/resize-observer.tsx +++ b/apps/demo/src/components/resize-observer.tsx @@ -1,48 +1,49 @@ -import { makeStyles } from "@griffel/react"; -import { useLayoutEffect, useState } from 'react'; -import { useCallbackRef, withDisplayName } from '../utils'; - -export interface Size { - width: number; - - height: number; -} - -export interface ResizeObserverProps { - onResize: (size: Size) => void; -} - -const useClasses = makeStyles({ - observer: { - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - visibility: 'hidden', - } -}); - -export const ResizeObserver = withDisplayName('ResizeObserver')(({ - onResize, -}: ResizeObserverProps): JSX.Element | null => { - const classes = useClasses(); - - const [iframeRef, setIframeRef] = useState(null); - - const handleResize = useCallbackRef(() => { - const { width, height } = iframeRef!.getBoundingClientRect(); - onResize({ width, height }); - }); - - useLayoutEffect(() => { - if (iframeRef) { - iframeRef.contentWindow!.addEventListener('resize', handleResize); - handleResize(); - } - }, [iframeRef, handleResize]); - - return ( -