Initial implementation of containerBehavior.margin

This commit is contained in:
zsviczian
2025-11-01 11:48:26 +00:00
parent 0ff0efe2a7
commit d668eaa060
12 changed files with 312 additions and 91 deletions

View File

@@ -329,16 +329,24 @@ const generateElementCanvas = (
boundTextCanvasContext.translate(-shiftX, -shiftY); boundTextCanvasContext.translate(-shiftX, -shiftY);
// Clear the bound text area // Clear the bound text area
boundTextCanvasContext.clearRect( boundTextCanvasContext.clearRect(
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) * -(
boundTextElement.width / 2 +
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING)
) *
window.devicePixelRatio * window.devicePixelRatio *
scale, scale,
-(boundTextElement.height / 2 + BOUND_TEXT_PADDING) * -(
boundTextElement.height / 2 +
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING)
) *
window.devicePixelRatio * window.devicePixelRatio *
scale, scale,
(boundTextElement.width + BOUND_TEXT_PADDING * 2) * (boundTextElement.width +
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING) * 2) *
window.devicePixelRatio * window.devicePixelRatio *
scale, scale,
(boundTextElement.height + BOUND_TEXT_PADDING * 2) * (boundTextElement.height +
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING) * 2) *
window.devicePixelRatio * window.devicePixelRatio *
scale, scale,
); );

View File

