diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 04445a9..dd23ef3 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -4136,13 +4136,14 @@ function currentTextStyle() { return canvasEditor.input?.style ? { ...text, ...canvasEditor.input.style } : text; } -async function loadRichText(item, firstFace, tab = documentTab(), firstData = null) { +async function loadRichText(item, firstFace, tab = documentTab(), firstData = null, reflow = false) { const wasmSeq = tab.state.wasmSeq; const callWASM = (name, ...args) => { if (tab.closed || wasmSeq !== tab.state.wasmSeq) throw new Error("文档已更改"); return callTabWASM(tab, name, ...args); }; - const model = item.richText || await callWASM("ofdgoGetRichText", item.index, item.id); + const prepared = reflow ? await callWASM("ofdgoGetRichText", item.index, item.id, true) : null; + const model = prepared?.model || item.richText || await callWASM("ofdgoGetRichText", item.index, item.id); const remaining = firstData ? { ...model, runs: model.runs.filter(run => run.format.font !== item.font) } : model; const fontData = remaining.runs.length ? await callWASM("ofdgoRichTextFonts", JSON.stringify(remaining)) : []; const resources = new Map(fontData.map(font => [font.key, font.data])); @@ -4154,10 +4155,10 @@ async function loadRichText(item, firstFace, tab = documentTab(), firstData = nu if (tab.closed || wasmSeq !== tab.state.wasmSeq) throw new Error("文档已更改"); faces.set(id, face); } - return { model, faces, resources }; + return { model, faces, resources, frame: prepared?.frame }; } -async function ensureRichInput() { +async function ensureRichInput(reflow = false) { let editing = canvasEditor.input; if (!editing) { const item = canvasEditor.selected; @@ -4169,9 +4170,9 @@ async function ensureRichInput() { if (editing.item.selectionIDs) return false; if (editing.rich) return true; if (editing.composing || editing.saving) return false; - const rich = await loadRichText(editing.item, editing.face); + const rich = await loadRichText(editing.item, editing.face, documentTab(), null, reflow); if (canvasEditor.input !== editing) return false; - return canvasEditor.promoteText(rich); + return Boolean(editing.rich) || canvasEditor.promoteText(rich); } async function reflowCanvasText() { @@ -4255,6 +4256,7 @@ async function validateRichText(item, model, width, fonts = []) { async function saveRichText(item, model, fonts = [], box = item) { const tab = documentTab(), openSeq = state.openSeq, wasmSeq = state.wasmSeq; const boundary = { x: box.x, y: box.y, width: box.width, height: box.height }; + if (item.richReflow) { boundary.reflow = true; boundary.width = item.textFrame.width; } 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; @@ -4557,17 +4559,18 @@ function inputMillimeters(input, original) { async function changeTextAlignment() { const align = el.textAlign.value, editing = canvasEditor.input, tab = documentTab(); if (!state.ready || state.exporting || document.body.hasAttribute("data-busy") || editing?.composing || editing?.saving || !align) return; + const sequence = editing ? editing.alignSequence = (editing.alignSequence || 0) + 1 : 0; try { if (editing) { if (!editing.rich && !editing.item.draft && !confirmTextReflow(editing.item)) return; - if (!await ensureRichInput() || !isForegroundDocument(tab) || canvasEditor.input !== editing) 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 saveRichText(item, { ...item.richText, paragraphs: undefined, layout: { ...item.richText.layout, align } }); } else await changeParagraph(); } catch (error) { - if (isForegroundDocument(tab) && canvasEditor.input === editing) showError(error, false); + if (isForegroundDocument(tab) && canvasEditor.input === editing && (!editing || editing.alignSequence === sequence)) showError(error, false); } finally { if (isForegroundDocument(tab)) updateObjectControls(canvasEditor.selected, true); } diff --git a/assets/webui/ofdgo_edit.js b/assets/webui/ofdgo_edit.js index 278342a..a5dbc06 100644 --- a/assets/webui/ofdgo_edit.js +++ b/assets/webui/ofdgo_edit.js @@ -227,11 +227,6 @@ export class RichTextSession { record(before) { const change = textChange(before, this.model.runs); const entry = { change, typing: { ...this.typing }, caret: [...this.caret] }, old = this.entries[this.index]; - const beforeText = before.map(run => run.text).join(""); - if (beforeText !== this.text && this.model.paragraphs === this.recordedParagraphs) { - const removed = change.before.map(run => run.text).join(""), inserted = change.after.map(run => run.text).join(""); - replaceParagraphs(this.model, beforeText, change.start, change.start + removed.length, inserted); - } const paragraphsChanged = JSON.stringify(this.recordedParagraphs) !== JSON.stringify(this.model.paragraphs); if (paragraphsChanged) entry.paragraphs = { before: this.recordedParagraphs, after: this.model.paragraphs }; this.recordedParagraphs = this.model.paragraphs; @@ -309,7 +304,7 @@ export class RichTextSession { this.remember(); const [start, end] = range || this.selection(); const before = this.model.runs; - if (paragraphs) replaceParagraphs(this.model, this.text, start, end, runs.map(run => run.text).join(""), paragraphs); + replaceParagraphs(this.model, this.text, start, end, runs.map(run => run.text).join(""), paragraphs); this.model.runs = normalizeRuns([...sliceRuns(this.model.runs, 0, start), ...runs, ...sliceRuns(this.model.runs, end, this.text.length)]); this.caret = [start + runs.reduce((length, run) => length + run.text.length, 0)]; this.caret.push(this.caret[0]); @@ -335,6 +330,7 @@ export class RichTextSession { if (end < previous.length && /[\uDC00-\uDFFF]/.test(previous[end])) { end++; nextEnd++; } } const runs = this.model.runs; + replaceParagraphs(this.model, previous, start, end, value.slice(start, nextEnd)); this.model.runs = normalizeRuns([...sliceRuns(this.model.runs, 0, start), { text: value.slice(start, nextEnd), format: { ...this.typing } }, ...sliceRuns(this.model.runs, end, previous.length)]); this.caret = caret; @@ -1854,7 +1850,11 @@ export class CanvasEditor { } rich.replace([], null, range); return; } - if (!editing.composing) { editing.rich.remember(); editing.rich.beforeCaret = editing.rich.selection(); } + if (!editing.composing) { + editing.rich.remember(); + const range = event.getTargetRanges()[0]; + editing.rich.beforeCaret = range ? editTextSelection(input, range) : editing.rich.selection(); + } } if (editing.source && (event.inputType === "historyUndo" || event.inputType === "historyRedo")) { event.preventDefault(); @@ -1957,33 +1957,40 @@ export class CanvasEditor { const caret = editTextSelection(editing.input, editing.range) || [0, 0]; const value = editTextValue(editing.input); const session = new RichTextSession(this, rich.model, rich.faces, rich.fonts, rich.resources); - const format = { ...session.typing, ...editing.style }; - if (editing.fontData) { - const key = `temp-font-${crypto.randomUUID()}`; - format.font = key; - session.imports.set(key, editing.fontData); - session.faces.set(key, editing.face); - if (editing.fontChoice) session.choices.set(key, editing.fontChoice); - } - const history = editing.history; - const values = history ? history.entries.slice(0, history.index + 1) : [{ value: editing.item.text }, { value }]; - for (const entry of values) { + const base = { ...session.typing }, fonts = new Map(); + const scale = rich.frame ? sourceTextScale(editing) : 1; + const current = { value, style: editing.style, face: editing.face, fontData: editing.fontData, fontChoice: editing.fontChoice, caret }; + const values = editing.history ? editing.history.entries.slice(0, editing.history.index + 1) : [{ value: editing.item.text }]; + for (const entry of [...values, current]) { + const format = { ...base, ...entry.style }; + if (entry.style?.size) format.size *= scale; + if (entry.fontData) { + let key = fonts.get(entry.fontData); + if (!key) { + key = `temp-font-${crypto.randomUUID()}`; fonts.set(entry.fontData, key); + session.imports.set(key, entry.fontData); session.faces.set(key, entry.face); + document.fonts.add(entry.face); + if (entry.fontChoice) session.choices.set(key, entry.fontChoice); + } + format.font = key; + } const before = session.model.runs; session.model.runs = normalizeRuns([{ text: entry.value, format }]); - session.typing = { ...format }; + session.typing = format; session.caret = entry.caret || caret; session.record(before); } - const before = session.model.runs; - session.model.runs = normalizeRuns([{ text: value, format }]); - session.caret = caret; - session.record(before); + if (rich.frame) { + editing.frame = rich.frame; + editing.item = { ...editing.item, richReflow: true, textFrame: rich.frame }; + Object.assign(editing.input.style, { whiteSpace: rich.model.layout.wrap ? "pre-wrap" : "pre", textIndent: "0px", letterSpacing: "0px" }); + } editing.rich = session; editing.source = false; document.removeEventListener("selectionchange", editing.selectionListener); editing.history = null; editing.range = null; - const [a, b, c, d, e, f] = editing.frame.matrix, left = editing.item.leftIndent || 0; + const [a, b, c, d, e, f] = editing.frame.matrix, left = rich.model.layout.leftIndent || 0; editing.input.style.transform = `matrix(${a},${b},${c},${d},${(e + a * left) * PX_PER_MM},${(f + b * left) * PX_PER_MM})`; if (!rich.model.layout.lineHeight) editing.input.style.lineHeight = "normal"; session.render(); diff --git a/internal/webui/webui.go b/internal/webui/webui.go index a71a3d3..898a299 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -4686,7 +4686,7 @@ func (e *businessEngine) readTextSelectionEdit(args []platformValue, analysis of if err := value.Replace(start, end, replacement); err != nil { return edit, err } - value.Layout.AutoBidi, value.Layout.Wrap = true, true + value.Layout.AutoBidi = true edit.Reflow = &value } return edit, nil @@ -5001,12 +5001,31 @@ func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.Graphi } // getRichText 读取文字分段与段落选项,颜色转换为浏览器RGB格式 -// 入参: args 页码和对象标识 -// 返回: any 富文本快照, error 错误信息 +// 入参: args 页码、对象标识和可选显式重排标记 +// 返回: any 富文本快照或重排模型及页面坐标框, error 错误信息 func (e *businessEngine) getRichText(args []platformValue) (any, error) { if e.currentEditor == nil { return nil, fmt.Errorf("no document is being edited") } + if len(args) > 2 && args[2].Bool() { + selection, err := e.readTextSelection([]string{args[1].String()}) + if err != nil { + return nil, err + } + analysis, err := e.currentEditor.AnalyzeTextSelection(args[0].Int(), selection) + if err != nil { + return nil, err + } + if !analysis.Reflow { + return nil, fmt.Errorf("text cannot be reflowed") + } + value := analysis.RichText + value.Layout.AutoBidi = true + box := analysis.Bounds + return map[string]any{"model": e.browserRichText(value), "frame": map[string]any{ + "width": box.W, "height": box.H, "matrix": e.editorMatrix(ofdgo.TranslationMatrix(box.X, box.Y)), + }}, nil + } path, err := e.compositePath(args[1].String()) if err != nil { return nil, err @@ -5115,7 +5134,7 @@ func (e *businessEngine) readRichText(value platformValue) (ofdgo.RichText, erro } // saveRichText 原子保存文字、样式和新增字体,空标识创建文本框 -// 入参: args 页码、标识、富文本JSON、新建边界、可选字体集合和父路径 +// 入参: args 页码、标识、富文本JSON、边界及重排标记、可选字体集合和父路径 // 返回: any 文档信息及文字选区, error 错误信息 func (e *businessEngine) saveRichText(args []platformValue) (any, error) { value, err := e.readRichText(args[2]) @@ -5129,13 +5148,20 @@ func (e *businessEngine) saveRichText(args []platformValue) (any, error) { return err } } + box := args[3] + if !box.IsNull() && !box.IsUndefined() && box.Get("reflow").Type() == platformTypeBoolean && box.Get("reflow").Bool() { + selection, err := e.readTextSelection([]string{id}) + if err != nil { + return err + } + return e.currentEditor.UpdateTextSelection(page, selection, ofdgo.TextSelectionEdit{Reflow: &value, Width: box.Get("width").Float()}) + } if len(args) > 5 && args[5].String() != "" { key := args[5].String() path, err := e.compositePath(key) if err != nil { return err } - box := args[3] if id != "" { indexes, err := e.compositeIndexes(key, []string{id}) if err != nil { @@ -5158,7 +5184,6 @@ func (e *businessEngine) saveRichText(args []platformValue) (any, error) { if err := e.currentEditor.UpdateRichText(page, id, value); err != nil { return err } - box := args[3] if !box.IsNull() && !box.IsUndefined() && box.Get("frameWidth").Type() == platformTypeNumber { offset := 0.0 if box.Get("frameOffset").Type() == platformTypeNumber { @@ -5168,7 +5193,6 @@ func (e *businessEngine) saveRichText(args []platformValue) (any, error) { } return nil } - box := args[3] 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 }) diff --git a/ofdgo_editor_text_selection.go b/ofdgo_editor_text_selection.go index bc2fef3..9b62606 100644 --- a/ofdgo_editor_text_selection.go +++ b/ofdgo_editor_text_selection.go @@ -817,7 +817,7 @@ func (e *Editor) reflowTextSelection(page int, selection TextSelection, edit Tex } return e.putRichTextMetadata(page, first.ID, meta, -1) } - return e.editCompositeObjects(page, *selection.Root, selection.Indexes, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error { + if err := e.editCompositeObjects(page, *selection.Root, selection.Indexes, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error { for i, node := range nodes { if selection.Indexes[i] != first.Index { node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, nil}) @@ -838,5 +838,18 @@ func (e *Editor) reflowTextSelection(page int, selection TextSelection, edit Tex node.setState(editorCompositeState{}) } return nil - }) + }); err != nil { + return err + } + index := first.Index + for _, part := range preview.analysis.Parts[1:] { + if part.Index < first.Index { + index-- + } + } + member, err := e.CompositeObject(page, *selection.Root, index) + if err != nil { + return err + } + return e.putCompositeRichTextMetadata(page, member.Object, meta, -1) }