Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2025 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 : #include "textlayout.h"
23 : #include "tlayout.h"
24 : #include "dom/box.h"
25 : #include "dom/page.h"
26 : #include "dom/staff.h"
27 : #include "dom/textlinebase.h"
28 : #include "types/typesconv.h"
29 :
30 : using namespace mu::engraving::rendering::score;
31 : using namespace mu::engraving;
32 : using namespace muse::draw;
33 :
34 28794 : void TextLayout::layoutBaseTextBase(const TextBase* item, TextBase::LayoutData* ldata)
35 : {
36 28794 : IF_ASSERT_FAILED(item->ownershipParent()) {
37 0 : return;
38 : }
39 :
40 28794 : ldata->setPos(PointF());
41 :
42 28794 : if (item->placeBelow()) {
43 22396 : ldata->setPosY(item->staff() ? item->staff()->staffHeight(item->tick()) : 0.0);
44 : }
45 :
46 28794 : layoutBaseTextBase1(item, ldata);
47 : }
48 :
49 242 : void TextLayout::layoutBaseTextBase(TextBase* item, LayoutContext&)
50 : {
51 242 : layoutBaseTextBase(item, item->mutldata());
52 242 : }
53 :
54 66841 : void TextLayout::layoutBaseTextBase1(const TextBase* item, TextBase::LayoutData* ldata)
55 : {
56 66856 : TRACEFUNC;
57 :
58 66841 : if (item->layoutParent() && item->layoutToParentWidth()) {
59 11933 : LD_CONDITION(item->layoutParent()->ldata()->isSetBbox());
60 : }
61 :
62 66841 : if (ldata->layoutInvalid) {
63 23725 : item->createBlocks(ldata);
64 : }
65 :
66 66841 : double y = 0.0;
67 :
68 66841 : double maxBlockWidth = -DBL_MAX;
69 : // adjust the bounding box for the text item
70 134240 : for (size_t i = 0; i < ldata->blocks.size(); ++i) {
71 67399 : TextBlock& t = ldata->blocks[i];
72 67399 : layoutTextBlock(&t, item);
73 67399 : y += t.lineSpacing();
74 67399 : t.setY(y);
75 67399 : maxBlockWidth = std::max(maxBlockWidth, t.boundingRect().width());
76 : }
77 :
78 66841 : Shape shape;
79 66841 : textHorizontalLayout(item, shape, maxBlockWidth, ldata);
80 :
81 66841 : RectF bb = shape.bbox();
82 :
83 66841 : double yoff = 0;
84 66841 : double h = 0;
85 66841 : const EngravingItem* parent = item->layoutParent();
86 66841 : if (parent) {
87 65308 : if (item->layoutToParentWidth()) {
88 11933 : if (parent->isTBox()) {
89 : // hack: vertical alignment is always TOP
90 131 : const_cast<TextBase*>(item)->setAlign({ item->align().horizontal, AlignV::TOP });
91 11802 : } else if (parent->isBox()) {
92 : // consider inner margins of frame
93 11562 : const Box* b = toBox(parent);
94 11562 : yoff = b->topMargin() * DPMM;
95 :
96 11562 : if (b->height() < bb.bottom()) {
97 95 : h = b->height() / 2 + bb.height();
98 : } else {
99 11467 : h = b->height() - yoff - b->bottomMargin() * DPMM;
100 : }
101 240 : } else if (parent->isPage()) {
102 240 : const Page* p = toPage(parent);
103 240 : h = p->height() - p->tm() - p->bm();
104 240 : yoff = p->tm();
105 0 : } else if (parent->isMeasure()) {
106 : } else {
107 0 : h = parent->height();
108 : }
109 : }
110 : } else {
111 1533 : ldata->setPos(PointF());
112 : }
113 :
114 66841 : if (item->align() == AlignV::BOTTOM) {
115 3035 : yoff += h - bb.bottom();
116 63806 : } else if (item->align() == AlignV::VCENTER) {
117 3425 : yoff += (h - (bb.top() + bb.bottom())) * .5;
118 60381 : } else if (item->align() == AlignV::BASELINE) {
119 50490 : yoff += h * .5 - ldata->blocks.front().lineSpacing();
120 : } else {
121 9891 : yoff += -bb.top();
122 : }
123 :
124 134240 : for (TextBlock& t : ldata->blocks) {
125 67399 : t.setY(t.y() + yoff);
126 : }
127 :
128 66841 : shape.translateY(yoff);
129 66841 : ldata->setShape(shape);
130 :
131 66841 : if (item->hasFrame()) {
132 144 : item->layoutFrame(ldata);
133 : }
134 :
135 66841 : if (!item->isDynamic() && !(parent && parent->isBox())) {
136 53863 : computeTextHighResShape(item, ldata);
137 : }
138 :
139 66841 : if (!muse::RealIsNull(item->textAngle())) {
140 0 : Transform t;
141 0 : t.rotate(item->textAngle());
142 0 : ldata->setShape(shape.transform(t));
143 0 : ldata->highResShape.mut_value().transform(t);
144 : }
145 :
146 66841 : PointF defaultPos = item->defaultPos();
147 66841 : ldata->move(defaultPos);
148 66841 : }
149 :
150 33946 : void TextLayout::layoutBaseTextBase1(TextBase* item, const LayoutContext&)
151 : {
152 33946 : layoutBaseTextBase1(item, item->mutldata());
153 33946 : }
154 :
155 56043 : void TextLayout::computeTextHighResShape(const TextBase* item, TextBase::LayoutData* ldata)
156 : {
157 56043 : Shape& shape = ldata->highResShape.mut_value();
158 56043 : shape.clear();
159 56043 : shape.elements().reserve(item->xmlText().size());
160 :
161 112392 : for (const TextBlock& block : ldata->blocks) {
162 56349 : double y = block.y();
163 112058 : for (const TextFragment& fragment : block.fragments()) {
164 55709 : FontMetrics fontMetrics = FontMetrics(fragment.font(item));
165 55709 : double x = fragment.pos.x();
166 55709 : size_t textSize = fragment.text.size();
167 332067 : for (size_t i = 0; i < textSize; ++i) {
168 276358 : Char character = fragment.text.at(i);
169 276358 : String text{ character };
170 276358 : if (character.isHighSurrogate() && i + 1 < textSize) {
171 388 : text += fragment.text.at(i + 1);
172 388 : i++;
173 : }
174 276358 : RectF characterBoundingRect = fontMetrics.tightBoundingRect(text);
175 276358 : characterBoundingRect.translate(x, y);
176 276358 : shape.add(characterBoundingRect, item);
177 276358 : if (i + 1 < textSize) {
178 220666 : x += fontMetrics.horizontalAdvance(text);
179 : }
180 276358 : }
181 55709 : }
182 : }
183 :
184 56043 : ldata->highResShape = shape;
185 56043 : }
186 :
187 66841 : void TextLayout::textHorizontalLayout(const TextBase* item, Shape& shape, double maxBlockWidth, TextBase::LayoutData* ldata)
188 : {
189 66841 : double leftMargin = 0.0;
190 66841 : double layoutWidth = 0;
191 66841 : EngravingItem* parent = item->layoutParent();
192 66841 : if (parent && item->layoutToParentWidth()) {
193 11933 : layoutWidth = parent->width();
194 11933 : switch (parent->type()) {
195 11693 : case ElementType::HBOX:
196 : case ElementType::VBOX:
197 : case ElementType::TBOX: {
198 11693 : Box* b = toBox(parent);
199 11693 : layoutWidth -= ((b->leftMargin() + b->rightMargin()) * DPMM);
200 11693 : leftMargin = b->leftMargin() * DPMM;
201 : }
202 11693 : break;
203 240 : case ElementType::PAGE: {
204 240 : Page* p = toPage(parent);
205 240 : layoutWidth -= (p->lm() + p->rm());
206 240 : leftMargin = p->lm();
207 : }
208 240 : break;
209 : break;
210 0 : default:
211 0 : ASSERT_X("Lay out to parent width only valid for items with page or frame as parent");
212 : }
213 54908 : } else if (item->positionRelativeToNoteheadRest()) {
214 : // For items aligned to notehead/rests
215 9616 : double mag = item->staff() ? item->staff()->staffMag(item) : 1.0;
216 9616 : layoutWidth = item->symWidth(SymId::noteheadBlack) * mag;
217 : }
218 :
219 : // Position and alignment
220 134240 : for (size_t i = 0; i < ldata->blocks.size(); ++i) {
221 67399 : TextBlock& textBlock = ldata->blocks[i];
222 67399 : double xAdj = leftMargin - textBlock.boundingRect().left();
223 :
224 : // Set position relative to reference point
225 67399 : AlignH position = item->position();
226 67399 : if (position == AlignH::HCENTER) {
227 18973 : xAdj += (layoutWidth - maxBlockWidth) * .5;
228 48426 : } else if (position == AlignH::RIGHT) {
229 37786 : xAdj += layoutWidth - maxBlockWidth;
230 : }
231 :
232 67399 : double diff = maxBlockWidth - textBlock.boundingRect().width();
233 67399 : if (muse::RealIsNull(diff)) {
234 : // This is the longest line, don't align
235 132971 : for (TextFragment& f : textBlock.fragments()) {
236 66125 : f.pos.rx() += xAdj;
237 : }
238 66846 : textBlock.shape().translate(PointF(xAdj, 0.0));
239 66846 : shape.add(textBlock.shape().translated(PointF(0.0, textBlock.y())));
240 66846 : continue;
241 66846 : }
242 :
243 : // Align relative to the longest line
244 553 : AlignH alignH = item->align().horizontal;
245 553 : if (alignH == AlignH::HCENTER) {
246 185 : xAdj += diff * 0.5;
247 368 : } else if (alignH == AlignH::RIGHT) {
248 147 : xAdj += diff;
249 221 : } else if (alignH == AlignH::JUSTIFY) {
250 : // Don't justify final line
251 0 : if (i != ldata->blocks.size() - 1) {
252 0 : justifyLine(item, &textBlock, maxBlockWidth);
253 : }
254 : }
255 :
256 1150 : for (TextFragment& fragment : textBlock.fragments()) {
257 597 : fragment.pos.rx() += xAdj;
258 : }
259 553 : textBlock.shape().translate(PointF(xAdj, 0.0));
260 553 : shape.add(textBlock.shape().translated(PointF(0.0, textBlock.y())));
261 : }
262 66841 : }
263 :
264 0 : void TextLayout::justifyLine(const TextBase* item, TextBlock* textBlock, double maxBlockWidth)
265 : {
266 : struct SubFragment {
267 : String text;
268 : CharFormat format;
269 : double width;
270 : };
271 0 : std::vector<SubFragment> subfrags;
272 0 : for (const TextFragment& f : textBlock->fragments()) {
273 0 : String current;
274 0 : FontMetrics fm(f.font(item));
275 0 : for (size_t i = 0; i < f.text.size(); ++i) {
276 0 : Char c = f.text.at(i);
277 0 : current += c;
278 0 : if (c.isSpace()) {
279 0 : double w = fm.width(current);
280 0 : subfrags.push_back({ current, f.format, w });
281 0 : current.clear();
282 : }
283 : }
284 0 : if (!current.isEmpty()) {
285 0 : double w = fm.width(current);
286 0 : subfrags.push_back({ current, f.format, w });
287 : }
288 0 : }
289 :
290 0 : int numSpaces = 0;
291 0 : double textWidth = 0;
292 0 : for (const SubFragment& sf : subfrags) {
293 0 : numSpaces++;
294 0 : textWidth += sf.width; // accumulate text width
295 : }
296 0 : numSpaces--;
297 0 : double spaceToFill = maxBlockWidth - textWidth;
298 0 : if (numSpaces > 0 && spaceToFill > 0) {
299 0 : textBlock->fragments().clear();
300 0 : double extraSpacing = spaceToFill / numSpaces;
301 0 : double x = 0.0;
302 0 : for (const SubFragment& sf : subfrags) {
303 0 : TextFragment frag;
304 0 : frag.text = sf.text;
305 0 : frag.format = sf.format;
306 0 : frag.pos.rx() = x;
307 0 : textBlock->fragments().push_back(frag);
308 0 : x += sf.width + extraSpacing;
309 0 : }
310 : }
311 0 : }
312 :
313 67399 : void TextLayout::layoutTextBlock(TextBlock* item, const TextBase* t)
314 : {
315 67399 : std::list<TextFragment>& fragments = item->fragments();
316 67399 : Shape& shape = item->shape();
317 67399 : shape.clear();
318 67399 : double x = 0.0;
319 67399 : double lineSpacing = 0.0;
320 :
321 67399 : if (fragments.empty()) {
322 1944 : FontMetrics fm = t->fontMetrics();
323 1944 : shape.add(RectF(0.0, -fm.ascent(), 1.0, fm.ascent()), t);
324 1944 : lineSpacing = fm.lineSpacing();
325 67399 : } else if (fragments.size() == 1 && fragments.front().text.isEmpty()) {
326 130 : auto fi = fragments.begin();
327 130 : TextFragment& f = *fi;
328 130 : f.pos.setX(x);
329 130 : Font font = f.font(t);
330 130 : substituteMusicFont(font, f.calculatedFontSize(t));
331 130 : FontMetrics fm(font);
332 130 : if (f.format.valign() != VerticalAlignment::AlignNormal) {
333 0 : double voffset = fm.xHeight() / SUBSCRIPT_SIZE; // use original height
334 0 : if (f.format.valign() == VerticalAlignment::AlignSubScript) {
335 0 : voffset *= SUBSCRIPT_OFFSET;
336 : } else {
337 0 : voffset *= SUPERSCRIPT_OFFSET;
338 : }
339 :
340 0 : f.pos.setY(voffset);
341 : } else {
342 130 : f.pos.setY(0.0);
343 : }
344 :
345 130 : RectF temp(0.0, -fm.ascent(), 1.0, fm.ascent());
346 130 : shape.add(temp, t);
347 130 : lineSpacing = std::max(lineSpacing, fm.lineSpacing());
348 130 : } else {
349 65325 : const auto fiLast = --fragments.end();
350 131917 : for (auto fi = fragments.begin(); fi != fragments.end(); ++fi) {
351 66592 : TextFragment& f = *fi;
352 66592 : f.pos.setX(x);
353 66592 : Font fragmentFont = f.font(t);
354 66592 : FontMetrics fm(fragmentFont);
355 66592 : if (f.format.valign() != VerticalAlignment::AlignNormal) {
356 2 : double voffset = fm.xHeight() / SUBSCRIPT_SIZE; // use original height
357 2 : if (f.format.valign() == VerticalAlignment::AlignSubScript) {
358 0 : voffset *= SUBSCRIPT_OFFSET;
359 : } else {
360 2 : voffset *= SUPERSCRIPT_OFFSET;
361 : }
362 2 : f.pos.setY(voffset);
363 : } else {
364 66590 : f.pos.setY(0.0);
365 : }
366 :
367 : // Optimization: don't calculate character position
368 : // for the next fragment if there is no next fragment
369 66592 : if (fi != fiLast) {
370 1267 : const double w = fm.width(f.text);
371 1267 : x += w;
372 : }
373 :
374 66592 : double yOffset = musicSymbolBaseLineAdjust(item, t, f, fi);
375 66592 : f.pos.ry() -= yOffset;
376 :
377 66592 : RectF textBRect = fm.tightBoundingRect(f.text).translated(f.pos);
378 66592 : bool useDynamicSymShape = fragmentFont.type() == Font::Type::MusicSymbol && t->isDynamic();
379 66592 : if (useDynamicSymShape) {
380 1284 : const Dynamic* dyn = toDynamic(t);
381 1284 : SymId symId = TConv::symId(dyn->dynamicType());
382 1284 : if (symId != SymId::noSym) {
383 1262 : shape.add(dyn->symShapeWithCutouts(symId).translated(f.pos));
384 : } else {
385 22 : shape.add(textBRect, t);
386 : }
387 : } else {
388 65308 : shape.add(textBRect, t);
389 : }
390 66592 : if (fragmentFont.type() == Font::Type::MusicSymbol || fragmentFont.type() == Font::Type::MusicSymbolText) {
391 : // SEMI-HACK: Music fonts can have huge linespacing because of tall symbols, so instead of using the
392 : // font linespacing value we just use the height of the individual fragment with some added margin
393 :
394 2830 : lineSpacing = std::max(lineSpacing, 1.25 * (shape.bbox().height() - shape.bbox().bottom()) + yOffset);
395 : } else {
396 63762 : lineSpacing = std::max(lineSpacing, fm.lineSpacing());
397 : }
398 66592 : }
399 : }
400 :
401 : // Apply style/custom line spacing
402 67399 : lineSpacing *= t->textLineSpacing();
403 67399 : item->setLineSpacing(lineSpacing);
404 67399 : }
405 :
406 130 : void TextLayout::substituteMusicFont(Font& font, double size)
407 : {
408 130 : if (font.type() == Font::Type::MusicSymbol) {
409 0 : String textFontId(font.family().id() + String(u" Text"));
410 0 : font.setFamily(textFontId, Font::Type::MusicSymbolText);
411 0 : font.setPointSizeF(size);
412 130 : } else if (font.type() == Font::Type::MusicSymbolText) {
413 0 : font.setPointSizeF(size);
414 : }
415 130 : }
416 :
417 66592 : double TextLayout::musicSymbolBaseLineAdjust(const TextBlock* block, const TextBase* t, const TextFragment& f,
418 : const std::list<TextFragment>::iterator fi)
419 : {
420 66592 : Font fragmentFont = f.font(t);
421 66592 : FontMetrics fm(fragmentFont);
422 66592 : const bool adjustSymbol = fragmentFont.type() == Font::Type::MusicSymbolText && t->isMarker();
423 66592 : if (!adjustSymbol) {
424 66334 : return 0.0;
425 : }
426 :
427 : // Align the x-height of the coda symbol to half the x-height of the surrounding text
428 258 : Font refFont;
429 258 : if (block->fragments().size() == 1) {
430 236 : refFont = t->font();
431 : } else {
432 44 : TextFragment& refFragment = fi != block->fragments().begin() ? *(std::prev(fi)) : *(std::next(fi));
433 22 : refFont = refFragment.font(t);
434 : }
435 258 : FontMetrics refFm(refFont);
436 :
437 258 : const double middle = (fm.tightBoundingRect(f.text).height() / 2) - fm.tightBoundingRect(f.text).bottom();
438 258 : const double refXHeight = refFm.capHeight() / 2;
439 258 : return refXHeight - middle;
440 66592 : }
|