@@ -12,6 +12,7 @@ import {
SHIFT_LOCKING_ANGLE, SHIFT_LOCKING_ANGLE,
rescalePoints, rescalePoints,
getFontString, getFontString,
BOUND_TEXT_PADDING,
} from "@excalidraw/common"; } from "@excalidraw/common";
import type { GlobalPoint } from "@excalidraw/math"; import type { GlobalPoint } from "@excalidraw/math";
@@ -741,10 +742,12 @@ export const resizeSingleElement = (
const minWidth = getApproxMinLineWidth( const minWidth = getApproxMinLineWidth(
getFontString(boundTextElement), getFontString(boundTextElement),
boundTextElement.lineHeight, boundTextElement.lineHeight,
latestElement.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
); );
const minHeight = getApproxMinLineHeight( const minHeight = getApproxMinLineHeight(
boundTextElement.fontSize, boundTextElement.fontSize,
boundTextElement.lineHeight, boundTextElement.lineHeight,
latestElement.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
); );
nextWidth = Math.max(nextWidth, minWidth); nextWidth = Math.max(nextWidth, minWidth);
nextHeight = Math.max(nextHeight, minHeight); nextHeight = Math.max(nextHeight, minHeight);

View File

@@ -108,6 +108,7 @@ export const redrawTextBoundingBox = (
const nextHeight = computeContainerDimensionForBoundText( const nextHeight = computeContainerDimensionForBoundText(
metrics.height, metrics.height,
container.type, container.type,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
); );
scene.mutateElement(container, { height: nextHeight }); scene.mutateElement(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight); updateOriginalContainerCache(container.id, nextHeight);
@@ -117,6 +118,7 @@ export const redrawTextBoundingBox = (
const nextWidth = computeContainerDimensionForBoundText( const nextWidth = computeContainerDimensionForBoundText(
metrics.width, metrics.width,
container.type, container.type,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
); );
scene.mutateElement(container, { width: nextWidth }); scene.mutateElement(container, { width: nextWidth });
} }
@@ -187,6 +189,7 @@ export const handleBindTextResize = (
containerHeight = computeContainerDimensionForBoundText( containerHeight = computeContainerDimensionForBoundText(
nextHeight, nextHeight,
container.type, container.type,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
); );
const diff = containerHeight - container.height; const diff = containerHeight - container.height;
@@ -353,8 +356,8 @@ export const getContainerCenter = (
}; };
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => { export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
let offsetX = BOUND_TEXT_PADDING; let offsetX = container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
let offsetY = BOUND_TEXT_PADDING; let offsetY = container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
if (container.type === "ellipse") { if (container.type === "ellipse") {
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172 // The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
@@ -446,9 +449,10 @@ export const isValidTextContainer = (element: {
export const computeContainerDimensionForBoundText = ( export const computeContainerDimensionForBoundText = (
dimension: number, dimension: number,
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>, containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
boundTextPadding: number,
) => { ) => {
dimension = Math.ceil(dimension); dimension = Math.ceil(dimension);
const padding = BOUND_TEXT_PADDING * 2; const padding = boundTextPadding * 2;
if (containerType === "ellipse") { if (containerType === "ellipse") {
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2); return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
@@ -467,6 +471,8 @@ export const getBoundTextMaxWidth = (
boundTextElement: ExcalidrawTextElement | null, boundTextElement: ExcalidrawTextElement | null,
) => { ) => {
const { width } = container; const { width } = container;
const boundTextPadding =
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
if (isArrowElement(container)) { if (isArrowElement(container)) {
const minWidth = const minWidth =
(boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) * (boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) *
@@ -477,14 +483,14 @@ export const getBoundTextMaxWidth = (
// The width of the largest rectangle inscribed inside an ellipse is // The width of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from // Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172 // equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; return Math.round((width / 2) * Math.sqrt(2)) - boundTextPadding * 2;
} }
if (container.type === "diamond") { if (container.type === "diamond") {
// The width of the largest rectangle inscribed inside a rhombus is // The width of the largest rectangle inscribed inside a rhombus is
// Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265 // Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
return Math.round(width / 2) - BOUND_TEXT_PADDING * 2; return Math.round(width / 2) - boundTextPadding * 2;
} }
return width - BOUND_TEXT_PADDING * 2; return width - boundTextPadding * 2;
}; };
export const getBoundTextMaxHeight = ( export const getBoundTextMaxHeight = (
@@ -492,8 +498,10 @@ export const getBoundTextMaxHeight = (
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
) => { ) => {
const { height } = container; const { height } = container;
const boundTextPadding =
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
if (isArrowElement(container)) { if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; const containerHeight = height - boundTextPadding * 8 * 2;
if (containerHeight <= 0) { if (containerHeight <= 0) {
return boundTextElement.height; return boundTextElement.height;
} }
@@ -503,14 +511,14 @@ export const getBoundTextMaxHeight = (
// The height of the largest rectangle inscribed inside an ellipse is // The height of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from // Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172 // equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; return Math.round((height / 2) * Math.sqrt(2)) - boundTextPadding * 2;
} }
if (container.type === "diamond") { if (container.type === "diamond") {
// The height of the largest rectangle inscribed inside a rhombus is // The height of the largest rectangle inscribed inside a rhombus is
// Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265 // Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
return Math.round(height / 2) - BOUND_TEXT_PADDING * 2; return Math.round(height / 2) - boundTextPadding * 2;
} }
return height - BOUND_TEXT_PADDING * 2; return height - boundTextPadding * 2;
}; };
/** retrieves text from text elements and concatenates to a single string */ /** retrieves text from text elements and concatenates to a single string */

View File

@@ -32,22 +32,24 @@ const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
export const getApproxMinLineWidth = ( export const getApproxMinLineWidth = (
font: FontString, font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"], lineHeight: ExcalidrawTextElement["lineHeight"],
boundTextPadding: number = BOUND_TEXT_PADDING,
) => { ) => {
const maxCharWidth = getMaxCharWidth(font); const maxCharWidth = getMaxCharWidth(font);
if (maxCharWidth === 0) { if (maxCharWidth === 0) {
return ( return (
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width + measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
BOUND_TEXT_PADDING * 2 boundTextPadding * 2
); );
} }
return maxCharWidth + BOUND_TEXT_PADDING * 2; return maxCharWidth + boundTextPadding * 2;
}; };
export const getMinTextElementWidth = ( export const getMinTextElementWidth = (
font: FontString, font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"], lineHeight: ExcalidrawTextElement["lineHeight"],
boundTextPadding: number = BOUND_TEXT_PADDING,
) => { ) => {
return measureText("", font, lineHeight).width + BOUND_TEXT_PADDING * 2; return measureText("", font, lineHeight).width + boundTextPadding * 2;
}; };
export const isMeasureTextSupported = () => { export const isMeasureTextSupported = () => {
@@ -99,8 +101,9 @@ export const getLineHeightInPx = (
export const getApproxMinLineHeight = ( export const getApproxMinLineHeight = (
fontSize: ExcalidrawTextElement["fontSize"], fontSize: ExcalidrawTextElement["fontSize"],
lineHeight: ExcalidrawTextElement["lineHeight"], lineHeight: ExcalidrawTextElement["lineHeight"],
boundTextPadding: number = BOUND_TEXT_PADDING,
) => { ) => {
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2; return getLineHeightInPx(fontSize, lineHeight) + boundTextPadding * 2;
}; };
let textMetricsProvider: TextMetricsProvider | undefined; let textMetricsProvider: TextMetricsProvider | undefined;

View File

@@ -1,4 +1,4 @@
import { getLineHeight } from "@excalidraw/common"; import { BOUND_TEXT_PADDING, getLineHeight } from "@excalidraw/common";
import { API } from "@excalidraw/excalidraw/tests/helpers/api"; import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "@excalidraw/common"; import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "@excalidraw/common";
@@ -63,9 +63,13 @@ describe("Test measureText", () => {
type: "rectangle", type: "rectangle",
...params, ...params,
}); });
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual( expect(
160, computeContainerDimensionForBoundText(
); 150,
element.type,
BOUND_TEXT_PADDING,
),
).toEqual(160);
}); });
it("should compute container height correctly for ellipse", () => { it("should compute container height correctly for ellipse", () => {
@@ -73,9 +77,13 @@ describe("Test measureText", () => {
type: "ellipse", type: "ellipse",
...params, ...params,
}); });
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual( expect(
226, computeContainerDimensionForBoundText(
); 150,
element.type,
BOUND_TEXT_PADDING,
),
).toEqual(226);
}); });
it("should compute container height correctly for diamond", () => { it("should compute container height correctly for diamond", () => {
@@ -83,9 +91,13 @@ describe("Test measureText", () => {
type: "diamond", type: "diamond",
...params, ...params,
}); });
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual( expect(
320, computeContainerDimensionForBoundText(
); 150,
element.type,
BOUND_TEXT_PADDING,
),
).toEqual(320);
}); });
}); });

