LCOV - code coverage report
Current view: top level - src/engraving/rendering/score - textlayout.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 79.0 % 271 214
Test Date: 2026-11-05 04:07:02

            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 : }
        

Generated by: LCOV version 2.0-1