From 52dcc79c158d4c7ecd37505e153fa3276ba56c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Thu, 27 Feb 2025 22:54:08 +0100 Subject: [PATCH] UPDATE: Adds wrapping! --- .../src/components/Toolbar/Toolbar.tsx | 63 ++++++++++-------- .../src/components/Workbook/Workbook.tsx | 6 ++ .../WorksheetCanvas/worksheetCanvas.ts | 64 ++++++++++++++++++- webapp/IronCalc/src/locale/en_us.json | 1 + 4 files changed, 106 insertions(+), 28 deletions(-) diff --git a/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx b/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx index 9dd50fe..ff78123 100644 --- a/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx +++ b/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx @@ -32,6 +32,7 @@ import { Type, Underline, Undo2, + WrapText, } from "lucide-react"; import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -64,6 +65,7 @@ type ToolbarProperties = { onToggleStrike: (v: boolean) => void; onToggleHorizontalAlign: (v: string) => void; onToggleVerticalAlign: (v: string) => void; + onToggleWrapText: (v: boolean) => void; onCopyStyles: () => void; onTextColorPicked: (hex: string) => void; onFillColorPicked: (hex: string) => void; @@ -81,6 +83,7 @@ type ToolbarProperties = { strike: boolean; horizontalAlign: HorizontalAlignment; verticalAlign: VerticalAlignment; + wrapText: boolean; canEdit: boolean; numFmt: string; showGridLines: boolean; @@ -206,6 +209,30 @@ function Toolbar(properties: ToolbarProperties) { + { + properties.onIncreaseFontSize(-1); + }} + title={t("toolbar.decrease_font_size")} + > + + + {properties.fontSize} + { + properties.onIncreaseFontSize(1); + }} + title={t("toolbar.increase_font_size")} + > + + + - - { - properties.onIncreaseFontSize(-1); - }} - title={t("toolbar.decrease_font_size")} - > - - - {properties.fontSize} - { - properties.onIncreaseFontSize(1); - }} - title={t("toolbar.increase_font_size")} - > - - - + { + properties.onToggleWrapText(!properties.wrapText); + }} + disabled={!canEdit} + title={t("toolbar.wrap_text")} + > + + { updateRangeStyle("alignment.vertical", value); }; + const onToggleWrapText = (value: boolean) => { + updateRangeStyle("alignment.wrap_text", `${value}`); + }; + const onTextColorPicked = (hex: string) => { updateRangeStyle("font.color", hex); }; @@ -532,6 +536,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { onToggleStrike={onToggleStrike} onToggleHorizontalAlign={onToggleHorizontalAlign} onToggleVerticalAlign={onToggleVerticalAlign} + onToggleWrapText={onToggleWrapText} onCopyStyles={onCopyStyles} onTextColorPicked={onTextColorPicked} onFillColorPicked={onFillColorPicked} @@ -639,6 +644,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { verticalAlign={ style.alignment?.vertical ? style.alignment.vertical : "bottom" } + wrapText={style.alignment?.wrap_text || false} canEdit={true} numFmt={style.num_fmt} showGridLines={model.getShowGridLines(model.getSelectedSheet())} diff --git a/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts b/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts index 31656b7..19a40d5 100644 --- a/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts +++ b/webapp/IronCalc/src/components/WorksheetCanvas/worksheetCanvas.ts @@ -70,6 +70,52 @@ function hexToRGBA10Percent(colorHex: string): string { return `rgba(${red}, ${green}, ${blue}, ${alpha})`; } +/** + * Splits the given text into multiple lines. If `wrapText` is true, it applies word-wrapping + * based on the specified canvas context, maximum width, and horizontal padding. + * + * - First, the text is split by newline characters so that explicit newlines are respected. + * - If wrapping is enabled, each line is further split into words and measured against the + * available width. Whenever adding an extra word would exceed + * this limit, a new line is started. + * + * @param text The text to split into lines. + * @param wrapText Whether to apply word-wrapping or just return text split by newlines. + * @param context The `CanvasRenderingContext2D` used for measuring text width. + * @param width The maximum width for each line. + * @returns An array of lines (strings), each fitting within the specified width if wrapping is enabled. + */ +function computeWrappedLines( + text: string, + wrapText: boolean, + context: CanvasRenderingContext2D, + width: number, +): string[] { + // Split the text into lines + const rawLines = text.split("\n"); + if (!wrapText) { + // If there is no wrapping, return the raw lines + return rawLines; + } + const wrappedLines = []; + for (const line of rawLines) { + const words = line.split(" "); + let currentLine = words[0]; + for (const word of words) { + const testLine = `${currentLine} ${word}`; + const textWidth = context.measureText(testLine).width; + if (textWidth < width) { + currentLine = testLine; + } else { + wrappedLines.push(currentLine); + currentLine = word; + } + } + wrappedLines.push(currentLine); + } + return wrappedLines; +} + export default class WorksheetCanvas { sheetWidth: number; @@ -371,6 +417,7 @@ export default class WorksheetCanvas { if (style.alignment?.vertical) { verticalAlign = style.alignment.vertical; } + const wrapText = style.alignment?.wrap_text || false; const context = this.ctx; context.font = font; @@ -496,9 +543,14 @@ export default class WorksheetCanvas { context.rect(x, y, width, height); context.clip(); - // Is there any better parameter? + // Is there any better to determine the line height? const lineHeight = fontSize * 1.5; - const lines = fullText.split("\n"); + const lines = computeWrappedLines( + fullText, + wrapText, + context, + width - padding, + ); const lineCount = lines.length; lines.forEach((text, line) => { @@ -682,13 +734,19 @@ export default class WorksheetCanvas { if (fullText === "") { continue; } + const width = this.getColumnWidth(sheet, column); const style = this.model.getCellStyle(sheet, row, column); const fontSize = style.font.sz; const lineHeight = fontSize * 1.5; let font = `${fontSize}px ${defaultCellFontFamily}`; font = style.font.b ? `bold ${font}` : `400 ${font}`; this.ctx.font = font; - const lines = fullText.split("\n"); + const lines = computeWrappedLines( + fullText, + style.alignment?.wrap_text || false, + this.ctx, + width, + ); const lineCount = lines.length; // This is computed so that the y position of the text is independent of the vertical alignment const textHeight = (lineCount - 1) * lineHeight + 8 + fontSize; diff --git a/webapp/IronCalc/src/locale/en_us.json b/webapp/IronCalc/src/locale/en_us.json index 60ae6b9..11cc0c7 100644 --- a/webapp/IronCalc/src/locale/en_us.json +++ b/webapp/IronCalc/src/locale/en_us.json @@ -26,6 +26,7 @@ "vertical_align_middle": " Align middle", "vertical_align_top": "Align top", "selected_png": "Export Selected area as PNG", + "wrap_text": "Wrap text", "format_menu": { "auto": "Auto", "number": "Number",