Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/react-devtools-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"json5": "^2.2.3",
"local-storage-fallback": "^4.1.1",
"react-virtualized-auto-sizer": "^1.0.23",
"react-window": "^1.8.10"
"react-window": "^1.8.10",
"rbush": "4.0.1"
}
}
59 changes: 56 additions & 3 deletions packages/react-devtools-shared/src/devtools/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,31 @@ import type {
import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError';
import type {DevToolsHookSettings} from '../backend/types';

import RBush from 'rbush';

// Custom version which works with our Rect data structure.
class RectRBush extends RBush<Rect> {
toBBox(rect: Rect): {
minX: number,
minY: number,
maxX: number,
maxY: number,
} {
return {
minX: rect.x,
minY: rect.y,
maxX: rect.x + rect.width,
maxY: rect.y + rect.height,
};
}
compareMinX(a: Rect, b: Rect): number {
return a.x - b.x;
}
compareMinY(a: Rect, b: Rect): number {
return a.y - b.y;
}
}

const debug = (methodName: string, ...args: Array<string>) => {
if (__DEBUG__) {
console.log(
Expand Down Expand Up @@ -194,6 +219,9 @@ export default class Store extends EventEmitter<{
// Renderer ID is needed to support inspection fiber props, state, and hooks.
_rootIDToRendererID: Map<Element['id'], number> = new Map();

// Stores all the SuspenseNode rects in an R-tree to make it fast to find overlaps.
_rtree: RBush<Rect> = new RectRBush();

// These options may be initially set by a configuration option when constructing the Store.
_supportsInspectMatchingDOMElement: boolean = false;
_supportsClickToInspect: boolean = false;
Expand Down Expand Up @@ -1622,7 +1650,12 @@ export default class Store extends EventEmitter<{
const y = operations[i + 1] / 1000;
const width = operations[i + 2] / 1000;
const height = operations[i + 3] / 1000;
rects.push({x, y, width, height});
const rect = {x, y, width, height};
if (parentID !== 0) {
// Track all rects except the root.
this._rtree.insert(rect);
}
rects.push(rect);
i += 4;
}
}
Expand Down Expand Up @@ -1680,13 +1713,20 @@ export default class Store extends EventEmitter<{

i += 1;

const {children, parentID} = suspense;
const {children, parentID, rects} = suspense;
if (children.length > 0) {
this._throwAndEmitError(
Error(`Suspense node "${id}" was removed before its children.`),
);
}

if (rects !== null && parentID !== 0) {
// Delete all the existing rects from the R-tree
for (let j = 0; j < rects.length; j++) {
this._rtree.remove(rects[j]);
}
}

this._idToSuspense.delete(id);
removedSuspenseIDs.set(id, parentID);

Expand Down Expand Up @@ -1785,6 +1825,14 @@ export default class Store extends EventEmitter<{
break;
}

const prevRects = suspense.rects;
if (prevRects !== null && suspense.parentID !== 0) {
// Delete all the existing rects from the R-tree
for (let j = 0; j < prevRects.length; j++) {
this._rtree.remove(prevRects[j]);
}
}

let nextRects: SuspenseNode['rects'];
if (numRects === -1) {
nextRects = null;
Expand All @@ -1796,7 +1844,12 @@ export default class Store extends EventEmitter<{
const width = operations[i + 2] / 1000;
const height = operations[i + 3] / 1000;

nextRects.push({x, y, width, height});
const rect = {x, y, width, height};
if (suspense.parentID !== 0) {
// Track all rects except the root.
this._rtree.insert(rect);
}
nextRects.push(rect);

i += 4;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@
pointer-events: none;
}

.SuspenseRectsTitle {
pointer-events: none;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
font-size: var(--font-size-sans-small);
line-height: var(--font-size-sans-small);
padding: .25rem;
container-type: size;
container-name: title;
}

@container title (width < 30px) or (height < 12px) {
.SuspenseRectsTitle > span {
display: none;
}
}

.SuspenseRectsScaledRect[data-visible='false'] > .SuspenseRectsBoundaryChildren {
overflow: initial;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
SuspenseTreeDispatcherContext,
} from './SuspenseTreeContext';
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
import type RBush from 'rbush';

function ScaledRect({
className,
Expand Down Expand Up @@ -78,8 +79,10 @@ function ScaledRect({

function SuspenseRects({
suspenseID,
parentRects,
}: {
suspenseID: SuspenseNode['id'],
parentRects: null | Array<Rect>,
}): React$Node {
const store = useContext(StoreContext);
const treeDispatch = useContext(TreeDispatcherContext);
Expand Down Expand Up @@ -167,7 +170,21 @@ function SuspenseRects({
}
}

const boundingBox = getBoundingBox(suspense.rects);
const rects = suspense.rects;
const boundingBox = getBoundingBox(rects);

// Next we'll try to find a rect within one of our rects that isn't intersecting with
// other rects.
// TODO: This should probably be memoized based on if any changes to the rtree has been made.
const titleBox: null | Rect =
rects === null ? null : findTitleBox(store._rtree, rects, parentRects);

const nextRects =
rects === null || rects.length === 0
? parentRects
: parentRects === null || parentRects.length === 0
? rects
: parentRects.concat(rects);

return (
<ScaledRect
Expand Down Expand Up @@ -205,11 +222,22 @@ function SuspenseRects({
className={styles.SuspenseRectsBoundaryChildren}
rect={boundingBox}>
{suspense.children.map(childID => {
return <SuspenseRects key={childID} suspenseID={childID} />;
return (
<SuspenseRects
key={childID}
suspenseID={childID}
parentRects={nextRects}
/>
);
})}
</ScaledRect>
)}
{selected ? (
{titleBox && suspense.name && visible ? (
<ScaledRect className={styles.SuspenseRectsTitle} rect={titleBox}>
<span>{suspense.name}</span>
</ScaledRect>
) : null}
{selected && visible ? (
<ScaledRect
className={styles.SuspenseRectOutline}
rect={boundingBox}
Expand Down Expand Up @@ -320,6 +348,77 @@ function getDocumentBoundingRect(
};
}

function findTitleBox(
rtree: RBush<Rect>,
rects: Array<Rect>,
parentRects: null | Array<Rect>,
): null | Rect {
for (let i = 0; i < rects.length; i++) {
const rect = rects[i];
if (rect.width < 50 || rect.height < 10) {
// Skip small rects. They're likely not able to be contain anything useful anyway.
continue;
}
// Find all overlapping rects elsewhere in the tree to limit our rect.
const overlappingRects = rtree.search({
minX: rect.x,
minY: rect.y,
maxX: rect.x + rect.width,
maxY: rect.y + rect.height,
});
if (
overlappingRects.length === 0 ||
(overlappingRects.length === 1 && overlappingRects[0] === rect)
) {
// There are no overlapping rects that isn't our own rect, so we can just use
// the full space of the rect.
return rect;
}
// We have some overlapping rects but they might not overlap everything. Let's
// shrink it up toward the top left corner until it has no more overlap.
const minX = rect.x;
const minY = rect.y;
let maxX = rect.x + rect.width;
let maxY = rect.y + rect.height;
for (let j = 0; j < overlappingRects.length; j++) {
const overlappingRect = overlappingRects[j];
if (overlappingRect === rect) {
continue;
}
const x = overlappingRect.x;
const y = overlappingRect.y;
if (y < maxY && x < maxX) {
if (
parentRects !== null &&
parentRects.indexOf(overlappingRect) !== -1
) {
// This rect overlaps but it's part of a parent boundary. We let
// title content render if it's on top and not a sibling.
continue;
}
// This rect cuts into the remaining space. Let's figure out if we're
// better off cutting on the x or y axis to maximize remaining space.
const remainderX = x - minX;
const remainderY = y - minY;
if (remainderX > remainderY) {
maxX = x;
} else {
maxY = y;
}
}
}
if (maxX > minX && maxY > minY) {
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
}
return null;
}

function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {
const store = useContext(StoreContext);
const root = store.getSuspenseByID(rootID);
Expand All @@ -329,7 +428,9 @@ function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {
}

return root.children.map(childID => {
return <SuspenseRects key={childID} suspenseID={childID} />;
return (
<SuspenseRects key={childID} suspenseID={childID} parentRects={null} />
);
});
}

Expand Down
Loading
Loading