,
tablerIconProps,
);
+
+export const upIcon = createIcon(
+
+
+
+ ,
+ tablerIconProps,
+);
diff --git a/packages/excalidraw/components/main-menu/DefaultItems.tsx b/packages/excalidraw/components/main-menu/DefaultItems.tsx
index 26ef26000..bb3059db5 100644
--- a/packages/excalidraw/components/main-menu/DefaultItems.tsx
+++ b/packages/excalidraw/components/main-menu/DefaultItems.tsx
@@ -15,6 +15,7 @@ import {
LoadIcon,
MoonIcon,
save,
+ searchIcon,
SunIcon,
TrashIcon,
usersIcon,
@@ -27,6 +28,7 @@ import {
actionLoadScene,
actionSaveToActiveFile,
actionShortcuts,
+ actionToggleSearchMenu,
actionToggleTheme,
} from "../../actions";
import clsx from "clsx";
@@ -40,7 +42,6 @@ import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemConten
import { THEME } from "../../constants";
import type { Theme } from "../../element/types";
import { trackEvent } from "../../analytics";
-
import "./DefaultItems.scss";
export const LoadScene = () => {
@@ -145,6 +146,27 @@ export const CommandPalette = (opts?: { className?: string }) => {
};
CommandPalette.displayName = "CommandPalette";
+export const SearchMenu = (opts?: { className?: string }) => {
+ const { t } = useI18n();
+ const actionManager = useExcalidrawActionManager();
+
+ return (
+
{
+ actionManager.executeAction(actionToggleSearchMenu);
+ }}
+ shortcut={getShortcutFromShortcutName("searchMenu")}
+ aria-label={t("search.title")}
+ className={opts?.className}
+ >
+ {t("search.title")}
+
+ );
+};
+SearchMenu.displayName = "SearchMenu";
+
export const Help = () => {
const { t } = useI18n();
diff --git a/packages/excalidraw/constants.ts b/packages/excalidraw/constants.ts
index 9601807f6..31982d4fb 100644
--- a/packages/excalidraw/constants.ts
+++ b/packages/excalidraw/constants.ts
@@ -113,6 +113,7 @@ export const ENV = {
export const CLASSES = {
SHAPE_ACTIONS_MENU: "App-menu__left",
ZOOM_ACTIONS: "zoom-actions",
+ SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
};
/**
@@ -382,6 +383,10 @@ export const DEFAULT_SIDEBAR = {
defaultTab: LIBRARY_SIDEBAR_TAB,
} as const;
+export const SEARCH_SIDEBAR = {
+ name: "search",
+};
+
export const LIBRARY_DISABLED_TYPES = new Set([
"iframe",
"embeddable",
diff --git a/packages/excalidraw/css/theme.scss b/packages/excalidraw/css/theme.scss
index 0ed6a7544..69c28afda 100644
--- a/packages/excalidraw/css/theme.scss
+++ b/packages/excalidraw/css/theme.scss
@@ -144,9 +144,9 @@
--border-radius-md: 0.375rem;
--border-radius-lg: 0.5rem;
- --color-surface-high: hsl(244, 100%, 97%);
- --color-surface-mid: hsl(240 25% 96%);
- --color-surface-low: hsl(240 25% 94%);
+ --color-surface-high: #f1f0ff;
+ --color-surface-mid: #f2f2f7;
+ --color-surface-low: #ececf4;
--color-surface-lowest: #ffffff;
--color-on-surface: #1b1b1f;
--color-brand-hover: #5753d0;
diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts
index 9fb4766b6..9abebc356 100644
--- a/packages/excalidraw/element/textElement.ts
+++ b/packages/excalidraw/element/textElement.ts
@@ -284,16 +284,17 @@ export const measureText = (
text: string,
font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"],
+ forceAdvanceWidth?: true,
) => {
- text = text
+ const _text = text
.split("\n")
// replace empty lines with single space because leading/trailing empty
// lines would be stripped from computation
.map((x) => x || " ")
.join("\n");
const fontSize = parseFloat(font);
- const height = getTextHeight(text, fontSize, lineHeight);
- const width = getTextWidth(text, font);
+ const height = getTextHeight(_text, fontSize, lineHeight);
+ const width = getTextWidth(_text, font, forceAdvanceWidth);
return { width, height };
};
diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json
index ebf9ff872..ff1fa2026 100644
--- a/packages/excalidraw/locales/en.json
+++ b/packages/excalidraw/locales/en.json
@@ -162,6 +162,13 @@
"hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.",
"hint_emptyPrivateLibrary": "Select an item on canvas to add it here."
},
+ "search": {
+ "title": "Find on canvas",
+ "noMatch": "No matches found...",
+ "singleResult": "result",
+ "multipleResults": "results",
+ "placeholder": "Find text..."
+ },
"buttons": {
"clearReset": "Reset the canvas",
"exportJSON": "Export to file",
@@ -297,6 +304,7 @@
"shapes": "Shapes"
},
"hints": {
+ "dismissSearch": "Escape to dismiss search",
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
"linearElement": "Click to start multiple points, drag for single line",
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts
index 5a27a3312..0d03b0f5a 100644
--- a/packages/excalidraw/renderer/interactiveScene.ts
+++ b/packages/excalidraw/renderer/interactiveScene.ts
@@ -30,8 +30,12 @@ import {
shouldShowBoundingBox,
} from "../element/transformHandles";
import { arrayToMap, throttleRAF } from "../utils";
-import type { InteractiveCanvasAppState } from "../types";
-import { DEFAULT_TRANSFORM_HANDLE_SPACING, FRAME_STYLE } from "../constants";
+import {
+ DEFAULT_TRANSFORM_HANDLE_SPACING,
+ FRAME_STYLE,
+ THEME,
+} from "../constants";
+import { type InteractiveCanvasAppState } from "../types";
import { renderSnaps } from "../renderer/renderSnaps";
@@ -952,9 +956,48 @@ const _renderInteractiveScene = ({
context.restore();
}
+ appState.searchMatches.forEach(({ id, focus, matchedLines }) => {
+ const element = elementsMap.get(id);
+
+ if (element && isTextElement(element)) {
+ const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords(
+ element,
+ elementsMap,
+ true,
+ );
+
+ context.save();
+ if (appState.theme === THEME.LIGHT) {
+ if (focus) {
+ context.fillStyle = "rgba(255, 124, 0, 0.4)";
+ } else {
+ context.fillStyle = "rgba(255, 226, 0, 0.4)";
+ }
+ } else if (focus) {
+ context.fillStyle = "rgba(229, 82, 0, 0.4)";
+ } else {
+ context.fillStyle = "rgba(99, 52, 0, 0.4)";
+ }
+
+ context.translate(appState.scrollX, appState.scrollY);
+ context.translate(cx, cy);
+ context.rotate(element.angle);
+
+ matchedLines.forEach((matchedLine) => {
+ context.fillRect(
+ elementX1 + matchedLine.offsetX - cx,
+ elementY1 + matchedLine.offsetY - cy,
+ matchedLine.width,
+ matchedLine.height,
+ );
+ });
+
+ context.restore();
+ }
+ });
+
renderSnaps(context, appState);
- // Reset zoom
context.restore();
renderRemoteCursors({
diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
index 7f9904a4d..3a5e14065 100644
--- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
@@ -866,6 +866,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -1068,6 +1069,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -1283,6 +1285,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -1613,6 +1616,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -1943,6 +1947,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -2158,6 +2163,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
@@ -2397,6 +2403,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0_copy": true,
},
@@ -2699,6 +2706,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -3065,6 +3073,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -3539,6 +3548,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id1": true,
},
@@ -3861,6 +3871,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id1": true,
},
@@ -4185,6 +4196,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -5370,6 +5382,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -6496,6 +6509,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -7431,6 +7445,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
@@ -8339,6 +8354,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -9235,6 +9251,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id1": true,
},
diff --git a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap
index 2994cfc3e..e5e431dfc 100644
--- a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap
@@ -239,6 +239,55 @@ exports[`
> Test UIOptions prop > Test canvasActions > should rende
Ctrl+Shift+E