fix(编辑能力): 修正文本框自适应与对齐
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-27 23:45:29 +08:00
1 parent d384527371
commit 7e3f2c468a
6 files changed
+59 -9

No files matched your search

+1 -1
View File
@@ -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 }] });
+12 -7
View File
@@ -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) {
+1 -1
View File
@@ -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,
+10
View File
@@ -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 {
+33
View File
@@ -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
+2
View File
@@ -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"`