diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index a663941..3160ee8 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -4314,7 +4314,7 @@ async function beginCanvasText(index, box, surface, page) { const node = document.createElement("div"); const item = { ...state.textDefaults, ...box, wrap: state.textDefaults.wrapOverride ?? box.wrap ?? state.textDefaults.wrap, index, page, surface, node, text: "", draft: true, fontData: data }; - const model = richTextModel(item); + const model = { ...richTextModel(item), autoWidth: !box.wrap }; const key = `temp-font-${crypto.randomUUID()}`; model.runs[0].format.font = key; canvasEditor.editText(item, face, null, { model, faces: new Map([[key, face]]), fonts: [{ key, data }] }); diff --git a/assets/webui/ofdgo_edit.js b/assets/webui/ofdgo_edit.js index d947284..d286e1c 100644 --- a/assets/webui/ofdgo_edit.js +++ b/assets/webui/ofdgo_edit.js @@ -390,7 +390,8 @@ export class RichTextSession { } render() { this.rendering = true; - const input = this.editing.input, layout = this.model.layout, lines = [[]], positions = []; + const input = this.editing.input, layout = this.model.layout, lines = [[]], positions = [], distributed = []; + const autoWidth = this.model.autoWidth && !layout.wrap; Object.assign(input.style, { whiteSpace: layout.wrap ? "pre-wrap" : "pre", lineHeight: layout.lineHeight ? `${layout.lineHeight * PX_PER_MM}px` : "normal", textIndent: `${(layout.firstLineIndent || 0) * PX_PER_MM}px`, letterSpacing: `${(layout.letterSpacing || 0) * PX_PER_MM}px` }); @@ -409,7 +410,7 @@ export class RichTextSession { const align = this.model.paragraphs?.[index]?.align || this.model.layout.align || "left"; const distribute = align === "distributed" && !CSS.supports("text-justify", "inter-character"); const key = JSON.stringify([parts.map(part => [part.text, part.format, this.faces.get(part.format.font)?.family]), align, layout, distribute ? input.offsetWidth : null]); - let paragraph = previous[index]?.key === key && previous[index].node.parentNode === input ? previous[index].node : null; + let paragraph = (!autoWidth || !distribute) && previous[index]?.key === key && previous[index].node.parentNode === input ? previous[index].node : null; if (!paragraph) { paragraph = document.createElement("div"); for (const {text, format} of parts) { @@ -427,9 +428,15 @@ export class RichTextSession { } Object.assign(paragraph.style, { minWidth: layout.wrap ? "" : "max-content", textAlign: distribute ? "left" : align === "distributed" ? "justify" : align, textAlignLast: !distribute && align === "distributed" ? "justify" : "auto", textJustify: align === "distributed" ? "inter-character" : "auto" }); - if (distribute) distributeParagraph(input, paragraph); } + if (distribute && (autoWidth || previous[index]?.node !== paragraph)) distributed.push(paragraph); if (input.childNodes[index] !== paragraph) input.insertBefore(paragraph, input.childNodes[index] || null); + return { key, node: paragraph }; + }); + while (input.childNodes.length > lines.length) input.lastChild.remove(); + if (autoWidth) input.style.width = getComputedStyle(input).width; + for (const paragraph of distributed) distributeParagraph(input, paragraph); + for (const [index, paragraph] of [...input.children].entries()) { if (index) offset++; const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT); let node; @@ -437,9 +444,7 @@ export class RichTextSession { positions.push({ node, start: offset, end: offset + node.textContent.length }); offset += node.textContent.length; } - return { key, node: paragraph }; - }); - while (input.childNodes.length > lines.length) input.lastChild.remove(); + } input.style.fontSize = this.model.runs.length ? "0px" : `${this.typing.size * PX_PER_MM}px`; input.style.caretColor = this.typing.color || "#000000"; this.editing.range = null; @@ -1929,7 +1934,7 @@ export class CanvasEditor { const { input, frame, item, rich } = editing; const layout = rich?.model.layout || item; const width = Math.max(0, frame.width - (layout.leftIndent || 0) - (layout.rightIndent || 0)) * PX_PER_MM + 4; - Object.assign(input.style, { width: `${width}px`, minHeight: "0px", height: "auto" }); + Object.assign(input.style, { width: rich?.model.autoWidth && !layout.wrap ? "max-content" : `${width}px`, minWidth: "4px", minHeight: "0px", height: "auto" }); } setTextError(error) { diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 7f0cefd..adf24c1 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -5071,7 +5071,7 @@ func (e *businessEngine) browserRichText(value ofdgo.RichText) map[string]any { paragraphs[i] = map[string]any{"align": paragraph.Align} } layout := value.Layout - return map[string]any{"runs": runs, "paragraphs": paragraphs, "layout": map[string]any{ + return map[string]any{"autoWidth": value.AutoWidth, "runs": runs, "paragraphs": paragraphs, "layout": map[string]any{ "autoBidi": layout.AutoBidi, "shape": layout.Shape, "shaping": map[string]any{ "Bidi": layout.Shaping.Bidi, "Direction": layout.Shaping.Direction, diff --git a/ofdgo_editor_rich_text.go b/ofdgo_editor_rich_text.go index ed13059..63d6877 100644 --- a/ofdgo_editor_rich_text.go +++ b/ofdgo_editor_rich_text.go @@ -134,6 +134,9 @@ func (e *Editor) updateRichTextFrame(page int, id string, value RichText, offset if err := value.Validate(); err != nil { return err } + if width > 0 { + value.AutoWidth = false + } if old.Type == "CompositeObject" && offset == 0 && (width == 0 || width == frame.W) && reflect.DeepEqual(normalizeRichText(current), normalizeRichText(value)) { return nil } @@ -356,6 +359,9 @@ func (e *Editor) updateCompositeRichText(page int, path ObjectPath, index int, v if err := value.Validate(); err != nil { return err } + if width > 0 { + value.AutoWidth = false + } if offset == 0 && (width == 0 || width == frame.W) && reflect.DeepEqual(normalizeRichText(current), normalizeRichText(value)) { return nil } @@ -460,6 +466,10 @@ func (e *Editor) prepareRichText(box Box, value RichText) (GraphicObject, richTe if err != nil { return GraphicObject{}, richTextMetadata{}, err } + box.W = result.width + if value.AutoWidth && !value.Layout.Wrap { + box.H = result.bounds.Y + result.bounds.H + } padX, padY := -min(0, result.bounds.X), -min(0, result.bounds.Y) extent := Box{W: max(box.W, result.bounds.X+result.bounds.W) + padX, H: max(box.H, result.bounds.Y+result.bounds.H) + padY} if !finite(extent.W) || !finite(extent.H) || extent.W <= 0 || extent.H <= 0 { diff --git a/ofdgo_rich_layout.go b/ofdgo_rich_layout.go index c9d13e1..c5f7c86 100644 --- a/ofdgo_rich_layout.go +++ b/ofdgo_rich_layout.go @@ -31,6 +31,7 @@ import ( // richLayout 保存局部标准对象、含墨迹的边界及各片段的字体锚点序号 type richLayout struct { + width float64 objects []GraphicObject bounds Box anchors []int @@ -124,6 +125,38 @@ func (e *Editor) layoutRichText(value RichText, width float64) (richLayout, erro } chars := []rune(value.Text()) var face *typefont.Face + if value.AutoWidth && !options.Wrap { + width = max(1, options.LeftIndent+options.RightIndent+max(0, options.FirstLineIndent)+1) + for start := 0; start <= len(chars); { + end := start + for end < len(chars) && chars[end] != '\n' { + end++ + } + paragraph := chars[start:end] + local := richParagraphOptions(paragraph, options) + if local.Shaping.Bidi && face == nil && len(paragraph) > 0 { + face, err = typefont.ParseTTF(bytes.NewReader(metrics[owners[start]].font.Write())) + if err != nil { + return richLayout{}, err + } + } + _, advances, err := richShapeLine(paragraph, owners[start:end], value, metrics, local, face) + if err != nil { + return richLayout{}, err + } + length := 0.0 + for _, advance := range advances { + length += advance + } + width = max(width, local.LeftIndent+local.RightIndent+local.FirstLineIndent+length) + if end == len(chars) { + break + } + start = end + 1 + } + available = width - options.LeftIndent - options.RightIndent + } + result.width = width var underlays []GraphicObject var shadows []GraphicObject var y float64 diff --git a/ofdgo_rich_text.go b/ofdgo_rich_text.go index 295f4e5..2567437 100644 --- a/ofdgo_rich_text.go +++ b/ofdgo_rich_text.go @@ -30,7 +30,9 @@ var ErrNotRichText = errors.New("object is not rich text") // RichText 保存有序文字片段及段落选项,长度和范围使用Unicode码点而非字节 // Paragraphs为空时所有段落沿用Layout,否则数量必须与LF分隔的段落一致 +// AutoWidth在不折行时按各段自然宽度确定文本框,指定宽度调整后关闭 type RichText struct { + AutoWidth bool `json:"autoWidth,omitempty"` Runs []RichTextRun `json:"runs"` Layout TextLayout `json:"layout"` Paragraphs []RichTextParagraph `json:"paragraphs,omitempty"`