Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -2,7 +2,7 @@
padding: 0.25rem;
}

.CallSite {
.CallSite, .BuiltInCallSite {
display: block;
padding-left: 1rem;
}
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 Expand Up @@ -75,7 +93,7 @@
transition: background-color 0.2s ease-out;
}

.SuspenseRectsBoundary[data-selected='true'] {
.SuspenseRectsBoundary[data-selected='true'][data-visible='true'] {
box-shadow: var(--elevation-4);
}

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,20 @@ 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 +221,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 +347,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 < 20 || 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 +427,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