From c2b0339fdfa3c37fa220aa20dddd5fd92d371075 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 28 Oct 2026 14:10:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BC=96=E8=BE=91=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=96=87=E5=AD=97=E6=A0=BC=E5=BC=8F=E4=B8=8E?= =?UTF-8?q?=E6=AE=B5=E8=90=BD=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 130 ++++++++++++++++--------------------- assets/webui/ofdgo_edit.js | 8 +++ internal/webui/webui.go | 67 +++++++++++-------- ofdgo_editor_rich_text.go | 26 ++++++-- ofdgo_editor_selection.go | 61 ++++++++++++++++- 5 files changed, 183 insertions(+), 109 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index b9431ec..6102e9a 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -874,7 +874,7 @@ el.textLineHeight.addEventListener("change", () => changeParagraph(undefined, "l editorClick(el.paragraphButton, () => { const item = currentTextStyle(); for (const key of ["leftIndent", "rightIndent", "firstLineIndent"]) { - const mixed = Boolean(canvasEditor.input?.rich || item.richText) && item[key] === undefined; + const mixed = Boolean(canvasEditor.input?.rich || item.richText || item.items) && item[key] === undefined; el[key].required = !mixed; el[key].placeholder = mixed ? "混合" : ""; el[key].value = mixed ? "" : item[key] || 0; @@ -1197,10 +1197,7 @@ el.paragraphForm.addEventListener("submit", async event => { } else if (item === state.textDefaults) { Object.assign(item, indents); } else { - if (!confirmTextReflow(item)) return; - const saved = item.richText ? await saveRichParagraphs(item, indents) - : await changeDocument("ofdgoLayoutText", item, null, null, Boolean(item.wrap), item.align || "left", - item.paragraphHeight || 0, item.letterSpacing || 0, ...textIndents(indents)); + const saved = await saveTextParagraphs(item, indents); if (!saved) return; } if (isForegroundDocument(tab)) el.paragraphPanel.close(); @@ -4163,13 +4160,15 @@ async function loadRichText(item, firstFace, tab = documentTab(), firstData = nu const fontData = remaining.runs.length ? await callWASM("ofdgoRichTextFonts", JSON.stringify(remaining)) : []; const resources = new Map(fontData.map(font => [font.key, font.data])); if (firstData) resources.set(item.font, firstData); - const faces = new Map(); - for (const id of new Set(model.runs.map(run => run.format.font))) { - const data = id === item.font && firstFace ? null : resources.get(id); - const face = data ? await new FontFace(`ofdgo-rich-${crypto.randomUUID()}`, data, fontFaceStyle(data)).load() : firstFace; - if (tab.closed || wasmSeq !== tab.state.wasmSeq) throw new Error("文档已更改"); - faces.set(id, face); - } + const faces = new Map(), pending = [...new Set(model.runs.map(run => run.format.font))]; + await Promise.all(Array.from({ length: Math.min(4, pending.length) }, async () => { + while (pending.length) { + const id = pending.shift(), data = id === item.font && firstFace ? null : resources.get(id); + const face = data ? await new FontFace(`ofdgo-rich-${crypto.randomUUID()}`, data, fontFaceStyle(data)).load() : firstFace; + if (tab.closed || wasmSeq !== tab.state.wasmSeq) throw new Error("文档已更改"); + faces.set(id, face); + } + })); return { model, faces, resources, frame: prepared.frame, reflow }; } @@ -4216,10 +4215,12 @@ async function changeRangeFormat(key, value) { updateObjectControls(null); return; } const item = canvasEditor.selected; - if (!canvasEditor.input && (item?.items || item?.scoped)) { + if (!canvasEditor.input && item) { + const reflow = !canEditObject(item, "layoutKnown"); + if (reflow && !confirmTextReflow(item)) return; const current = currentTextStyle()[key]; const patch = { [key]: value ?? (key === "background" ? current ? "" : textBackground.value : current !== true) }; - await changeDocument("ofdgoFormatText", { ...item, id: (item.items || [item]).map(member => member.id) }, JSON.stringify(patch)); + await changeDocument("ofdgoFormatText", { ...item, id: (item.items || [item]).map(member => member.id) }, JSON.stringify(patch), reflow); return; } if (!await ensureRichInput()) return; @@ -4278,15 +4279,13 @@ async function validateRichText(item, model, width, fonts = []) { } } -async function saveRichText(item, model, fonts = [], box = item) { - const tab = documentTab(), openSeq = state.openSeq, wasmSeq = state.wasmSeq; +function saveRichText(item, model, fonts = [], box = item) { const boundary = { x: box.x, y: box.y, width: box.width, height: box.height }; if (item.richReflow) boundary.reflow = true; if (box.frameWidth !== undefined) Object.assign(boundary, { frameWidth: box.frameWidth, frameOffset: box.frameOffset || 0 }); - fonts = await validateRichText(item, model, box.frameWidth ?? item.textFrame?.width ?? item.width, fonts); - if (tab.closed || !isForegroundDocument(tab) || tab.state.openSeq !== openSeq || tab.state.wasmSeq !== wasmSeq) return false; return changeDocument("ofdgoSaveRichText", item.draft ? null : item, - ...(item.draft ? [item.index, ""] : []), JSON.stringify(model), boundary, fonts); + ...(item.draft ? [item.index, ""] : []), JSON.stringify(model), boundary, + () => validateRichText(item, model, box.frameWidth ?? item.textFrame?.width ?? item.width, fonts)); } function textIndents(item) { @@ -4452,15 +4451,6 @@ async function changeTextFont() { const item = selectedText(canvasEditor.selected); const font = state.textFonts[Number(fontPicker.value)]; if (!state.ready || document.body.hasAttribute("data-busy") || !font || font.disabled) return; - if (item?.richText && !canvasEditor.input) { - try { - await editCanvasObject(item); - if (!isForegroundDocument(tab) || openSeq !== state.openSeq || !canvasEditor.input?.rich) return; - } catch (error) { - if (isForegroundDocument(tab) && openSeq === state.openSeq) showError(error, false); - return; - } - } const editing = canvasEditor.input; if (editing) { if (editing.composing) return; @@ -4595,10 +4585,7 @@ async function changeTextAlignment() { if (!editing.rich && !editing.item.draft && !confirmTextReflow(editing.item)) return; if (!await ensureRichInput(!canEditObject(editing.item, "layoutKnown")) || !isForegroundDocument(tab) || canvasEditor.input !== editing || editing.alignSequence !== sequence) return; editing.rich.align(align); - } else if (canvasEditor.selected?.richText) { - const item = canvasEditor.selected; - await saveRichParagraphs(item, { align }); - } else await changeParagraph(); + } else await saveTextParagraphs(currentTextStyle(), { align }); } catch (error) { if (isForegroundDocument(tab) && canvasEditor.input === editing && (!editing || editing.alignSequence === sequence)) showError(error, false); } finally { @@ -4606,10 +4593,14 @@ async function changeTextAlignment() { } } -async function saveRichParagraphs(item, patch) { - const { wrap, ...values } = patch, model = item.richText; - return saveRichText(item, { ...model, layout: { ...model.layout, ...(wrap !== undefined ? { wrap } : {}) }, - paragraphs: model.runs.map(run => run.text).join("").split("\n").map((_, i) => ({ align: "", ...model.paragraphs?.[i], ...values })) }); +async function saveTextParagraphs(item, patch) { + if (!Object.keys(patch).length) return true; + if (item === state.textDefaults) { + Object.assign(item, patch); + return true; + } + if (!confirmTextReflow(item)) return false; + return changeDocument("ofdgoLayoutTexts", item, JSON.stringify(patch)); } async function changeParagraph(wrap, changed) { @@ -4623,33 +4614,18 @@ async function changeParagraph(wrap, changed) { } return changeInputParagraph(patch); } - wrap ??= item.wrap; if (document.body.hasAttribute("data-busy") || item.draft) return; - if (!el.textLineHeight.checkValidity()) { - updateObjectControls(item, true); - return; + const patch = wrap !== undefined ? { wrap: Boolean(wrap) } : {}; + for (const [input, key, original] of [[el.textLineHeight, "lineHeight", item.paragraphHeight], [el.textSpacing, "letterSpacing", item.letterSpacing]]) { + if (key !== changed) continue; + if (!input.checkValidity()) return updateObjectControls(item, true); + patch[key] = inputMillimeters(input, original || 0); } - const height = inputMillimeters(el.textLineHeight, item.paragraphHeight || 0); - if (!el.textSpacing.checkValidity()) { - updateObjectControls(canvasEditor.selected, true); - return; - } - const spacing = inputMillimeters(el.textSpacing, item.letterSpacing || 0); if (item === state.textDefaults) { - Object.assign(item, { wrap: Boolean(wrap), align: el.textAlign.value, paragraphHeight: height, letterSpacing: spacing }); - updateObjectControls(canvasEditor.selected, true); - return; - } - if (!confirmTextReflow(item)) { - updateObjectControls(item, true); - return; - } - try { - if (item.richText) await saveRichParagraphs(item, changed === "lineHeight" ? { lineHeight: height } : changed === "letterSpacing" ? { letterSpacing: spacing } : { wrap: Boolean(wrap) }); - else await changeDocument("ofdgoLayoutText", item, null, null, Boolean(wrap), el.textAlign.value, height, spacing, ...textIndents(item)); - } catch (error) { - if (isForegroundDocument(tab)) showError(error, false); - } + if (patch.lineHeight !== undefined) item.paragraphHeight = patch.lineHeight; + if (patch.letterSpacing !== undefined) item.letterSpacing = patch.letterSpacing; + if (patch.wrap !== undefined) item.wrap = patch.wrap; + } else await saveTextParagraphs(item, patch); if (isForegroundDocument(tab)) updateObjectControls(canvasEditor.selected, true); } @@ -4679,7 +4655,7 @@ async function changeTextStyle(color) { const size = color ? item.size : inputMillimeters(el.textSize, item.size); const selectedColor = el.textColor.value; const fill = color && selectedColor !== item.color ? selectedColor : null; - if (canvasEditor.input || item.richText) { + if (canvasEditor.input) { const editing = canvasEditor.input, tab = documentTab(); try { if (!await ensureRichInput() || !isForegroundDocument(tab) || editing && canvasEditor.input !== editing) return; @@ -4691,6 +4667,10 @@ async function changeTextStyle(color) { updateObjectControls(canvasEditor.selected, true); return; } + if (item.richText) { + await changeDocument("ofdgoFormatText", item, JSON.stringify(color ? { color: selectedColor } : { size })); + return; + } if (item?.scoped && color) { if (color && fill !== null && canEditObject(item, "paint")) { await changeDocument("ofdgoCompositeTextColor", item, fill); @@ -4813,7 +4793,7 @@ async function changeDocument(name, item, ...args) { const toolbarFocus = el.editorTools.contains(active) ? active : null; const previous = pageView.currentPageInfo() || state.doc.pages[Math.min(state.pageIndex, state.doc.pages.length - 1)] || {}; const originalDocument = state.doc; - const before = editorLocation(), revision = state.editorInfo.revision; + const before = editorLocation(), revision = state.editorInfo.revision, wasmSeq = state.wasmSeq; const restoring = name === "ofdgoUndo" || name === "ofdgoRedo"; const { scrollLeft, scrollTop } = el.viewerPanel; const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments"; @@ -4830,8 +4810,8 @@ async function changeDocument(name, item, ...args) { } }, 250) : null; try { - args = await Promise.all(args); - if (tab.closed) { + args = await Promise.all(args.map(value => typeof value === "function" ? value() : value)); + if (tab.closed || state.wasmSeq !== wasmSeq || state.doc !== originalDocument || state.editorInfo?.revision !== revision) { return; } const scope = objectParent(item); @@ -4849,8 +4829,8 @@ async function changeDocument(name, item, ...args) { const scopedRichText = scoped && name === "ofdgoSaveRichText"; if (scopedRichText) args.push(scope); const textSelection = name === "ofdgoUpdateTextSelection"; - if (scoped && !operation && !textSelection && !scopedRichText) throw new Error("内部对象暂不支持此操作"); - const doc = ["ofdgoStyleText", "ofdgoFormatText"].includes(name) + if (scoped && !operation && !textSelection && !scopedRichText && name !== "ofdgoLayoutTexts") throw new Error("内部对象暂不支持此操作"); + const doc = ["ofdgoStyleText", "ofdgoFormatText", "ofdgoLayoutTexts"].includes(name) ? await callWASM(name, item.index, ids, ...args) : textSelection ? await callWASM(name, item.index, item.selectionIDs, ...args) @@ -4987,7 +4967,9 @@ async function changeDocument(name, item, ...args) { if (name === "ofdgoChangeFontBinding") err.message = fontBindingError(err.message); if (name === "ofdgoChangeInvoice") err.message = invoiceIssues([err.message]); if (isForegroundDocument(tab) && openSeq === state.openSeq) { - if (settingsElements.Panel.open) { + if (name === "ofdgoSaveRichText" && canvasEditor.input?.saving) { + canvasEditor.setTextError(err); + } else if (settingsElements.Panel.open) { settingsElements.Status.textContent = err.message; } else if (fontBindingElements.Panel.open) { fontBindingElements.Status.textContent = fontBindingError(err.message); @@ -12553,22 +12535,22 @@ function updateObjectControls(item, reset = false) { el.undoButton.disabled = !state.editorInfo?.canUndo || !state.ready || state.exporting; el.redoButton.disabled = !state.editorInfo?.canRedo || !state.ready || state.exporting; } - el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items); + el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled; el.textWrap.disabled = el.textLineHeight.disabled || text === state.textDefaults; const reflow = document.querySelector("#textReflow"), positioned = canvasEditor.input?.source && (item?.selectionIDs || !canEditObject(item, "layoutKnown")); reflow.hidden = !positioned; el.textWrap.hidden = !!positioned; reflow.disabled = textDisabled || !canEditObject(item, "reflow") || !!canvasEditor.input?.reflow; el.paragraphButton.disabled = el.textLineHeight.disabled; - el.textAlign.disabled = el.textSize.disabled || Boolean(item?.items || item?.selectionIDs); + el.textAlign.disabled = el.textSize.disabled || Boolean(item?.selectionIDs); const layoutKnown = text === state.textDefaults || Boolean(item?.draft) || canEditObject(item, "layoutKnown"); const rich = canvasEditor.input?.rich; - const alignment = rich ? richTextAlignment(rich.model, ...rich.selection()) : item?.richText ? richTextAlignment(item.richText) : layoutKnown ? text.align || "left" : ""; - el.textAlign.options[0].textContent = rich || item?.richText ? "混合" : "原文"; + const alignment = item?.items ? text.align ?? "" : rich ? richTextAlignment(rich.model, ...rich.selection()) : item?.richText ? richTextAlignment(item.richText) : layoutKnown ? text.align || "left" : ""; + el.textAlign.options[0].textContent = rich || item?.richText || item?.items ? "混合" : "原文"; el.textAlign.value = alignment; - el.textLineHeight.placeholder = (rich || item?.richText) && text.paragraphHeight === undefined ? "混合" : layoutKnown ? "自动" : "原文"; - el.textSpacing.placeholder = (rich || item?.richText) && text.letterSpacing === undefined ? "混合" : layoutKnown ? "0" : "原文"; - el.textWrap.setAttribute("data-pressed", String(Boolean(text.wrap))); + el.textLineHeight.placeholder = (rich || item?.richText || item?.items) && text.paragraphHeight === undefined ? "混合" : layoutKnown ? "自动" : "原文"; + el.textSpacing.placeholder = (rich || item?.richText || item?.items) && text.letterSpacing === undefined ? "混合" : layoutKnown ? "0" : "原文"; + el.textWrap.setAttribute("data-pressed", item?.items && text.wrap === undefined ? "mixed" : String(Boolean(text.wrap))); if (reset || document.activeElement !== el.textLineHeight) { el.textLineHeight.value = text.paragraphHeight ? String(displayPoints(text.paragraphHeight)) : ""; } diff --git a/assets/webui/ofdgo_edit.js b/assets/webui/ofdgo_edit.js index 3656d93..b054220 100644 --- a/assets/webui/ofdgo_edit.js +++ b/assets/webui/ofdgo_edit.js @@ -754,6 +754,14 @@ export function selectedText(item) { const value = formats[0][key]; text[key] = formats.every(format => format[key] === value) ? value : undefined; } + const layouts = item.items.map(member => member.richText ? { ...richTextLayout(member.richText), wrap: Boolean(member.richText.layout.wrap) } : { + align: member.align || "left", wrap: Boolean(member.wrap), lineHeight: member.paragraphHeight || 0, + letterSpacing: member.letterSpacing || 0, leftIndent: member.leftIndent || 0, rightIndent: member.rightIndent || 0, firstLineIndent: member.firstLineIndent || 0, + }); + for (const key of ["align", "wrap", "lineHeight", "letterSpacing", "leftIndent", "rightIndent", "firstLineIndent"]) { + const values = layouts.map(layout => layout[key]); + text[key === "lineHeight" ? "paragraphHeight" : key] = values.every(value => value === values[0]) ? values[0] : undefined; + } return text; } diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 776290d..e436697 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -4926,7 +4926,7 @@ func (e *businessEngine) styleText(args []platformValue) (any, error) { } // formatText 批量修改选区格式,未指定的属性保持各自原值 -// 入参: args 页面索引、完整路径数组和格式JSON +// 入参: args 页面索引、完整路径数组、格式JSON及可选显式重排标志 // 返回: any 文档信息, error 校验或提交错误 func (e *businessEngine) formatText(args []platformValue) (any, error) { return e.changeObjects(func() error { @@ -4948,10 +4948,33 @@ func (e *businessEngine) formatText(args []platformValue) (any, error) { } *value = color.Value } + if len(args) > 3 && args[3].Bool() { + return e.currentEditor.ReflowTargetsText(args[0].Int(), paths, patch) + } return e.currentEditor.FormatTargetsText(args[0].Int(), paths, patch) }) } +// layoutTexts 批量修改段落增量,未指定的选项沿用各对象原值 +// 入参: args 页面索引、完整路径数组和段落JSON +// 返回: any 文档信息, error 校验或提交错误 +func (e *businessEngine) layoutTexts(args []platformValue) (any, error) { + return e.changeObjects(func() error { + paths, err := e.editorTargetPaths(e.stringsFromJS(args[1])) + if err != nil { + return err + } + var patch struct { + ofdgo.RichTextParagraph + Wrap *bool `json:"wrap"` + } + if err := json.Unmarshal([]byte(args[2].String()), &patch); err != nil { + return err + } + return e.currentEditor.LayoutTargetsText(args[0].Int(), paths, patch.RichTextParagraph, patch.Wrap) + }) +} + // editorRichText 将富文本复合图元作为一个文本框提供给编辑器 // 入参: item 浏览器对象, object 标准复合图元, value 富文本, fonts 字体名称, local 局部编辑框, matrix 页面变换 func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.GraphicObject, value ofdgo.RichText, fonts map[string]string, local ofdgo.Box, matrix ofdgo.Matrix) { @@ -5151,43 +5174,30 @@ func (e *businessEngine) saveRichText(args []platformValue) (any, error) { } return e.currentEditor.ReflowRichText(page, path, value) } + if id != "" { + path, err := e.compositePath(id) + if err != nil { + return err + } + offset, width := 0.0, 0.0 + if !box.IsNull() && !box.IsUndefined() && box.Get("frameWidth").Type() == platformTypeNumber { + width = box.Get("frameWidth").Float() + if box.Get("frameOffset").Type() == platformTypeNumber { + offset = box.Get("frameOffset").Float() + } + } + return e.currentEditor.UpdateTargetRichText(page, path, value, offset, width) + } if len(args) > 5 && args[5].String() != "" { key := args[5].String() path, err := e.compositePath(key) if err != nil { return err } - if id != "" { - indexes, err := e.compositeIndexes(key, []string{id}) - if err != nil { - return err - } - width, offset := 0.0, 0.0 - if !box.IsNull() && !box.IsUndefined() && box.Get("frameWidth").Type() == platformTypeNumber { - width = box.Get("frameWidth").Float() - if box.Get("frameOffset").Type() == platformTypeNumber { - offset = box.Get("frameOffset").Float() - } - } - return e.currentEditor.UpdateCompositeRichText(page, path, indexes[0], value, offset, width) - } index, err := e.currentEditor.AddCompositeRichText(page, path, ofdgo.Box{X: box.Get("x").Float(), Y: box.Get("y").Float(), W: box.Get("width").Float(), H: box.Get("height").Float()}, value) id = fmt.Sprintf("%s/%d", key, index) return err } - if id != "" { - if err := e.currentEditor.UpdateRichText(page, id, value); err != nil { - return err - } - if !box.IsNull() && !box.IsUndefined() && box.Get("frameWidth").Type() == platformTypeNumber { - offset := 0.0 - if box.Get("frameOffset").Type() == platformTypeNumber { - offset = box.Get("frameOffset").Float() - } - return e.currentEditor.ResizeRichTextFrame(page, id, offset, box.Get("frameWidth").Float()) - } - return nil - } id, err = e.currentEditor.AddRichText(page, ofdgo.Box{X: box.Get("x").Float(), Y: box.Get("y").Float(), W: box.Get("width").Float(), H: box.Get("height").Float()}, value) return err }) @@ -8054,6 +8064,7 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an "ofdgoReshapeLine": e.reshapeLine, "ofdgoResetImageCrop": e.resetImageCrop, "ofdgoLayoutText": e.layoutText, + "ofdgoLayoutTexts": e.layoutTexts, "ofdgoTransformObjects": e.transformObjects, "ofdgoCompositeObjects": e.compositeObjects, "ofdgoChangeCompositeObjects": e.changeCompositeObjects, diff --git a/ofdgo_editor_rich_text.go b/ofdgo_editor_rich_text.go index 3a47912..68ef56a 100644 --- a/ofdgo_editor_rich_text.go +++ b/ofdgo_editor_rich_text.go @@ -105,6 +105,24 @@ func (e *Editor) UpdateRichText(page int, id string, value RichText) error { return e.updateRichTextFrame(page, id, value, 0, 0, false) } +// UpdateTargetRichText 一次提交正文或组合内富文本的内容和局部宽度,保留页面变换 +// 入参: page 页面索引, path 完整对象路径, value 富文本, offset 局部偏移, width 宽度,零值保留宽度模式 +// 返回: error 路径、尺寸或排版错误 +func (e *Editor) UpdateTargetRichText(page int, path ObjectPath, value RichText, offset, width float64) error { + if !finite(offset) || !finite(width) || width < 0 || width == 0 && offset != 0 { + return fmt.Errorf("invalid text frame") + } + if len(path.Children) == 0 { + if path.Annotation != "" { + return ErrNotRichText + } + return e.updateRichTextFrame(page, path.ID, value, offset, width, false) + } + index := path.Children[len(path.Children)-1] + path.Children = path.Children[:len(path.Children)-1] + return e.updateCompositeRichText(page, path, index, value, offset, width, false) +} + // ResizeRichTextFrame 调整局部左边界与段落宽度并原子重排,保留页面方向和缩放 // 入参: page 页面索引, id 对象标识, offset 局部横向偏移, width 新的局部段落宽度 // 返回: error 尺寸、排版或提交错误 @@ -1066,17 +1084,17 @@ func richTextMetadataFontSafe(value string) bool { } // formatCompositeText 修改内部富文本格式,普通文字安全提升后保留原实例变换 -// 入参: page 页面索引, path 父路径, index 成员序号, patch 格式增量 +// 入参: page 页面索引, path 父路径, index 成员序号, patch 格式增量, reflow 显式重排 // 返回: error 识别、排版或提交错误 -func (e *Editor) formatCompositeText(page int, path ObjectPath, index int, patch TextFormatPatch) error { - value, err := e.TargetRichText(page, ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: append(slices.Clone(path.Children), index)}) +func (e *Editor) formatCompositeText(page int, path ObjectPath, index int, patch TextFormatPatch, reflow bool) error { + value, _, _, err := e.PrepareRichText(page, ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: append(slices.Clone(path.Children), index)}, reflow) if err != nil { return err } if err := value.ApplyFormat(0, value.Len(), patch); err != nil { return err } - return e.UpdateCompositeRichText(page, path, index, value, 0, 0) + return e.updateCompositeRichText(page, path, index, value, 0, 0, reflow) } // updateCompositePlainRichText 将可验证布局的内部文字提升为富文本并保留实例坐标 diff --git a/ofdgo_editor_selection.go b/ofdgo_editor_selection.go index 02eaa44..04c55dd 100644 --- a/ofdgo_editor_selection.go +++ b/ofdgo_editor_selection.go @@ -886,6 +886,61 @@ func (e *Editor) FormatTargetsText(page int, paths []ObjectPath, patch TextForma return e.formatTargetsText(page, paths, patch, false) } +// ReflowTargetsText 显式重排跨容器文字并应用格式增量,保留对象位置及组合结构 +// 入参: page 页面索引, paths 完整文字路径, patch 格式增量 +// 返回: error 路径、格式或排版错误 +func (e *Editor) ReflowTargetsText(page int, paths []ObjectPath, patch TextFormatPatch) error { + return e.formatTargetsText(page, paths, patch, true) +} + +// LayoutTargetsText 原子修改跨容器文字的段落增量,保留各自宽度、变换及未指定的样式 +// 定位文字显式重排为富文本,任一对象失败时回滚全部修改及新增资源 +// 入参: page 页面索引, paths 完整文字路径, patch 段落增量, wrap 折行选项,nil保持原值 +// 返回: error 路径、段落或排版错误 +func (e *Editor) LayoutTargetsText(page int, paths []ObjectPath, patch RichTextParagraph, wrap *bool) error { + if patch == (RichTextParagraph{}) && wrap == nil { + return nil + } + apply := func(path ObjectPath) error { + value, _, _, err := e.PrepareRichText(page, path, true) + if err != nil { + return err + } + if wrap != nil { + value.Layout.Wrap = *wrap + } + if err := value.FormatParagraphs(0, value.Len(), patch); err != nil { + return err + } + if strings.HasSuffix(value.Text(), "\n") { + if err := value.FormatParagraphs(value.Len(), value.Len(), patch); err != nil { + return err + } + } + return e.ReflowRichText(page, path, value) + } + return e.editTargets(paths, func(ids, annotations []string) error { + if len(annotations) != 0 { + return ErrNotRichText + } + for _, id := range ids { + if err := apply(ObjectPath{ID: id}); err != nil { + return err + } + } + return nil + }, func(path ObjectPath, indexes []int) error { + for _, index := range indexes { + target := path + target.Children = append(slices.Clone(path.Children), index) + if err := apply(target); err != nil { + return err + } + } + return nil + }) +} + // formatTargetsText 统一文字选区格式事务,普通字体和颜色修改沿用定位保留路径 // 入参: page 页面索引, paths 完整路径, patch 格式增量, reflow 显式重排 // 返回: error 校验或编辑错误 @@ -925,14 +980,14 @@ func (e *Editor) formatTargetsText(page int, paths []ObjectPath, patch TextForma plain = append(plain, id) continue } - value, err := e.richTextObject(page, object) + value, _, _, err := e.PrepareRichText(page, ObjectPath{ID: id}, reflow) if err != nil { return err } if err := value.ApplyFormat(0, value.Len(), patch); err != nil { return err } - if err := e.UpdateRichText(page, id, value); err != nil { + if err := e.updateRichTextFrame(page, id, value, 0, 0, reflow); err != nil { return err } } @@ -951,7 +1006,7 @@ func (e *Editor) formatTargetsText(page int, paths []ObjectPath, patch TextForma plain = append(plain, indexes[i]) continue } - if err := e.formatCompositeText(page, path, indexes[i], patch); err != nil { + if err := e.formatCompositeText(page, path, indexes[i], patch, reflow); err != nil { return err } }