192 lines
6.1 KiB
TypeScript
192 lines
6.1 KiB
TypeScript
import { isTransparent } from "@excalidraw/common";
|
|
|
|
import {
|
|
shouldAllowVerticalAlign,
|
|
suppportsHorizontalAlign,
|
|
getColorTargetElement,
|
|
hasBoundTextElement,
|
|
isElbowArrow,
|
|
isImageElement,
|
|
isLinearElement,
|
|
isTextElement,
|
|
hasStrokeColor,
|
|
toolIsArrow,
|
|
} from "@excalidraw/element";
|
|
|
|
import type {
|
|
ElementsMap,
|
|
ExcalidrawElement,
|
|
ExcalidrawElementType,
|
|
NonDeletedElementsMap,
|
|
NonDeletedSceneElementsMap,
|
|
} from "@excalidraw/element/types";
|
|
|
|
import { alignActionsPredicate } from "../actions/actionAlign";
|
|
import {
|
|
canChangeRoundness,
|
|
canHaveArrowheads,
|
|
getSelectedElements,
|
|
hasBackground,
|
|
hasFillStyle,
|
|
hasFreedrawMode,
|
|
hasRoughness,
|
|
hasStrokeStyle,
|
|
hasStrokeWidth,
|
|
} from "../scene";
|
|
|
|
import type { ElementOrToolType } from "../types";
|
|
|
|
import type { AppClassProperties, UIAppState } from "../types";
|
|
|
|
export const canChangeStrokeColor = (
|
|
appState: UIAppState,
|
|
targetElements: ExcalidrawElement[],
|
|
) => {
|
|
let commonSelectedType: ExcalidrawElementType | null =
|
|
targetElements[0]?.type || null;
|
|
|
|
for (const element of targetElements) {
|
|
if (element.type !== commonSelectedType) {
|
|
commonSelectedType = null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return (
|
|
(hasStrokeColor(appState.activeTool.type) &&
|
|
commonSelectedType !== "image" &&
|
|
commonSelectedType !== "frame" &&
|
|
commonSelectedType !== "magicframe") ||
|
|
targetElements.some((element) => hasStrokeColor(element.type))
|
|
);
|
|
};
|
|
|
|
export const canChangeBackgroundColor = (
|
|
appState: UIAppState,
|
|
targetElements: ExcalidrawElement[],
|
|
elementsMap: ElementsMap,
|
|
) => {
|
|
return (
|
|
hasBackground(appState.activeTool.type) ||
|
|
// a note's label (the target while editing it) has no fill, but a
|
|
// background pick on it colors the note — so the picker stays available
|
|
targetElements.some((element) =>
|
|
hasBackground(
|
|
getColorTargetElement(element, "backgroundColor", elementsMap).type,
|
|
),
|
|
)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Single source of truth for "which shape-action controls are relevant right
|
|
* now". Each flag answers whether a given control should be shown, given the
|
|
* active tool and the current selection. All three styles-panel layouts
|
|
* (full / compact / mobile) and the compact popovers consume these flags so
|
|
* that visibility logic lives in one place and layout stays pure.
|
|
*/
|
|
export const getShapeActionPredicates = (
|
|
appState: UIAppState,
|
|
targetElements: ExcalidrawElement[],
|
|
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
|
|
app: AppClassProperties,
|
|
) => {
|
|
const activeToolType = appState.activeTool.type;
|
|
|
|
// A property is relevant when it applies to the active tool (so it can be
|
|
// preconfigured before drawing) or to any currently selected element.
|
|
const forToolOrSelection = (
|
|
predicate: (type: ElementOrToolType) => boolean,
|
|
) =>
|
|
predicate(activeToolType) ||
|
|
targetElements.some((element) => predicate(element.type));
|
|
|
|
const singleSelected = targetElements.length === 1;
|
|
|
|
// a container + its bound text read as a single logical element (2 selected)
|
|
const isSingleElementBoundContainer =
|
|
targetElements.length === 2 &&
|
|
(hasBoundTextElement(targetElements[0]) ||
|
|
hasBoundTextElement(targetElements[1]));
|
|
|
|
const isEditingTextOrNewElement = Boolean(
|
|
appState.editingTextElement || appState.newElement,
|
|
);
|
|
const hasSelection = targetElements.length > 0;
|
|
|
|
return {
|
|
/** some element(s) selected */
|
|
hasSelection,
|
|
/** actions on selected elements (delete/duplicate/...) */
|
|
showExtraActions: hasSelection && !isEditingTextOrNewElement,
|
|
|
|
// color
|
|
strokeColor: canChangeStrokeColor(appState, targetElements),
|
|
backgroundColor: canChangeBackgroundColor(
|
|
appState,
|
|
targetElements,
|
|
elementsMap,
|
|
),
|
|
fill:
|
|
// bucket fill never renders transparent (it falls back to a real
|
|
// color), so its fill style stays relevant either way
|
|
activeToolType === "bucketfill" ||
|
|
(hasFillStyle(activeToolType) &&
|
|
!isTransparent(appState.currentItemBackgroundColor)) ||
|
|
targetElements.some(
|
|
(element) =>
|
|
hasFillStyle(element.type) && !isTransparent(element.backgroundColor),
|
|
),
|
|
|
|
// stroke / shape properties
|
|
strokeWidth: forToolOrSelection(hasStrokeWidth),
|
|
freedrawMode: forToolOrSelection(hasFreedrawMode),
|
|
strokeStyle: forToolOrSelection(hasStrokeStyle),
|
|
sloppiness: forToolOrSelection(hasRoughness),
|
|
roundness: forToolOrSelection(canChangeRoundness),
|
|
arrowType: forToolOrSelection(toolIsArrow),
|
|
arrowheads: forToolOrSelection(canHaveArrowheads),
|
|
|
|
// text
|
|
text: activeToolType === "text" || targetElements.some(isTextElement),
|
|
textAlign:
|
|
activeToolType === "text" ||
|
|
suppportsHorizontalAlign(targetElements, elementsMap),
|
|
verticalAlign: shouldAllowVerticalAlign(targetElements, elementsMap),
|
|
|
|
opacity: activeToolType !== "autoshape" || hasSelection,
|
|
|
|
// arrangement
|
|
// z-order controls are hidden while the freedraw or drawShape tool is
|
|
// active without an actual selection
|
|
layers:
|
|
(activeToolType !== "freedraw" &&
|
|
activeToolType !== "autoshape" &&
|
|
!targetElements.some((element) => element.type === "freedraw")) ||
|
|
getSelectedElements(elementsMap, appState).some(
|
|
(element) => element.type === "freedraw",
|
|
),
|
|
align:
|
|
!isSingleElementBoundContainer && alignActionsPredicate(appState, app),
|
|
distribute: targetElements.length > 2,
|
|
|
|
// per-element actions
|
|
// NOTE: the full panel treats a bound container as linkable; the compact /
|
|
// mobile layouts only link a truly single selection. Both are preserved via
|
|
// the two flags below.
|
|
link: singleSelected || isSingleElementBoundContainer,
|
|
linkSingleOnly: singleSelected,
|
|
cropEditor:
|
|
!appState.croppingElementId &&
|
|
singleSelected &&
|
|
isImageElement(targetElements[0]),
|
|
lineEditor:
|
|
!appState.selectedLinearElement?.isEditing &&
|
|
singleSelected &&
|
|
isLinearElement(targetElements[0]) &&
|
|
!isElbowArrow(targetElements[0]),
|
|
};
|
|
};
|
|
|
|
export type ShapeActionPredicates = ReturnType<typeof getShapeActionPredicates>;
|