View File

@@ -236,6 +236,9 @@ export const actionWrapTextInContainer = register({
let updatedElements: readonly ExcalidrawElement[] = elements.slice(); let updatedElements: readonly ExcalidrawElement[] = elements.slice();
const containerIds: Mutable<AppState["selectedElementIds"]> = {}; const containerIds: Mutable<AppState["selectedElementIds"]> = {};
const boundTextPadding =
appState.currentItemContainerBehavior?.margin ?? BOUND_TEXT_PADDING;
for (const textElement of selectedElements) { for (const textElement of selectedElements) {
if (isTextElement(textElement) && !isBoundToContainer(textElement)) { if (isTextElement(textElement) && !isBoundToContainer(textElement)) {
const container = newElement({ const container = newElement({
@@ -261,15 +264,17 @@ export const actionWrapTextInContainer = register({
: null, : null,
opacity: 100, opacity: 100,
locked: false, locked: false,
x: textElement.x - BOUND_TEXT_PADDING, x: textElement.x - boundTextPadding,
y: textElement.y - BOUND_TEXT_PADDING, y: textElement.y - boundTextPadding,
width: computeContainerDimensionForBoundText( width: computeContainerDimensionForBoundText(
textElement.width, textElement.width,
"rectangle", "rectangle",
boundTextPadding,
), ),
height: computeContainerDimensionForBoundText( height: computeContainerDimensionForBoundText(
textElement.height, textElement.height,
"rectangle", "rectangle",
boundTextPadding,
), ),
groupIds: textElement.groupIds, groupIds: textElement.groupIds,
frameId: textElement.frameId, frameId: textElement.frameId,

View File

@@ -26,6 +26,7 @@ import {
import { import {
canBecomePolygon, canBecomePolygon,
getNonDeletedElements, getNonDeletedElements,
hasContainerBehavior,
isFlowchartNodeElement, isFlowchartNodeElement,
} from "@excalidraw/element"; } from "@excalidraw/element";
@@ -132,6 +133,9 @@ import {
ArrowheadCrowfootOneOrManyIcon, ArrowheadCrowfootOneOrManyIcon,
stickyNoteIcon, stickyNoteIcon,
growingContainerIcon, growingContainerIcon,
marginLargeIcon,
marginMediumIcon,
marginSmallIcon,
} from "../components/icons"; } from "../components/icons";
import { Fonts } from "../fonts"; import { Fonts } from "../fonts";
@@ -1518,6 +1522,35 @@ export const actionChangeRoundness = register({
}, },
}); });
const getMargin = (value: "small" | "medium" | "large") => {
switch (value) {
case "small":
return BOUND_TEXT_PADDING;
case "medium":
return 15;
case "large":
return 25;
default:
return BOUND_TEXT_PADDING;
}
};
const getMarginValue = (margin: number | null) => {
if (margin === null) {
return null;
}
switch (margin) {
case BOUND_TEXT_PADDING:
return "small";
case 15:
return "medium";
case 25:
return "large";
default:
return null;
}
};
export const actionChangeContainerBehavior = register({ export const actionChangeContainerBehavior = register({
name: "changeContainerBehavior", name: "changeContainerBehavior",
label: "labels.container", label: "labels.container",
@@ -1536,7 +1569,7 @@ export const actionChangeContainerBehavior = register({
// collect directly selected eligible containers // collect directly selected eligible containers
for (const el of selected) { for (const el of selected) {
if (isFlowchartNodeElement(el) && getBoundTextElement(el, elementsMap)) { if (isFlowchartNodeElement(el)) {
containerIdsToUpdate.add(el.id); containerIdsToUpdate.add(el.id);
} }
} }
@@ -1558,12 +1591,61 @@ export const actionChangeContainerBehavior = register({
} }
} }
if (value.hasOwnProperty("margin")) {
if (containerIdsToUpdate.size === 0) { if (containerIdsToUpdate.size === 0) {
// nothing to update return {
return false; appState: {
...appState,
currentItemContainerBehavior: {
textFlow:
appState.currentItemContainerBehavior?.textFlow ?? "growing",
margin: getMargin(value.margin as "small" | "medium" | "large"),
},
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
} }
const nextElements = elements.map((el) => const nextElements = changeProperty(elements, appState, (el) =>
containerIdsToUpdate.has(el.id)
? newElementWith(el, {
containerBehavior: {
textFlow: el.containerBehavior?.textFlow ?? "growing",
margin: getMargin(value.margin as "small" | "medium" | "large"),
},
})
: el,
);
// Invalidate containers to trigger re-render
containerIdsToUpdate.forEach((id) => {
const container = nextElements.find((el) => el.id === id);
if (container) {
const boundText = getBoundTextElement(
container,
arrayToMap(nextElements),
);
if (boundText) {
redrawTextBoundingBox(boundText, container, app.scene);
}
}
});
return {
elements: nextElements,
appState: {
...appState,
currentItemContainerBehavior: {
textFlow:
appState.currentItemContainerBehavior?.textFlow ?? "growing",
margin: getMargin(value.margin as "small" | "medium" | "large"),
},
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
}
const nextElements = changeProperty(elements, appState, (el) =>
containerIdsToUpdate.has(el.id) containerIdsToUpdate.has(el.id)
? newElementWith(el, { ? newElementWith(el, {
containerBehavior: { containerBehavior: {
@@ -1615,29 +1697,39 @@ export const actionChangeContainerBehavior = register({
} }
} else { } else {
// case 2: any eligible containers directly selected // case 2: any eligible containers directly selected
targetContainers = selected.filter( targetContainers = selected.filter((el) => isFlowchartNodeElement(el));
(el) =>
isFlowchartNodeElement(el) && getBoundTextElement(el, elementsMap),
);
} }
if (targetContainers.length === 0) { if (
targetContainers.length === 0 &&
!hasContainerBehavior(appState.activeTool.type)
) {
return null; return null;
} }
const value = const textFlow =
reduceToCommonValue( targetContainers.length === 0
? appState.currentItemContainerBehavior?.textFlow ?? "growing"
: reduceToCommonValue(
targetContainers, targetContainers,
(el) => el.containerBehavior?.textFlow ?? "growing", (el) => el.containerBehavior?.textFlow ?? "growing",
) ?? ) ??
// mixed selection -> show null so nothing appears selected // mixed selection -> show null so nothing appears selected
null; null;
const marginValue =
targetContainers.length === 0
? appState.currentItemContainerBehavior?.margin ?? BOUND_TEXT_PADDING
: reduceToCommonValue(
targetContainers,
(el) => el.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
) ??
// mixed selection -> show null so nothing appears selected
null;
return ( return (
<fieldset> <fieldset>
{appState.stylesPanelMode === "full" && (
<legend>{t("labels.container")}</legend> <legend>{t("labels.container")}</legend>
)}
<div className="buttonList"> <div className="buttonList">
<RadioSelection <RadioSelection
group="container" group="container"
@@ -1654,12 +1746,42 @@ export const actionChangeContainerBehavior = register({
}, },
]} ]}
value={ value={
value ?? textFlow ??
(targetContainers.length (targetContainers.length
? null ? null
: appState.currentItemContainerBehavior?.textFlow ?? "growing") : appState.currentItemContainerBehavior?.textFlow ?? "growing")
} }
onChange={(val) => updateData(val)} onChange={(val) => updateData({ textFlow: val })}
/>
</div>
<div className="buttonList">
<RadioSelection
group="container"
options={[
{
value: "small",
text: t("labels.container_margin_small"),
icon: marginSmallIcon,
},
{
value: "medium",
text: t("labels.container_margin_medium"),
icon: marginMediumIcon,
},
{
value: "large",
text: t("labels.container_margin_large"),
icon: marginLargeIcon,
},
]}
value={getMarginValue(
marginValue ??
(targetContainers.length
? null
: appState.currentItemContainerBehavior?.margin ??
BOUND_TEXT_PADDING),
)}
onChange={(val) => updateData({ margin: val })}
/> />
</div> </div>
</fieldset> </fieldset>

View File

@@ -223,11 +223,10 @@ export const SelectedShapeActions = ({
<>{renderAction("changeRoundness")}</> <>{renderAction("changeRoundness")}</>
)} )}
{hasContainerBehavior(appState.activeTool.type) || {(hasContainerBehavior(appState.activeTool.type) ||
(targetElements.some( targetElements.some((element) => isFlowchartNodeElement(element))) && (
(element) => <>{renderAction("changeContainerBehavior")}</>
isFlowchartNodeElement(element) && hasBoundTextElement(element), )}
) && <>{renderAction("changeContainerBehavior")}</>)}
{(toolIsArrow(appState.activeTool.type) || {(toolIsArrow(appState.activeTool.type) ||
targetElements.some((element) => toolIsArrow(element.type))) && ( targetElements.some((element) => toolIsArrow(element.type))) && (
@@ -419,12 +418,10 @@ const CombinedShapeProperties = ({
canChangeRoundness(element.type), canChangeRoundness(element.type),
)) && )) &&
renderAction("changeRoundness")} renderAction("changeRoundness")}
{hasContainerBehavior(appState.activeTool.type) || {(hasContainerBehavior(appState.activeTool.type) ||
(targetElements.some( targetElements.some((element) =>
(element) => isFlowchartNodeElement(element),
isFlowchartNodeElement(element) && )) && <>{renderAction("changeContainerBehavior")}</>}
hasBoundTextElement(element),
) && <>{renderAction("changeContainerBehavior")}</>)}
{renderAction("changeOpacity")} {renderAction("changeOpacity")}
</div> </div>
</PropertiesPopover> </PropertiesPopover>
@@ -832,24 +829,6 @@ export const CompactShapeActions = ({
appState.editingTextElement || appState.newElement, appState.editingTextElement || appState.newElement,
); );
const textContainer =
targetElements.length === 1 && isTextElement(targetElements[0])
? getContainerElement(targetElements[0], elementsMap)
: null;
const isStickyNoteContainer =
textContainer && isFlowchartNodeElement(textContainer);
const showFillIcons =
(hasBackground(appState.activeTool.type) &&
!isTransparent(appState.currentItemBackgroundColor)) ||
targetElements.some(
(element) =>
hasBackground(element.type) && !isTransparent(element.backgroundColor),
);
const showLinkIcon = targetElements.length === 1;
const showLineEditorAction = const showLineEditorAction =
!appState.selectedLinearElement?.isEditing && !appState.selectedLinearElement?.isEditing &&
targetElements.length === 1 && targetElements.length === 1 &&

View File

@@ -104,6 +104,7 @@ import {
MQ_MAX_TABLET, MQ_MAX_TABLET,
MQ_MAX_HEIGHT_LANDSCAPE, MQ_MAX_HEIGHT_LANDSCAPE,
MQ_MAX_WIDTH_LANDSCAPE, MQ_MAX_WIDTH_LANDSCAPE,
BOUND_TEXT_PADDING,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
@@ -5413,8 +5414,13 @@ class App extends React.Component<AppProps, AppState> {
const minWidth = getApproxMinLineWidth( const minWidth = getApproxMinLineWidth(
getFontString(fontString), getFontString(fontString),
lineHeight, lineHeight,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
);
const minHeight = getApproxMinLineHeight(
fontSize,
lineHeight,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
); );
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
const newHeight = Math.max(container.height, minHeight); const newHeight = Math.max(container.height, minHeight);
const newWidth = Math.max(container.width, minWidth); const newWidth = Math.max(container.width, minWidth);
this.scene.mutateElement(container, { this.scene.mutateElement(container, {

View File

@@ -2328,19 +2328,82 @@ export const strokeIcon = createIcon(
); );
export const stickyNoteIcon = createIcon( export const stickyNoteIcon = createIcon(
<g <g>
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
>
<path d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" /> <path d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" />
<path d="M15 3v4a2 2 0 0 0 2 2h4" /> <path d="M15 3v4a2 2 0 0 0 2 2h4" />
</g>, </g>,
tablerIconProps, tablerIconProps,
); );
export const marginSmallIcon = createIcon(
<g fill="none">
<rect
x="0"
y="0"
width="24"
height="24"
stroke="currentColor"
stroke-width="1.5"
/>
<rect
x="2"
y="2"
width="20"
height="20"
stroke="currentColor"
stroke-width="1"
stroke-dasharray="1 1"
/>
</g>,
tablerIconProps,
);
export const marginMediumIcon = createIcon(
<g fill="none">
<rect
x="0"
y="0"
width="24"
height="24"
stroke="currentColor"
stroke-width="1.5"
/>
<rect
x="4"
y="4"
width="16"
height="16"
stroke="currentColor"
stroke-width="1"
stroke-dasharray="1 1"
/>
</g>,
tablerIconProps,
);
export const marginLargeIcon = createIcon(
<g fill="none">
<rect
x="0"
y="0"
width="24"
height="24"
stroke="currentColor"
stroke-width="1.5"
/>
<rect
x="6"
y="6"
width="12"
height="12"
stroke="currentColor"
stroke-width="1"
stroke-dasharray="1 1"
/>
</g>,
tablerIconProps,
);
export const growingContainerIcon = createIcon( export const growingContainerIcon = createIcon(
<g <g
stroke="currentColor" stroke="currentColor"

View File

@@ -34,6 +34,9 @@
"container": "Container", "container": "Container",
"container_fixed": "Sticky note", "container_fixed": "Sticky note",
"container_growing": "Fit to text", "container_growing": "Fit to text",
"container_margin_small": "Small margin",
"container_margin_medium": "Medium margin",
"container_margin_large": "Large margin",
"opacity": "Opacity", "opacity": "Opacity",
"textAlign": "Text align", "textAlign": "Text align",
"edges": "Edges", "edges": "Edges",

View File

@@ -8,6 +8,7 @@ import {
getFontFamilyString, getFontFamilyString,
isTestEnv, isTestEnv,
MIME_TYPES, MIME_TYPES,
BOUND_TEXT_PADDING,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
@@ -248,7 +249,11 @@ export const textWysiwyg = ({
// autogrow container height if text exceeds // autogrow container height if text exceeds
if (!isArrowElement(container) && height > maxHeight) { if (!isArrowElement(container) && height > maxHeight) {
const targetContainerHeight = const targetContainerHeight =
computeContainerDimensionForBoundText(height, container.type); computeContainerDimensionForBoundText(
height,
container.type,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
);
app.scene.mutateElement(container, { app.scene.mutateElement(container, {
height: targetContainerHeight, height: targetContainerHeight,
@@ -262,7 +267,11 @@ export const textWysiwyg = ({
height < maxHeight height < maxHeight
) { ) {
const targetContainerHeight = const targetContainerHeight =
computeContainerDimensionForBoundText(height, container.type); computeContainerDimensionForBoundText(
height,
container.type,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
);
app.scene.mutateElement(container, { app.scene.mutateElement(container, {
height: targetContainerHeight, height: targetContainerHeight,
}); });