(
): boolean =>
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
+// TODO make pointInsideBounds inclusive and remove this function once we
+// test nothing is breaking
+export const pointInsideBoundsInclusive =
(
+ p: P,
+ bounds: Bounds,
+): boolean =>
+ p[0] >= bounds[0] &&
+ p[0] <= bounds[2] &&
+ p[1] >= bounds[1] &&
+ p[1] <= bounds[3];
+
export const doBoundsIntersect = (
bounds1: Bounds | null,
bounds2: Bounds | null,
@@ -1275,13 +1287,21 @@ export const doBoundsIntersect = (
return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
};
+export const boundsContainBounds = (outerBounds: Bounds, innerBounds: Bounds) =>
+ [
+ pointFrom(innerBounds[0], innerBounds[1]),
+ pointFrom(innerBounds[0], innerBounds[3]),
+ pointFrom(innerBounds[2], innerBounds[1]),
+ pointFrom(innerBounds[2], innerBounds[3]),
+ ].every((point) => pointInsideBoundsInclusive(point, outerBounds));
+
export const elementCenterPoint = (
element: ExcalidrawElement,
elementsMap: ElementsMap,
xOffset: number = 0,
yOffset: number = 0,
) => {
- if (isLinearElement(element)) {
+ if (isLinearElement(element) || isFreeDrawElement(element)) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const [x, y] = pointFrom((x1 + x2) / 2, (y1 + y2) / 2);
diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts
index b17d563dbe..c260ae5267 100644
--- a/packages/element/src/collision.ts
+++ b/packages/element/src/collision.ts
@@ -154,14 +154,11 @@ export const hitElementItself = ({
// Hit test against the extended, rotated bounding box of the element first
const bounds = getElementBounds(element, elementsMap, true);
- const hitBounds = isPointWithinBounds(
- pointFrom(bounds[0] - threshold, bounds[1] - threshold),
- pointRotateRads(
- point,
- getCenterForBounds(bounds),
- -element.angle as Radians,
- ),
- pointFrom(bounds[2] + threshold, bounds[3] + threshold),
+ const hitBounds = isPointInRotatedBounds(
+ point,
+ bounds,
+ element.angle,
+ threshold,
);
// PERF: Bail out early if the point is not even in the
@@ -192,18 +189,32 @@ export const hitElementItself = ({
return result;
};
+const isPointInRotatedBounds = (
+ point: GlobalPoint,
+ bounds: Bounds,
+ angle: Radians,
+ tolerance = 0,
+) => {
+ const adjustedPoint =
+ angle === 0
+ ? point
+ : pointRotateRads(point, getCenterForBounds(bounds), -angle as Radians);
+
+ return isPointWithinBounds(
+ pointFrom(bounds[0] - tolerance, bounds[1] - tolerance),
+ adjustedPoint,
+ pointFrom(bounds[2] + tolerance, bounds[3] + tolerance),
+ );
+};
+
export const hitElementBoundingBox = (
point: GlobalPoint,
element: ExcalidrawElement,
elementsMap: ElementsMap,
tolerance = 0,
) => {
- let [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
- x1 -= tolerance;
- y1 -= tolerance;
- x2 += tolerance;
- y2 += tolerance;
- return isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2));
+ const bounds = getElementBounds(element, elementsMap, true);
+ return isPointInRotatedBounds(point, bounds, element.angle, tolerance);
};
export const hitElementBoundingBoxOnly = (
@@ -573,7 +584,9 @@ const intersectLinearOrFreeDrawWithLineSegment = (
continue;
}
- const hits = curveIntersectLineSegment(c, segment);
+ const hits = curveIntersectLineSegment(c, segment, {
+ iterLimit: 10,
+ });
if (hits.length > 0) {
intersections.push(...hits);
diff --git a/packages/element/src/frame.ts b/packages/element/src/frame.ts
index 3c82099546..787c2692f9 100644
--- a/packages/element/src/frame.ts
+++ b/packages/element/src/frame.ts
@@ -1,7 +1,6 @@
import { arrayToMap } from "@excalidraw/common";
import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
-import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
import type {
AppClassProperties,
@@ -18,6 +17,8 @@ import {
getElementLineSegments,
getCommonBounds,
getElementAbsoluteCoords,
+ doBoundsIntersect,
+ getElementBounds,
} from "./bounds";
import { mutateElement } from "./mutateElement";
import { getBoundTextElement, getContainerElement } from "./textElement";
@@ -920,16 +921,17 @@ export const getFrameLikeTitle = (element: ExcalidrawFrameLikeElement) => {
export const getElementsOverlappingFrame = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
+ elementsMap: ElementsMap,
) => {
- return (
- elementsOverlappingBBox({
- elements,
- bounds: frame,
- type: "overlap",
- })
- // removes elements who are overlapping, but are in a different frame,
+ return elements.filter(
+ (el) =>
+ // exclude elements which are overlapping, but are in a different frame,
// and thus invisible in target frame
- .filter((el) => !el.frameId || el.frameId === frame.id)
+ (!el.frameId || el.frameId === frame.id) &&
+ doBoundsIntersect(
+ getElementBounds(el, elementsMap),
+ getElementBounds(frame, elementsMap),
+ ),
);
};
diff --git a/packages/element/src/selection.ts b/packages/element/src/selection.ts
index ea7fdb1b77..8e9c4e8086 100644
--- a/packages/element/src/selection.ts
+++ b/packages/element/src/selection.ts
@@ -1,15 +1,32 @@
-import { arrayToMap, isShallowEqual } from "@excalidraw/common";
+import { arrayToMap, isShallowEqual, type Bounds } from "@excalidraw/common";
+import {
+ lineSegment,
+ pointFrom,
+ pointRotateRads,
+ type GlobalPoint,
+} from "@excalidraw/math";
import type {
AppState,
+ BoxSelectionMode,
InteractiveCanvasAppState,
} from "@excalidraw/excalidraw/types";
-import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
+import {
+ boundsContainBounds,
+ doBoundsIntersect,
+ elementCenterPoint,
+ getElementAbsoluteCoords,
+ getElementBounds,
+ pointInsideBounds,
+} from "./bounds";
+import { intersectElementWithLineSegment } from "./collision";
import { isElementInViewport } from "./sizeHelpers";
import {
+ isArrowElement,
isBoundToContainer,
isFrameLikeElement,
+ isFreeDrawElement,
isLinearElement,
isTextElement,
} from "./typeChecks";
@@ -17,19 +34,38 @@ import {
elementOverlapsWithFrame,
getContainingFrame,
getFrameChildren,
+ isElementIntersectingFrame,
} from "./frame";
import { LinearElementEditor } from "./linearElementEditor";
import { selectGroupsForSelectedElements } from "./groups";
+import { getBoundTextElement } from "./textElement";
import type {
ElementsMap,
ElementsMapOrArray,
ExcalidrawElement,
+ ExcalidrawFrameLikeElement,
NonDeleted,
NonDeletedExcalidrawElement,
} from "./types";
+const shouldIgnoreElementFromSelection = (
+ element: NonDeletedExcalidrawElement,
+) => element.locked || isBoundToContainer(element);
+
+const excludeElementsFromFrames = (
+ selectedElements: readonly T[],
+ framesInSelection: Set,
+) => {
+ return selectedElements.filter((element) => {
+ if (element.frameId && framesInSelection.has(element.frameId)) {
+ return false;
+ }
+ return true;
+ });
+};
+
/**
* Frames and their containing elements are not to be selected at the same time.
* Given an array of selected elements, if there are frames and their containing elements
@@ -49,55 +85,243 @@ export const excludeElementsInFramesFromSelection = <
}
});
- return selectedElements.filter((element) => {
- if (element.frameId && framesInSelection.has(element.frameId)) {
- return false;
- }
- return true;
- });
+ return excludeElementsFromFrames(selectedElements, framesInSelection);
};
export const getElementsWithinSelection = (
elements: readonly NonDeletedExcalidrawElement[],
selection: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
+ // TODO remove (this flag is effectively unused AFAIK)
excludeElementsInFrames: boolean = true,
-) => {
- const [selectionX1, selectionY1, selectionX2, selectionY2] =
+ boxSelectionMode: BoxSelectionMode = "contain",
+): NonDeletedExcalidrawElement[] => {
+ const [selectionStartX, selectionStartY, selectionEndX, selectionEndY] =
getElementAbsoluteCoords(selection, elementsMap);
+ const selectionX1 = Math.min(selectionStartX, selectionEndX);
+ const selectionY1 = Math.min(selectionStartY, selectionEndY);
+ const selectionX2 = Math.max(selectionStartX, selectionEndX);
+ const selectionY2 = Math.max(selectionStartY, selectionEndY);
+ const selectionBounds = [
+ selectionX1,
+ selectionY1,
+ selectionX2,
+ selectionY2,
+ ] as Bounds;
+ const selectionEdges = [
+ lineSegment(
+ pointFrom(selectionX1, selectionY1),
+ pointFrom(selectionX2, selectionY1),
+ ),
+ lineSegment(
+ pointFrom(selectionX2, selectionY1),
+ pointFrom(selectionX2, selectionY2),
+ ),
+ lineSegment(
+ pointFrom(selectionX2, selectionY2),
+ pointFrom(selectionX1, selectionY2),
+ ),
+ lineSegment(
+ pointFrom(selectionX1, selectionY2),
+ pointFrom(selectionX1, selectionY1),
+ ),
+ ];
- let elementsInSelection = elements.filter((element) => {
- let [elementX1, elementY1, elementX2, elementY2] = getElementBounds(
- element,
- elementsMap,
- );
+ const framesInSelection = excludeElementsInFrames
+ ? new Set()
+ : null;
+ let elementsInSelection: NonDeletedExcalidrawElement[] = [];
- const containingFrame = getContainingFrame(element, elementsMap);
- if (containingFrame) {
- const [fx1, fy1, fx2, fy2] = getElementBounds(
- containingFrame,
- elementsMap,
- );
-
- elementX1 = Math.max(fx1, elementX1);
- elementY1 = Math.max(fy1, elementY1);
- elementX2 = Math.min(fx2, elementX2);
- elementY2 = Math.min(fy2, elementY2);
+ for (const element of elements) {
+ if (shouldIgnoreElementFromSelection(element)) {
+ continue;
}
- return (
- element.locked === false &&
- element.type !== "selection" &&
- !isBoundToContainer(element) &&
- selectionX1 <= elementX1 &&
- selectionY1 <= elementY1 &&
- selectionX2 >= elementX2 &&
- selectionY2 >= elementY2
- );
- });
+ const strokeWidth = element.strokeWidth;
+ let labelAABB: Bounds | null = null;
+ let elementAABB = getElementBounds(element, elementsMap);
- elementsInSelection = excludeElementsInFrames
- ? excludeElementsInFramesFromSelection(elementsInSelection)
+ elementAABB = [
+ elementAABB[0] - strokeWidth / 2,
+ elementAABB[1] - strokeWidth / 2,
+ elementAABB[2] + strokeWidth / 2,
+ elementAABB[3] + strokeWidth / 2,
+ ] as Bounds;
+
+ // Whether the element bounds should include the bound text element bounds
+ const boundTextElement =
+ isArrowElement(element) && getBoundTextElement(element, elementsMap);
+ if (boundTextElement) {
+ const { x, y } = LinearElementEditor.getBoundTextElementPosition(
+ element,
+ boundTextElement,
+ elementsMap,
+ );
+ labelAABB = [
+ x,
+ y,
+ x + boundTextElement.width,
+ y + boundTextElement.height,
+ ] as Bounds;
+ }
+
+ // Clip element bounds by its containing frame (if any), since only the
+ // visible (frame-clipped) portion of the element is relevant for selection.
+ const associatedFrame = getContainingFrame(element, elementsMap);
+ if (
+ associatedFrame &&
+ isElementIntersectingFrame(element, associatedFrame, elementsMap)
+ ) {
+ const frameAABB = getElementBounds(associatedFrame, elementsMap);
+ elementAABB = [
+ Math.max(elementAABB[0], frameAABB[0]),
+ Math.max(elementAABB[1], frameAABB[1]),
+ Math.min(elementAABB[2], frameAABB[2]),
+ Math.min(elementAABB[3], frameAABB[3]),
+ ] as Bounds;
+
+ labelAABB = labelAABB
+ ? ([
+ Math.max(labelAABB[0], frameAABB[0]),
+ Math.max(labelAABB[1], frameAABB[1]),
+ Math.min(labelAABB[2], frameAABB[2]),
+ Math.min(labelAABB[3], frameAABB[3]),
+ ] as Bounds)
+ : null;
+ }
+
+ const commonAABB = labelAABB
+ ? ([
+ Math.min(labelAABB[0], elementAABB[0]),
+ Math.min(labelAABB[1], elementAABB[1]),
+ Math.max(labelAABB[2], elementAABB[2]),
+ Math.max(labelAABB[3], elementAABB[3]),
+ ] as Bounds)
+ : elementAABB;
+
+ // ============== Evaluation ==============
+
+ // 1. If the selection box WRAPs the element's AABB, then add it to the
+ // selection and move on, regardless of the selection mode.
+ //
+ // PERF: This trick only works with axis-aligned box selection and the
+ // current convex element shapes!
+ if (boundsContainBounds(selectionBounds, commonAABB)) {
+ if (framesInSelection && isFrameLikeElement(element)) {
+ framesInSelection.add(element.id);
+ } else {
+ elementsInSelection.push(element);
+ continue;
+ }
+ }
+
+ // 2. Handle the case where the label is overlapped by the selection box
+ if (
+ boxSelectionMode === "overlap" &&
+ labelAABB &&
+ doBoundsIntersect(selectionBounds, labelAABB)
+ ) {
+ elementsInSelection.push(element);
+ continue;
+ }
+
+ // 3. Handle the case where the selection is not wrapping the element, but
+ // it does intersect the element's outline (non-AABB).
+ if (
+ boxSelectionMode === "overlap" &&
+ doBoundsIntersect(selectionBounds, elementAABB)
+ ) {
+ let hasIntersection = false;
+
+ // Preliminary check potential intersection imprecision
+ if (isLinearElement(element) || isFreeDrawElement(element)) {
+ const center = elementCenterPoint(element, elementsMap);
+ hasIntersection = element.points.some((point) => {
+ const rotatedPoint = pointRotateRads(
+ pointFrom(element.x + point[0], element.y + point[1]),
+ center,
+ element.angle,
+ );
+
+ return pointInsideBounds(rotatedPoint, selectionBounds);
+ });
+ } else {
+ const nonRotatedElementBounds = getElementBounds(
+ element,
+ elementsMap,
+ true,
+ );
+ const center = elementCenterPoint(element, elementsMap);
+ hasIntersection = [
+ pointRotateRads(
+ pointFrom(
+ (nonRotatedElementBounds[0] + nonRotatedElementBounds[2]) / 2,
+ nonRotatedElementBounds[1],
+ ),
+ center,
+ element.angle,
+ ),
+ pointRotateRads(
+ pointFrom(
+ nonRotatedElementBounds[2],
+ (nonRotatedElementBounds[1] + nonRotatedElementBounds[3]) / 2,
+ ),
+ center,
+ element.angle,
+ ),
+ pointRotateRads(
+ pointFrom(
+ (nonRotatedElementBounds[0] + nonRotatedElementBounds[2]) / 2,
+ nonRotatedElementBounds[3],
+ ),
+ center,
+ element.angle,
+ ),
+ pointRotateRads(
+ pointFrom(
+ nonRotatedElementBounds[0],
+ (nonRotatedElementBounds[1] + nonRotatedElementBounds[3]) / 2,
+ ),
+ center,
+ element.angle,
+ ),
+ ].some((point) => {
+ return pointInsideBounds(
+ pointRotateRads(point, center, element.angle),
+ selectionBounds,
+ );
+ });
+ }
+
+ if (!hasIntersection) {
+ hasIntersection = selectionEdges.some(
+ (selectionEdge) =>
+ intersectElementWithLineSegment(
+ element,
+ elementsMap,
+ selectionEdge,
+ strokeWidth / 2,
+ true, // Stop at first hit for better performance
+ ).length > 0,
+ );
+ }
+
+ if (hasIntersection) {
+ if (framesInSelection && isFrameLikeElement(element)) {
+ framesInSelection.add(element.id);
+ }
+
+ elementsInSelection.push(element);
+ continue;
+ }
+ }
+
+ // 4. We don't need to handle when the selection is inside the element
+ // as it is separately handled in App.
+ }
+
+ elementsInSelection = framesInSelection
+ ? excludeElementsFromFrames(elementsInSelection, framesInSelection)
: elementsInSelection;
elementsInSelection = elementsInSelection.filter((element) => {
diff --git a/packages/element/tests/collision.test.tsx b/packages/element/tests/collision.test.tsx
index 4061a16cb6..a44f1f7bb0 100644
--- a/packages/element/tests/collision.test.tsx
+++ b/packages/element/tests/collision.test.tsx
@@ -1,4 +1,4 @@
-import { arrayToMap } from "@excalidraw/common";
+import { arrayToMap, reseed } from "@excalidraw/common";
import { type GlobalPoint, type LocalPoint, pointFrom } from "@excalidraw/math";
import { Excalidraw } from "@excalidraw/excalidraw";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
@@ -12,6 +12,7 @@ import { hitElementItself } from "../src/collision";
describe("check rotated elements can be hit:", () => {
beforeEach(async () => {
localStorage.clear();
+ reseed(7);
await render();
});
@@ -56,6 +57,7 @@ describe("hitElementItself cache", () => {
});
localStorage.clear();
+ reseed(7);
await render();
});
diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts
index e51865b2ea..93fe770286 100644
--- a/packages/excalidraw/appState.ts
+++ b/packages/excalidraw/appState.ts
@@ -128,6 +128,7 @@ export const getDefaultAppState = (): Omit<
lockedMultiSelections: {},
activeLockedId: null,
bindMode: "orbit",
+ boxSelectionMode: "contain",
};
};
@@ -193,6 +194,7 @@ const APP_STATE_STORAGE_CONF = (<
gridModeEnabled: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: true, export: false, server: false },
+ boxSelectionMode: { browser: true, export: false, server: false },
bindingPreference: { browser: true, export: false, server: false },
isMidpointSnappingEnabled: { browser: true, export: false, server: false },
defaultSidebarDockedPreference: {
diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx
index 5561594cb0..4db823c2d4 100644
--- a/packages/excalidraw/components/App.tsx
+++ b/packages/excalidraw/components/App.tsx
@@ -27,6 +27,7 @@ import {
KEYS,
APP_NAME,
CURSOR_TYPE,
+ DEFAULT_TRANSFORM_HANDLE_SPACING,
DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT,
DEFAULT_VERTICAL_ALIGN,
DRAGGING_THRESHOLD,
@@ -2524,6 +2525,7 @@ class App extends React.Component {
const magicFrameChildren = getElementsOverlappingFrame(
this.scene.getNonDeletedElements(),
magicFrame,
+ this.scene.getNonDeletedElementsMap(),
).filter((el) => !isMagicFrameElement(el));
if (!magicFrameChildren.length) {
@@ -7239,6 +7241,14 @@ class App extends React.Component {
this.interactiveCanvas,
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
);
+ } else if (
+ !event[KEYS.CTRL_OR_CMD] &&
+ this.isHittingCommonBoundingBoxOfSelectedElements(
+ scenePointer,
+ selectedElements,
+ )
+ ) {
+ setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
} else if (this.state.viewModeEnabled) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
} else if (this.state.openDialog?.name === "elementLinkSelector") {
@@ -7730,17 +7740,24 @@ class App extends React.Component {
const hitSelectedElement =
pointerDownState.hit.element &&
this.isASelectedElement(pointerDownState.hit.element);
+ const shouldForceLassoReselect =
+ event.altKey &&
+ event[KEYS.CTRL_OR_CMD] &&
+ !pointerDownState.resize.handleType;
+ const shouldStartLassoSelection =
+ shouldForceLassoReselect ||
+ (!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements &&
+ !pointerDownState.resize.handleType &&
+ !hitSelectedElement);
- if (
- !pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements &&
- !pointerDownState.resize.handleType &&
- !hitSelectedElement
- ) {
- this.lassoTrail.startPath(
- pointerDownState.origin.x,
- pointerDownState.origin.y,
- event.shiftKey,
- );
+ if (shouldStartLassoSelection) {
+ if (!this.lassoTrail.hasCurrentTrail) {
+ this.lassoTrail.startPath(
+ pointerDownState.origin.x,
+ pointerDownState.origin.y,
+ event.shiftKey,
+ );
+ }
// block dragging after lasso selection on PCs until the next pointer down
// (on mobile or tablet, we want to allow user to drag immediately)
@@ -8729,12 +8746,14 @@ class App extends React.Component {
DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value,
1,
);
+ const boundsPadding =
+ (DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / this.state.zoom.value;
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
return (
- point.x > x1 - threshold &&
- point.x < x2 + threshold &&
- point.y > y1 - threshold &&
- point.y < y2 + threshold
+ point.x > x1 - boundsPadding - threshold &&
+ point.x < x2 + boundsPadding + threshold &&
+ point.y > y1 - boundsPadding - threshold &&
+ point.y < y2 + boundsPadding + threshold
);
}
@@ -10267,6 +10286,7 @@ class App extends React.Component {
this.state.selectionElement,
this.scene.getNonDeletedElementsMap(),
false,
+ this.state.boxSelectionMode,
)
: [];
diff --git a/packages/excalidraw/components/RadioGroup.scss b/packages/excalidraw/components/RadioGroup.scss
index 28ddc8889b..d550d95b3b 100644
--- a/packages/excalidraw/components/RadioGroup.scss
+++ b/packages/excalidraw/components/RadioGroup.scss
@@ -26,13 +26,16 @@
background: var(--RadioGroup-background);
border: 1px solid var(--RadioGroup-border);
+ gap: 2px;
+
&__choice {
position: relative;
display: flex;
align-items: center;
justify-content: center;
- width: 32px;
+ min-width: 20px;
height: 24px;
+ padding: 0 0.375rem;
color: var(--RadioGroup-choice-color-off);
background: var(--RadioGroup-choice-background-off);
diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx
index 283bdb40d3..24d739afa9 100644
--- a/packages/excalidraw/components/Stats/stats.test.tsx
+++ b/packages/excalidraw/components/Stats/stats.test.tsx
@@ -750,7 +750,7 @@ describe("frame resizing behavior", () => {
x: 0,
y: 0,
width: 100,
- height: 100,
+ height: 103,
});
// Create a rectangle outside the frame
diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss b/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss
index e207203d60..3080314250 100644
--- a/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss
+++ b/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss
@@ -2,7 +2,7 @@
.excalidraw {
.dropdown-menu {
- max-width: 16rem;
+ max-width: 20rem;
z-index: 1;
&--placement-top {
diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx
index 4f2986c30e..63646745fb 100644
--- a/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx
+++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx
@@ -1,4 +1,5 @@
import { useEditorInterface } from "../App";
+import { Ellipsify } from "../Ellipsify";
import { RadioGroup } from "../RadioGroup";
type Props = {
@@ -12,6 +13,7 @@ type Props = {
onChange: (value: T) => void;
children: React.ReactNode;
name: string;
+ icon?: React.ReactNode;
};
const DropdownMenuItemContentRadio = ({
@@ -21,13 +23,17 @@ const DropdownMenuItemContentRadio = ({
choices,
children,
name,
+ icon,
}: Props) => {
const editorInterface = useEditorInterface();
return (
<>