mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
1 parent
d384527371
commit
7e3f2c468a
6 files changed
+59
-9
No files matched your search
@@ -4314,7 +4314,7 @@ async function beginCanvasText(index, box, surface, page) {
|
|||||||
const node = document.createElement("div");
|
const node = document.createElement("div");
|
||||||
const item = { ...state.textDefaults, ...box, wrap: state.textDefaults.wrapOverride ?? box.wrap ?? state.textDefaults.wrap,
|
const item = { ...state.textDefaults, ...box, wrap: state.textDefaults.wrapOverride ?? box.wrap ?? state.textDefaults.wrap,
|
||||||
index, page, surface, node, text: "", draft: true, fontData: data };
|
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()}`;
|
const key = `temp-font-${crypto.randomUUID()}`;
|
||||||
model.runs[0].format.font = key;
|
model.runs[0].format.font = key;
|
||||||
canvasEditor.editText(item, face, null, { model, faces: new Map([[key, face]]), fonts: [{ key, data }] });
|
canvasEditor.editText(item, face, null, { model, faces: new Map([[key, face]]), fonts: [{ key, data }] });
|
||||||
|
|||||||
@@ -390,7 +390,8 @@ export class RichTextSession {
|
|||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
this.rendering = true;
|
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",
|
Object.assign(input.style, { whiteSpace: layout.wrap ? "pre-wrap" : "pre",
|
||||||
lineHeight: layout.lineHeight ? `${layout.lineHeight * PX_PER_MM}px` : "normal",
|
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` });
|
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 align = this.model.paragraphs?.[index]?.align || this.model.layout.align || "left";
|
||||||
const distribute = align === "distributed" && !CSS.supports("text-justify", "inter-character");
|
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]);
|
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) {
|
if (!paragraph) {
|
||||||
paragraph = document.createElement("div");
|
paragraph = document.createElement("div");
|
||||||
for (const {text, format} of parts) {
|
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,
|
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" });
|
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);
|
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++;
|
if (index) offset++;
|
||||||
const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
|
const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
|
||||||
let node;
|
let node;
|
||||||
@@ -437,9 +444,7 @@ export class RichTextSession {
|
|||||||
positions.push({ node, start: offset, end: offset + node.textContent.length });
|
positions.push({ node, start: offset, end: offset + node.textContent.length });
|
||||||
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.fontSize = this.model.runs.length ? "0px" : `${this.typing.size * PX_PER_MM}px`;
|
||||||
input.style.caretColor = this.typing.color || "#000000";
|
input.style.caretColor = this.typing.color || "#000000";
|
||||||
this.editing.range = null;
|
this.editing.range = null;
|
||||||
@@ -1929,7 +1934,7 @@ export class CanvasEditor {
|
|||||||
const { input, frame, item, rich } = editing;
|
const { input, frame, item, rich } = editing;
|
||||||
const layout = rich?.model.layout || item;
|
const layout = rich?.model.layout || item;
|
||||||
const width = Math.max(0, frame.width - (layout.leftIndent || 0) - (layout.rightIndent || 0)) * PX_PER_MM + 4;
|
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) {
|
setTextError(error) {
|
||||||
|
|||||||
@@ -5071,7 +5071,7 @@ func (e *businessEngine) browserRichText(value ofdgo.RichText) map[string]any {
|
|||||||
paragraphs[i] = map[string]any{"align": paragraph.Align}
|
paragraphs[i] = map[string]any{"align": paragraph.Align}
|
||||||
}
|
}
|
||||||
layout := value.Layout
|
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,
|
"autoBidi": layout.AutoBidi, "shape": layout.Shape,
|
||||||
"shaping": map[string]any{
|
"shaping": map[string]any{
|
||||||
"Bidi": layout.Shaping.Bidi, "Direction": layout.Shaping.Direction,
|
"Bidi": layout.Shaping.Bidi, "Direction": layout.Shaping.Direction,
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ func (e *Editor) updateRichTextFrame(page int, id string, value RichText, offset
|
|||||||
if err := value.Validate(); err != nil {
|
if err := value.Validate(); err != nil {
|
||||||
return err
|
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)) {
|
if old.Type == "CompositeObject" && offset == 0 && (width == 0 || width == frame.W) && reflect.DeepEqual(normalizeRichText(current), normalizeRichText(value)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -356,6 +359,9 @@ func (e *Editor) updateCompositeRichText(page int, path ObjectPath, index int, v
|
|||||||
if err := value.Validate(); err != nil {
|
if err := value.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if width > 0 {
|
||||||
|
value.AutoWidth = false
|
||||||
|
}
|
||||||
if offset == 0 && (width == 0 || width == frame.W) && reflect.DeepEqual(normalizeRichText(current), normalizeRichText(value)) {
|
if offset == 0 && (width == 0 || width == frame.W) && reflect.DeepEqual(normalizeRichText(current), normalizeRichText(value)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -460,6 +466,10 @@ func (e *Editor) prepareRichText(box Box, value RichText) (GraphicObject, richTe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return GraphicObject{}, richTextMetadata{}, err
|
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)
|
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}
|
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 {
|
if !finite(extent.W) || !finite(extent.H) || extent.W <= 0 || extent.H <= 0 {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
|
|
||||||
// richLayout 保存局部标准对象、含墨迹的边界及各片段的字体锚点序号
|
// richLayout 保存局部标准对象、含墨迹的边界及各片段的字体锚点序号
|
||||||
type richLayout struct {
|
type richLayout struct {
|
||||||
|
width float64
|
||||||
objects []GraphicObject
|
objects []GraphicObject
|
||||||
bounds Box
|
bounds Box
|
||||||
anchors []int
|
anchors []int
|
||||||
@@ -124,6 +125,38 @@ func (e *Editor) layoutRichText(value RichText, width float64) (richLayout, erro
|
|||||||
}
|
}
|
||||||
chars := []rune(value.Text())
|
chars := []rune(value.Text())
|
||||||
var face *typefont.Face
|
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 underlays []GraphicObject
|
||||||
var shadows []GraphicObject
|
var shadows []GraphicObject
|
||||||
var y float64
|
var y float64
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ var ErrNotRichText = errors.New("object is not rich text")
|
|||||||
|
|
||||||
// RichText 保存有序文字片段及段落选项,长度和范围使用Unicode码点而非字节
|
// RichText 保存有序文字片段及段落选项,长度和范围使用Unicode码点而非字节
|
||||||
// Paragraphs为空时所有段落沿用Layout,否则数量必须与LF分隔的段落一致
|
// Paragraphs为空时所有段落沿用Layout,否则数量必须与LF分隔的段落一致
|
||||||
|
// AutoWidth在不折行时按各段自然宽度确定文本框,指定宽度调整后关闭
|
||||||
type RichText struct {
|
type RichText struct {
|
||||||
|
AutoWidth bool `json:"autoWidth,omitempty"`
|
||||||
Runs []RichTextRun `json:"runs"`
|
Runs []RichTextRun `json:"runs"`
|
||||||
Layout TextLayout `json:"layout"`
|
Layout TextLayout `json:"layout"`
|
||||||
Paragraphs []RichTextParagraph `json:"paragraphs,omitempty"`
|
Paragraphs []RichTextParagraph `json:"paragraphs,omitempty"`
|
||||||
|
|||||||
Reference in new issue
Block a user