LCOV - code coverage report
Current view: top level - src/engraving/rendering/score - lyricslayout.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 92.2 % 500 461
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) 2023 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 "lyricslayout.h"
      23              : 
      24              : #include "dom/factory.h"
      25              : #include "dom/repeatlist.h"
      26              : #include "style/styledef.h"
      27              : 
      28              : #include "dom/chordrest.h"
      29              : #include "dom/lyrics.h"
      30              : #include "dom/measure.h"
      31              : #include "dom/score.h"
      32              : #include "dom/segment.h"
      33              : #include "dom/staff.h"
      34              : #include "dom/stafftype.h"
      35              : #include "dom/system.h"
      36              : 
      37              : #include "tlayout.h"
      38              : #include "textlayout.h"
      39              : 
      40              : using namespace mu;
      41              : using namespace mu::engraving;
      42              : using namespace mu::engraving::rendering::score;
      43              : 
      44         2180 : void LyricsLayout::layout(Lyrics* item, LayoutContext& ctx)
      45              : {
      46         2180 :     if (!item->ownershipParent()) {   // palette & clone trick
      47            0 :         item->setPos(PointF());
      48            0 :         TextLayout::layoutBaseTextBase1(item, ctx);
      49            0 :         return;
      50              :     }
      51              : 
      52         2180 :     Lyrics::LayoutData* ldata = item->mutldata();
      53              : 
      54              :     //
      55              :     // parse leading verse number and/or punctuation, so we can factor it into layout separately
      56              :     //
      57         2180 :     bool hasNumber = false;   // _verseNumber;
      58              : 
      59              :     // find:
      60              :     // 1) string of numbers and non-word characters at start of syllable
      61              :     // 2) at least one other character (indicating start of actual lyric)
      62              :     // 3) string of non-word characters at end of syllable
      63              :     //QRegularExpression leadingPattern("(^[\\d\\W]+)([^\\d\\W]+)");
      64              : 
      65         2180 :     const String text = item->plainText();
      66         2180 :     String leading;
      67         2180 :     String trailing;
      68              : 
      69         2180 :     if (ctx.conf().styleB(Sid::lyricsAlignVerseNumber)) {
      70         2180 :         size_t leadingIdx = 0;
      71         2338 :         for (size_t i = 0; i < text.size(); ++i) {
      72         2268 :             Char ch = text.at(i);
      73         2268 :             if (ch.isLetter()) {
      74         2110 :                 leadingIdx = i;
      75         2110 :                 break;
      76              :             }
      77              :         }
      78              : 
      79         2180 :         if (leadingIdx != 0) {
      80           30 :             leading = text.mid(0, leadingIdx);
      81              :         }
      82              : 
      83         2180 :         size_t trailingIdx = text.size() - 1;
      84         2566 :         for (int i = static_cast<int>(text.size() - 1); i >= 0; --i) {
      85         2496 :             Char ch = text.at(i);
      86         2496 :             if (ch.isLetter()) {
      87         2110 :                 trailingIdx = i;
      88         2110 :                 break;
      89              :             }
      90              :         }
      91              : 
      92         2180 :         if (trailingIdx != text.size() - 1) {
      93          288 :             trailing = text.mid(trailingIdx + 1);
      94              :         }
      95              : 
      96         2180 :         if (!leading.isEmpty() && leading.at(0).isDigit()) {
      97           24 :             hasNumber = true;
      98              :         }
      99              :     }
     100              : 
     101         2180 :     createOrRemoveLyricsLine(item, ctx);
     102              : 
     103         2180 :     if (item->isMelisma() || hasNumber) {
     104              :         // use the melisma style alignment setting
     105          182 :         if (item->isStyled(Pid::POSITION)) {
     106          182 :             if (ctx.conf().styleB(Sid::lyricsCenterDashedSyllables) && !(item->separator() && item->separator()->isEndMelisma())) {
     107            0 :                 item->setPosition(AlignH::HCENTER);
     108              :             } else {
     109          182 :                 item->setPosition(ctx.conf().styleV(Sid::lyricsMelismaAlign).value<Align>().horizontal);
     110              :             }
     111              :         }
     112              :     } else {
     113              :         // use the text style alignment setting
     114         1998 :         if (item->isStyled(Pid::POSITION)) {
     115         1973 :             item->setPosition(item->propertyDefault(Pid::POSITION).value<AlignH>());
     116              :         }
     117              :     }
     118              : 
     119              :     // Negate ChordRest offset
     120         2180 :     ChordRest* cr = item->chordRest();
     121         2180 :     double x = -cr->x();
     122              : 
     123              :     /* Preserve the vertical position: a lyric's Y is set in computeVerticalPositions(). Necessary
     124              :      * because layoutBaseTextBase1() ends with ldata->move(defaultPos), which accumulates a downward
     125              :      * shift of the lyric if the current function is called in a partially-relaid system without a
     126              :      * following computeVerticalPositions() call. */
     127         2180 :     const double posY = ldata->pos().y();
     128         2180 :     TextLayout::layoutBaseTextBase1(item, ctx);
     129         2180 :     TextLayout::computeTextHighResShape(item, ldata);
     130         2180 :     ldata->setPosY(posY);
     131              : 
     132         2180 :     double centerAdjust = 0.0;
     133         2180 :     double leftAdjust   = 0.0;
     134              : 
     135         2180 :     if (ctx.conf().styleB(Sid::lyricsAlignVerseNumber)) {
     136              :         // Calculate leading and trailing parts widths. Lyrics
     137              :         // should have text layout to be able to do it correctly.
     138         2180 :         DO_ASSERT(ldata->blocks.size() != 0);
     139         2180 :         if (!leading.isEmpty() || !trailing.isEmpty()) {
     140              : //                   LOGD("create leading, trailing <%s> -- <%s><%s>", muPrintable(text), muPrintable(leading), muPrintable(trailing));
     141          315 :             const TextBlock& tb = ldata->blocks.at(0);
     142              : 
     143          315 :             const double leadingWidth = tb.xpos(leading.size(), item) - tb.boundingRect().x();
     144          315 :             const size_t trailingPos = text.size() - trailing.size();
     145          315 :             const double trailingWidth = tb.boundingRect().right() - tb.xpos(trailingPos, item);
     146              : 
     147          315 :             leftAdjust = leadingWidth;
     148          315 :             centerAdjust = leadingWidth - trailingWidth;
     149              :         }
     150              :     }
     151              : 
     152         2180 :     if (item->position() == AlignH::HCENTER) {
     153              :         //
     154              :         // center under notehead, not origin
     155              :         // however, lyrics that are melismas or have verse numbers will be forced to left alignment
     156              :         //
     157              :         // center under note head
     158         1973 :         x += -centerAdjust * 0.5;
     159          207 :     } else if (item->position() == AlignH::LEFT) {
     160              :         // even for left aligned syllables, ignore leading verse numbers and/or punctuation
     161          207 :         x -= leftAdjust;
     162              :     }
     163              : 
     164         2180 :     ldata->setPosX(x);
     165              : 
     166         2180 :     if (item->ticks().isNotZero()) {
     167              :         // set melisma end
     168          100 :         ChordRest* ecr = ctx.mutDom().findCR(item->endTick(), item->track());
     169          100 :         if (ecr) {
     170          100 :             ecr->setMelismaEnd(true);
     171              :         }
     172              :     }
     173         2180 : }
     174              : 
     175          328 : void LyricsLayout::layout(LyricsLine* item)
     176              : {
     177          328 :     if (item->isDash()) {    // dash(es)
     178          228 :         item->setNextLyrics(searchNextLyrics(item->lyrics()->segment(),
     179              :                                              item->staffIdx(),
     180          228 :                                              item->lyrics()->verse(),
     181          228 :                                              item->lyrics()->placement()
     182              :                                              ));
     183          228 :         item->setTrack2(item->nextLyrics() ? item->nextLyrics()->track() : item->track());
     184              : 
     185          228 :         Fraction endTick = item->tick();
     186          228 :         const Measure* lyricsMeasure = item->lyrics()->segment()->measure();
     187          228 :         const Segment* endCRSeg = lyricsMeasure ? lyricsMeasure->last(SegmentType::ChordRest) : nullptr;
     188              : 
     189          228 :         const ChordRest* endCR = nullptr;
     190          228 :         if (endCRSeg && !endCRSeg->empty()) {
     191          526 :             for (EngravingItem* cr : endCRSeg->elist()) {
     192          526 :                 if (cr && cr->isChordRest()) {
     193          228 :                     endCR = toChordRest(cr);
     194          228 :                     break;
     195              :                 }
     196              :             }
     197              :         }
     198              : 
     199          228 :         if (item->nextLyrics()) {
     200          223 :             endTick = item->nextLyrics()->tick();
     201            5 :         } else if (endCR && endCR->hasFollowingJumpItem()) {
     202            0 :             endTick = endCR->tick();
     203              :         }
     204              : 
     205          228 :         item->setTick2(endTick);
     206              :     }
     207          328 : }
     208              : 
     209          348 : void LyricsLayout::layout(LyricsLineSegment* item, LayoutContext& ctx)
     210              : {
     211              :     UNUSED(ctx);
     212              : 
     213          348 :     assert(item->isPartialLyricsLineSegment() || item->lyrics());
     214              : 
     215          348 :     LyricsLineSegment::LayoutData* ldata = item->mutldata();
     216          348 :     ldata->clearDashes();
     217              : 
     218          348 :     if (item->lyricsLine()->isEndMelisma()) {
     219          117 :         layoutMelismaLine(item);
     220              :     } else {
     221          231 :         layoutDashes(item);
     222              :     }
     223              : 
     224          348 :     double halfLineWidth = item->absoluteFromSpatium(item->lineWidth());
     225          348 :     RectF rect(PointF(), item->pos2());
     226          348 :     rect.adjust(0.0, -halfLineWidth, 0.0, halfLineWidth);
     227              : 
     228          348 :     ldata->setShape(Shape(rect, item));
     229          348 : }
     230              : 
     231          117 : void LyricsLayout::layoutMelismaLine(LyricsLineSegment* item)
     232              : {
     233          117 :     const bool isPartialLyricsLine = item->isPartialLyricsLineSegment();
     234          117 :     LyricsLine* lyricsLine = item->lyricsLine();
     235          117 :     Lyrics* startLyrics = lyricsLine->lyrics();
     236              : 
     237          117 :     System* system = item->system();
     238          117 :     if (!system) {
     239            0 :         return;
     240              :     }
     241          117 :     const MStyle& style = item->style();
     242              : 
     243          117 :     double startX = lyricsLineStartX(item);
     244          117 :     double endX = lyricsLineEndX(item);
     245              : 
     246          117 :     double tolerance = 0.05 * item->spatium();
     247          117 :     if (endX - startX < style.styleAbsolute(Sid::lyricsMelismaMinLength) - tolerance) {
     248            3 :         const Fraction ticks = isPartialLyricsLine ? lyricsLine->ticks() : startLyrics->ticks();
     249            3 :         if (style.styleB(Sid::lyricsMelismaForce) || ticks == Lyrics::TEMP_MELISMA_TICKS) {
     250            0 :             endX = startX + style.styleAbsolute(Sid::lyricsMelismaMinLength);
     251              :         } else {
     252            3 :             endX = startX;
     253              :         }
     254              :     }
     255              : 
     256          117 :     adjustLyricsLineYOffset(item);
     257              : 
     258          117 :     item->mutldata()->setPosX(startX);
     259          117 :     item->setPos2(PointF(endX - startX, 0.0));
     260              : 
     261          117 :     item->mutldata()->addDash(LineF(PointF(), item->pos2()));
     262              : }
     263              : 
     264          231 : void LyricsLayout::layoutDashes(LyricsLineSegment* item)
     265              : {
     266          231 :     const bool isPartialLyricsLine = item->isPartialLyricsLineSegment();
     267          231 :     LyricsLine* lyricsLine = item->lyricsLine();
     268              : 
     269          231 :     ChordRest* endCR = lyricsLine->endElement()
     270          231 :                        && lyricsLine->endElement()->isChordRest() ? toChordRest(lyricsLine->endElement()) : nullptr;
     271          231 :     Lyrics* endLyrics = nullptr;
     272          231 :     if (endCR) {
     273          315 :         for (Lyrics* lyr : endCR->lyrics()) {
     274          315 :             if (lyr->verse() == item->verse()) {
     275          231 :                 endLyrics = lyr;
     276          231 :                 break;
     277              :             }
     278              :         }
     279              :     }
     280              : 
     281              :     // When the end element is a timetick segment rather than a chordrest, the start element should be a chord with a following repeat
     282          231 :     ChordRest* startCR = lyricsLine->startElement()
     283          231 :                          && lyricsLine->startElement()->isChordRest() ? toChordRest(lyricsLine->startElement()) : nullptr;
     284          231 :     bool hasFollowingJump = endCR ? endCR->hasFollowingJumpItem() : (startCR ? startCR->hasFollowingJumpItem() : false);
     285              : 
     286          231 :     if (!endLyrics && !isPartialLyricsLine && !hasFollowingJump) {
     287            0 :         return;
     288              :     }
     289              : 
     290          231 :     System* system = item->system();
     291          231 :     if (!system) {
     292            0 :         return;
     293              :     }
     294          231 :     const MStyle& style = item->style();
     295              : 
     296          231 :     double startX = lyricsLineStartX(item);
     297          231 :     double endX = 0.0;
     298          231 :     if (endCR) {
     299          231 :         endX = lyricsLineEndX(item, endLyrics);
     300              :     } else {
     301            0 :         endX = startCR ? startCR->measure()->endingXForOpenEndedLines() : endX;
     302              :     }
     303              : 
     304          231 :     adjustLyricsLineYOffset(item, endLyrics);
     305              : 
     306          231 :     item->mutldata()->setPosX(startX);
     307          231 :     item->setPos2(PointF(endX - startX, 0.0));
     308              : 
     309          231 :     bool isDashOnFirstSyllable = lyricsLine->tick2() == system->firstMeasure()->tick();
     310          231 :     const double userStart = startX + item->offset().x();
     311          231 :     const double userEnd = endX + item->userOff2().x();
     312          231 :     double curLength = userEnd - userStart;
     313          231 :     double dashMinLength = style.styleAbsolute(Sid::lyricsDashMinLength);
     314          231 :     bool firstAndLastGapAreHalf = style.styleB(Sid::lyricsDashFirstAndLastGapAreHalf);
     315          231 :     bool forceDash = style.styleB(Sid::lyricsDashForce)
     316          231 :                      || (style.styleB(Sid::lyricsShowDashIfSyllableOnFirstNote) && isDashOnFirstSyllable);
     317          231 :     double maxDashDistance = style.styleAbsolute(Sid::lyricsDashMaxDistance);
     318          231 :     int dashCount = firstAndLastGapAreHalf && curLength > maxDashDistance ? std::ceil(curLength / maxDashDistance)
     319          217 :                     : std::floor(curLength / maxDashDistance);
     320              : 
     321          231 :     if (curLength > dashMinLength || forceDash) {
     322          231 :         dashCount = std::max(dashCount, 1);
     323              :     }
     324              : 
     325          231 :     if (style.styleB(Sid::lyricsLimitDashCount)) {
     326            0 :         dashCount = std::min(dashCount, style.styleI(Sid::lyricsMaxDashCount));
     327              :     }
     328              : 
     329          231 :     if (curLength < dashMinLength && dashCount > 0) {
     330           13 :         double diff = dashMinLength - curLength;
     331           13 :         if (isDashOnFirstSyllable) {
     332            0 :             startX -= diff;
     333              :         } else {
     334           13 :             startX -= 0.5 * diff;
     335           13 :             endX += 0.5 * diff;
     336              :         }
     337           13 :         item->mutldata()->setPosX(startX);
     338           13 :         item->setPos2(PointF(endX - startX, 0.0));
     339           13 :         curLength = endX - startX;
     340              :     }
     341              : 
     342          231 :     double dashWidth = std::min(curLength, style.styleAbsolute(Sid::lyricsDashMaxLength));
     343              : 
     344          231 :     LyricsDashSystemStart lyricsDashSystemStart = style.styleV(Sid::lyricsDashPosAtStartOfSystem).value<LyricsDashSystemStart>();
     345          231 :     bool dashesLeftAligned = lyricsDashSystemStart != LyricsDashSystemStart::STANDARD && !item->isSingleBeginType();
     346          231 :     double dashDist = curLength / (dashesLeftAligned || firstAndLastGapAreHalf ? dashCount : dashCount + 1);
     347          231 :     double xDash = 0.0;
     348          231 :     if (dashesLeftAligned) {
     349            0 :         for (int i = 0; i < dashCount; ++i) {
     350            0 :             item->mutldata()->addDash(LineF(PointF(xDash, 0.0), PointF(xDash + dashWidth, 0.0)));
     351            0 :             xDash += dashDist;
     352              :         }
     353              :     } else {
     354          479 :         for (int i = 0; i < dashCount; ++i) {
     355          248 :             if (firstAndLastGapAreHalf && i == 0) {
     356          231 :                 xDash += 0.5 * dashDist;
     357              :             } else {
     358           17 :                 xDash += dashDist;
     359              :             }
     360          248 :             item->mutldata()->addDash(LineF(PointF(xDash - 0.5 * dashWidth, 0.0), PointF(xDash + 0.5 * dashWidth, 0.0)));
     361              :         }
     362              :     }
     363              : }
     364              : 
     365           20 : Lyrics* LyricsLayout::findNextLyrics(const ChordRest* endChordRest, int verseNumber, std::optional<PlacementV> placement)
     366              : {
     367           20 :     if (!endChordRest) {
     368            0 :         return nullptr;
     369              :     }
     370           76 :     for (Segment* segment = endChordRest->segment()->next1(SegmentType::ChordRest); segment;
     371           56 :          segment = segment->next1(SegmentType::ChordRest)) {
     372           61 :         if (!segment->element(endChordRest->track())) {
     373           20 :             continue;
     374              :         }
     375           41 :         ChordRest* nextCR = toChordRest(segment->element(endChordRest->track()));
     376           41 :         for (Lyrics* lyr : nextCR->lyrics()) {
     377            5 :             if (lyr->verse() == verseNumber && (!placement || lyr->placement() == placement)) {
     378            5 :                 return lyr;
     379              :             }
     380              :         }
     381              :     }
     382              : 
     383           15 :     return nullptr;
     384              : }
     385              : 
     386         2180 : void LyricsLayout::createOrRemoveLyricsLine(Lyrics* item, LayoutContext& ctx)
     387              : {
     388         2180 :     if (item->needRemoveInvalidSegments()) {
     389            2 :         item->removeInvalidSegments();
     390              :     }
     391              : 
     392         4360 :     auto isEndMelisma = [item]() {
     393         4360 :         return item->ticks().isNotZero();
     394         2180 :     };
     395              : 
     396         2180 :     Fraction lyricsLineTicks = Lyrics::TEMP_MELISMA_TICKS;
     397              : 
     398              :     // Update the end tick
     399         2180 :     if (isEndMelisma()) {
     400          100 :         const track_idx_t track = item->track();
     401              : 
     402          100 :         const Segment* const startSegment = item->segment();
     403          100 :         const Fraction startTick = startSegment->tick();
     404              : 
     405              :         // if lyrics has a temporary one-chord melisma, interpret as 0 ticks (just its own chord)
     406          100 :         Fraction itemEndTick = item->ticks() == Lyrics::TEMP_MELISMA_TICKS ? startTick : item->endTick();
     407          100 :         Fraction endTick = itemEndTick;
     408              : 
     409          100 :         const Segment* endSegment = startSegment;
     410          356 :         while (endSegment && endSegment->tick() < endTick) {
     411          256 :             endSegment = endSegment->nextCR(track, true);
     412              :         }
     413          100 :         if (!endSegment) {
     414              :             // user probably deleted measures at end of score, leaving this melisma too long
     415              :             // set endSegment to last segment and reset lyricsEndTick to trigger FIXUP code below
     416            2 :             endSegment = ctx.dom().lastSegment();
     417            2 :             endTick = Fraction(-1, 1);
     418              :         }
     419              : 
     420          100 :         EngravingItem* endSegmentElement = endSegment->element(track);
     421          100 :         if (endSegment->tick() == endTick && endSegmentElement && endSegmentElement->isChord()) {
     422              :             // everything is OK if we have reached a chord at right tick on right track
     423              :             // advance to next CR after duration of note, or last segment if no next CR
     424           98 :             const Segment* endChordSeg = endSegment;
     425           98 :             const Chord* endChord = toChord(endSegmentElement);
     426              : 
     427           98 :             endSegment = endChordSeg->nextCR(track, false);
     428              : 
     429           98 :             if (!endSegment || endSegment->tick() > endChord->endTick()) {
     430            7 :                 endSegment = endChordSeg;
     431           26 :                 while (endSegment && endSegment->tick() < endChord->endTick()) {
     432           19 :                     endSegment = endSegment->nextCR(muse::nidx, true);
     433              :                 }
     434              :             }
     435              :         } else {
     436              :             // FIXUP - lyrics tick count not valid
     437              :             // this happens if edits to score have removed the original end segment
     438              :             // so let's fix it here
     439              :             // endSegment is already pointing to segment past endTick (or to last segment)
     440              :             // we should shorten the lyrics tick count to make this work
     441            2 :             const Segment* ns = endSegment;
     442            2 :             const Segment* ps = endSegment->prev1(SegmentType::ChordRest);
     443            5 :             while (ps && ps != startSegment) {
     444            5 :                 EngravingItem* pe = ps->element(track);
     445              :                 // we're looking for an actual chord on this track
     446            5 :                 if (pe && pe->isChord()) {
     447            2 :                     break;
     448              :                 }
     449            3 :                 endSegment = ps;
     450            3 :                 ps = ps->prev1(SegmentType::ChordRest);
     451              :             }
     452              : 
     453            2 :             if (!ps || ps == startSegment) {
     454              :                 // either there is no valid previous CR, or the previous CR is the one the lyric starts on
     455              :                 // we don't want to make the melisma longer arbitrarily, but there is a possibility that the next
     456              :                 // CR won't extend the melisma, so let's check it
     457            0 :                 ps = ns;
     458            0 :                 endSegment = ps->nextCR(track, false);
     459            0 :                 EngravingItem* e = endSegment ? endSegment->element(track) : nullptr;
     460              : 
     461              :                 // check to make sure we have a chord
     462            0 :                 if (!e || !e->isChord() || ps->tick() > itemEndTick) {
     463              :                     // nope, nothing to do but set ticks to 0
     464              :                     // this will result in no melisma
     465            0 :                     item->undoChangeProperty(Pid::LYRIC_TICKS, Fraction::fromTicks(0));
     466              :                 } else {
     467            0 :                     item->undoChangeProperty(Pid::LYRIC_TICKS, ps->tick() - startTick);
     468              :                 }
     469            0 :             } else {
     470            2 :                 item->undoChangeProperty(Pid::LYRIC_TICKS, ps->tick() - startTick);
     471              :             }
     472              :         }
     473              : 
     474          100 :         if (endSegment) {
     475              :             // Lyrics::_ticks points to the beginning of the last spanned segment,
     476              :             // but the line shall include it:
     477              :             // include the duration of this last segment in the melisma duration
     478           93 :             lyricsLineTicks = endSegment->tick() - startTick;
     479              :         } else {
     480            7 :             lyricsLineTicks = item->score()->endTick() - startTick;
     481              :         }
     482              :     }
     483              : 
     484         2180 :     ChordRest* cr = item->chordRest();
     485              : 
     486         2180 :     if (isEndMelisma() || item->syllabic() == LyricsSyllabic::BEGIN || item->syllabic() == LyricsSyllabic::MIDDLE) {
     487          328 :         if (!item->separator()) {
     488          243 :             LyricsLine* separator = Factory::createLyricsLine(ctx.mutDom().dummyParent());
     489          243 :             separator->setGenerated(true);
     490          243 :             separator->setTick(cr->tick());
     491          243 :             separator->setVisible(item->visible());
     492          243 :             item->setSeparator(separator);
     493          243 :             ctx.mutDom().addUnmanagedSpanner(item->separator());
     494              :         }
     495          328 :         item->separator()->setOwnershipParent(item);
     496          328 :         item->separator()->setTick(cr->tick());
     497          328 :         item->separator()->setTicks(lyricsLineTicks);
     498          328 :         item->separator()->setTrack(item->track());
     499          328 :         item->separator()->setTrack2(item->track());
     500          328 :         item->separator()->styleChanged();
     501              :     } else {
     502         1852 :         if (item->separator()) {
     503            0 :             item->separator()->removeUnmanaged();
     504            0 :             delete item->separator();
     505            0 :             item->setSeparator(nullptr);
     506              :         }
     507              :     }
     508         2180 : }
     509              : 
     510         7388 : void LyricsLayout::computeVerticalPositions(System* system, LayoutContext& ctx)
     511              : {
     512         7388 :     staff_idx_t nStaves = system->score()->nstaves();
     513              : 
     514         7388 :     std::vector<staff_idx_t> visibleStaves;
     515         7388 :     visibleStaves.reserve(system->staves().size());
     516              : 
     517        18583 :     for (staff_idx_t staffIdx = 0; staffIdx < nStaves; ++staffIdx) {
     518        11195 :         if (system->staff(staffIdx)->show()) {
     519        10992 :             computeVerticalPositions(staffIdx, system, ctx);
     520              :         }
     521              :     }
     522         7388 : }
     523              : 
     524        10992 : void LyricsLayout::computeVerticalPositions(staff_idx_t staffIdx, System* system, LayoutContext& ctx)
     525              : {
     526        10992 :     LyricsVersesMap lyricsVersesAbove;
     527        10992 :     LyricsVersesMap lyricsVersesBelow;
     528              : 
     529        10992 :     collectLyricsVerses(staffIdx, system, lyricsVersesAbove, lyricsVersesBelow);
     530              : 
     531        10992 :     setDefaultPositions(staffIdx, lyricsVersesAbove, lyricsVersesBelow, ctx);
     532              : 
     533        10992 :     checkCollisionsWithStaffElements(system, staffIdx, ctx, lyricsVersesAbove, lyricsVersesBelow);
     534              : 
     535        10992 :     addToSkyline(system, staffIdx, ctx, lyricsVersesAbove, lyricsVersesBelow);
     536        10992 : }
     537              : 
     538        10992 : void LyricsLayout::collectLyricsVerses(staff_idx_t staffIdx, System* system, LyricsVersesMap& lyricsVersesAbove,
     539              :                                        LyricsVersesMap& lyricsVersesBelow)
     540              : {
     541        10992 :     track_idx_t startTrack = staffIdx * VOICES;
     542        10992 :     track_idx_t endTrack = startTrack + VOICES;
     543              : 
     544        57035 :     for (MeasureBase* mb : system->measures()) {
     545        46043 :         if (!mb->isMeasure()) {
     546          184 :             continue;
     547              :         }
     548       219094 :         for (Segment& segment : toMeasure(mb)->segments()) {
     549       173235 :             if (!segment.isChordRestType()) {
     550        82903 :                 continue;
     551              :             }
     552       451660 :             for (track_idx_t track = startTrack; track < endTrack; ++track) {
     553       361328 :                 EngravingItem* element = segment.element(track);
     554       361328 :                 if (!element) {
     555       271645 :                     continue;
     556              :                 }
     557        91863 :                 for (Lyrics* lyrics : toChordRest(element)->lyrics()) {
     558         2180 :                     int verse = lyrics->verse();
     559         2180 :                     if (lyrics->placeAbove()) {
     560           16 :                         lyricsVersesAbove[verse].addLyrics(lyrics);
     561              :                     } else {
     562         2164 :                         lyricsVersesBelow[verse].addLyrics(lyrics);
     563              :                     }
     564              :                 }
     565              :             }
     566              :         }
     567              :     }
     568              : 
     569        20782 :     for (SpannerSegment* spannerSegment : system->spannerSegments()) {
     570         9790 :         if (spannerSegment->staffIdx() == staffIdx && spannerSegment->isLyricsLineSegment()) {
     571          348 :             if (muse::RealIsNull(spannerSegment->pos2().x())) {
     572            3 :                 continue;
     573              :             }
     574          345 :             LyricsLineSegment* lyricsLineSegment = toLyricsLineSegment(spannerSegment);
     575          345 :             int verse = lyricsLineSegment->verse();
     576          345 :             if (lyricsLineSegment->lyricsPlaceAbove()) {
     577            4 :                 lyricsVersesAbove[verse].addLine(lyricsLineSegment);
     578              :             } else {
     579          341 :                 lyricsVersesBelow[verse].addLine(lyricsLineSegment);
     580              :             }
     581              :         }
     582              :     }
     583        10992 : }
     584              : 
     585        10992 : void LyricsLayout::setDefaultPositions(staff_idx_t staffIdx, const LyricsVersesMap& lyricsVersesAbove,
     586              :                                        const LyricsVersesMap& lyricsVersesBelow,
     587              :                                        LayoutContext& ctx)
     588              : {
     589        10992 :     double staffHeight = ctx.dom().staff(staffIdx)->staffHeight();
     590        10992 :     double lyricsLineHeightFactor = ctx.conf().styleD(Sid::lyricsLineHeight);
     591              : 
     592        10992 :     int maxVerseAbove = !lyricsVersesAbove.empty() ? lyricsVersesAbove.crbegin()->first : 0;
     593        11000 :     for (auto& pair : lyricsVersesAbove) {
     594            8 :         int verse = pair.first;
     595            8 :         const LyricsVerse& lyricsVerse = pair.second;
     596           24 :         for (Lyrics* lyrics : lyricsVerse.lyrics()) {
     597           16 :             double y = -(maxVerseAbove - verse) * lyrics->lineSpacing() * lyricsLineHeightFactor;
     598           16 :             PointF defaultPos = lyrics->defaultPos();
     599           16 :             y += defaultPos.y();
     600           16 :             lyrics->setYRelativeToStaff(y);
     601              :         }
     602           12 :         for (LyricsLineSegment* lyricsLineSegment : lyricsVerse.lines()) {
     603            4 :             double y = -(maxVerseAbove - verse) * lyricsLineSegment->lineSpacing() * lyricsLineHeightFactor;
     604            4 :             lyricsLineSegment->move(PointF(0.0, y + lyricsLineSegment->baseLineShift()));
     605              :         }
     606              :     }
     607              : 
     608        11516 :     for (auto& pair : lyricsVersesBelow) {
     609          524 :         int verse = pair.first;
     610          524 :         const LyricsVerse& lyricsVerse = pair.second;
     611         2688 :         for (Lyrics* lyrics : lyricsVerse.lyrics()) {
     612         2164 :             double y = staffHeight + verse * lyrics->lineSpacing() * lyricsLineHeightFactor;
     613         2164 :             PointF defaultPos = lyrics->defaultPos();
     614         2164 :             y += defaultPos.y();
     615         2164 :             lyrics->setYRelativeToStaff(y);
     616              :         }
     617          865 :         for (LyricsLineSegment* lyricsLineSegment : lyricsVerse.lines()) {
     618          341 :             double y = staffHeight + verse * lyricsLineSegment->lineSpacing() * lyricsLineHeightFactor;
     619          341 :             lyricsLineSegment->move(PointF(0.0, y + lyricsLineSegment->baseLineShift()));
     620              :         }
     621              :     }
     622        10992 : }
     623              : 
     624        10992 : void LyricsLayout::checkCollisionsWithStaffElements(System* system, staff_idx_t staffIdx,  LayoutContext& ctx,
     625              :                                                     const LyricsVersesMap& lyricsVersesAbove,
     626              :                                                     const LyricsVersesMap& lyricsVersesBelow)
     627              : {
     628        10992 :     SysStaff* systemStaff = system->staff(staffIdx);
     629              : 
     630        10992 :     double lyricsMinDist = ctx.conf().styleAbsolute(Sid::lyricsMinTopDistance);
     631              : 
     632        10992 :     SkylineLine& staffSkylineNorth = systemStaff->skyline().north();
     633        10992 :     SkylineLine& staffSkylineSouth = systemStaff->skyline().south();
     634              : 
     635        10992 :     int maxVerseAbove = !lyricsVersesAbove.empty() ? lyricsVersesAbove.crbegin()->first : 0;
     636        10992 :     int maxVerseBelow = !lyricsVersesBelow.empty() ? lyricsVersesBelow.crbegin()->first : 0;
     637              : 
     638        21984 :     for (int verse = maxVerseAbove; verse >= 0; --verse) {
     639        10992 :         if (lyricsVersesAbove.count(verse) == 0) {
     640        10984 :             continue;
     641              :         }
     642            8 :         SkylineLine verseSkyline = createSkylineForVerse(verse, false, lyricsVersesAbove, system);
     643            8 :         double minDistance = -verseSkyline.minDistance(staffSkylineNorth);
     644            8 :         if (minDistance < lyricsMinDist) {
     645            6 :             double diff = lyricsMinDist - minDistance;
     646            6 :             moveThisVerseAndOuterOnes(verse, 0, true, -diff, lyricsVersesAbove);
     647              :         }
     648            8 :     }
     649              : 
     650        22046 :     for (int verse = 0; verse <= maxVerseBelow; ++verse) {
     651        11054 :         if (lyricsVersesBelow.count(verse) == 0) {
     652        10530 :             continue;
     653              :         }
     654          524 :         SkylineLine verseSkyline = createSkylineForVerse(verse, true, lyricsVersesBelow, system);
     655          524 :         double minDistance = -staffSkylineSouth.minDistance(verseSkyline);
     656          524 :         if (minDistance < lyricsMinDist) {
     657          384 :             double diff = lyricsMinDist - minDistance;
     658          384 :             moveThisVerseAndOuterOnes(verse, maxVerseBelow, false, diff, lyricsVersesBelow);
     659              :         }
     660          524 :     }
     661        10992 : }
     662              : 
     663          532 : SkylineLine LyricsLayout::createSkylineForVerse(int verse, bool north, const LyricsVersesMap& lyricsVerses, System* system)
     664              : {
     665          532 :     double systemX = system->pageX();
     666              : 
     667          532 :     SkylineLine lyricsSkyline(north);
     668              : 
     669          532 :     if (lyricsVerses.count(verse) > 0) {
     670          532 :         const LyricsVerse& lyricsVerse = lyricsVerses.at(verse);
     671         2712 :         for (Lyrics* lyrics : lyricsVerse.lyrics()) {
     672         2180 :             if (lyrics->addToSkyline()) {
     673         2165 :                 Shape lyricsShape = lyrics->highResShape().translated(PointF(lyrics->pageX() - systemX, lyrics->yRelativeToStaff()));
     674         2165 :                 lyricsSkyline.add(lyricsShape);
     675         2165 :             }
     676              :         }
     677          877 :         for (LyricsLineSegment* lyricsLineSeg : lyricsVerse.lines()) {
     678          345 :             if (lyricsLineSeg->lyricsAddToSkyline()) {
     679          345 :                 lyricsSkyline.add(lyricsLineSeg->shape().translate(lyricsLineSeg->pos()));
     680              :             }
     681              :         }
     682              :     }
     683              : 
     684          532 :     return lyricsSkyline;
     685            0 : }
     686              : 
     687          390 : void LyricsLayout::moveThisVerseAndOuterOnes(int verse, int lastVerse, bool above, double diff, const LyricsVersesMap& lyricsVerses)
     688              : {
     689          424 :     auto moveVerse = [&](int verse) {
     690          424 :         if (lyricsVerses.count(verse) > 0) {
     691          424 :             const LyricsVerse& lyricsVerse = lyricsVerses.at(verse);
     692         2341 :             for (Lyrics* lyrics : lyricsVerse.lyrics()) {
     693         1917 :                 lyrics->move(PointF(0.0, diff));
     694              :             }
     695          699 :             for (LyricsLineSegment* lyricsLineSeg : lyricsVerse.lines()) {
     696          275 :                 lyricsLineSeg->move(PointF(0.0, diff));
     697              :             }
     698              :         }
     699          424 :     };
     700              : 
     701          390 :     if (above) {
     702           12 :         for (int otherVerse = verse; otherVerse >= lastVerse; --otherVerse) {
     703            6 :             moveVerse(otherVerse);
     704              :         }
     705              :     } else {
     706          802 :         for (int otherVerse = verse; otherVerse <= lastVerse; ++otherVerse) {
     707          418 :             moveVerse(otherVerse);
     708              :         }
     709              :     }
     710          390 : }
     711              : 
     712        10992 : void LyricsLayout::addToSkyline(System* system, staff_idx_t staffIdx, LayoutContext& ctx, const LyricsVersesMap& lyricsVersesAbove,
     713              :                                 const LyricsVersesMap& lyricsVersesBelow)
     714              : {
     715        10992 :     double systemX = system->pageX();
     716              :     // HACK: subtract minVerticalDistance here because it's added later during staff distance calculations. Needs a better solution.
     717        10992 :     double lyricsVerticalPadding = ctx.conf().styleAbsolute(Sid::lyricsMinBottomDistance) - ctx.conf().styleAbsolute(
     718        10992 :         Sid::minVerticalDistance);
     719        10992 :     Skyline& skyline = system->staff(staffIdx)->skyline();
     720        11000 :     for (auto& pair : lyricsVersesAbove) {
     721            8 :         const LyricsVerse& lyricsVerse = pair.second;
     722           24 :         for (Lyrics* lyrics : lyricsVerse.lyrics()) {
     723           16 :             if (lyrics->addToSkyline()) {
     724              :                 Shape lyricsShape
     725           16 :                     = lyrics->highResShape().translated(PointF(lyrics->pageX() - systemX, lyrics->yRelativeToStaff()));
     726           16 :                 skyline.north().add(lyricsShape.adjust(0.0, -lyricsVerticalPadding, 0.0, 0.0));
     727           16 :             }
     728              :         }
     729           12 :         for (LyricsLineSegment* lyricsLineSeg : lyricsVerse.lines()) {
     730            4 :             if (lyricsLineSeg->lyricsAddToSkyline()) {
     731            4 :                 Shape lineShape = lyricsLineSeg->shape().translate(lyricsLineSeg->pos());
     732            4 :                 skyline.north().add(lineShape.adjust(0.0, -lyricsVerticalPadding, 0.0, 0.0));
     733            4 :             }
     734              :         }
     735              :     }
     736        11516 :     for (auto& pair : lyricsVersesBelow) {
     737          524 :         const LyricsVerse& lyricsVerse = pair.second;
     738         2688 :         for (Lyrics* lyrics : lyricsVerse.lyrics()) {
     739         2164 :             if (lyrics->addToSkyline()) {
     740              :                 Shape lyricsShape
     741         2149 :                     = lyrics->highResShape().translated(PointF(lyrics->pageX() - systemX, lyrics->yRelativeToStaff()));
     742         2149 :                 skyline.south().add(lyricsShape.adjust(0.0, 0.0, 0.0, lyricsVerticalPadding));
     743         2149 :             }
     744              :         }
     745          865 :         for (LyricsLineSegment* lyricsLineSeg : lyricsVerse.lines()) {
     746          341 :             if (lyricsLineSeg->lyricsAddToSkyline()) {
     747          341 :                 Shape lineShape = lyricsLineSeg->shape().translate(lyricsLineSeg->pos());
     748          341 :                 skyline.south().add(lineShape.adjust(0.0, 0.0, 0.0, lyricsVerticalPadding));
     749          341 :             }
     750              :         }
     751              :     }
     752        10992 : }
     753              : 
     754          348 : double LyricsLayout::lyricsLineStartX(const LyricsLineSegment* item)
     755              : {
     756          348 :     const System* system = item->system();
     757          348 :     const LyricsLine* lyricsLine = item->lyricsLine();
     758          348 :     const Lyrics* startLyrics = lyricsLine->lyrics();
     759          348 :     const MStyle& style = item->style();
     760          348 :     const bool melisma = lyricsLine->isEndMelisma();
     761              : 
     762          348 :     const LyricsDashSystemStart lyricsDashSystemStart = style.styleV(Sid::lyricsDashPosAtStartOfSystem).value<LyricsDashSystemStart>();
     763          348 :     const bool leading = melisma || (lyricsDashSystemStart != LyricsDashSystemStart::UNDER_FIRST_NOTE);
     764              : 
     765              :     // Full melisma or dashes at beginning of system
     766          348 :     if (!item->isSingleBeginType()) {
     767           14 :         return system->firstNoteRestSegmentX(leading);
     768              :     }
     769              : 
     770              :     // Partial melisma or dashes
     771          334 :     if (lyricsLine->isPartialLyricsLine()) {
     772            6 :         const Measure* measure = lyricsLine->findStartMeasure();
     773            6 :         return measure->firstNoteRestSegmentX(leading);
     774              :     }
     775              : 
     776              :     // Full melisma or dashes in middle of system
     777          328 :     const double pad = melisma ? style.styleAbsolute(Sid::lyricsMelismaPad) : style.styleAbsolute(Sid::lyricsDashPad);
     778          328 :     const double lyricsRightEdge = startLyrics->pageX() - system->pageX() + startLyrics->shape().right();
     779          328 :     return lyricsRightEdge + pad;
     780              : }
     781              : 
     782          348 : double LyricsLayout::lyricsLineEndX(const LyricsLineSegment* item, const Lyrics* endLyrics)
     783              : {
     784          348 :     const System* system = item->system();
     785          348 :     const LyricsLine* lyricsLine = item->lyricsLine();
     786          348 :     const ChordRest* endChordRest = toChordRest(lyricsLine->endElement());
     787          348 :     if (!endChordRest) {
     788            0 :         return system->endingXForOpenEndedLines();
     789              :     }
     790          348 :     const double systemPageX = system->pageX();
     791          348 :     const MStyle& style = item->style();
     792          348 :     const bool melisma = lyricsLine->isEndMelisma();
     793          348 :     const bool hasFollowingJump = endChordRest->hasFollowingJumpItem();
     794          348 :     const bool hasPrecedingJump = endChordRest->hasPrecedingJumpItem();
     795              : 
     796          348 :     if (!melisma && !item->isPartialLyricsLineSegment() && !endLyrics && !hasFollowingJump) {
     797            0 :         return 0.0;
     798              :     }
     799              : 
     800          348 :     const bool dashesEndOfSystem = !melisma && lyricsLine->tick2() == system->endTick();
     801              : 
     802              :     // Full melisma or dashes at end of system
     803          348 :     if (!item->isSingleEndType() || dashesEndOfSystem || !lyricsLine->endElement()) {
     804           14 :         return system->endingXForOpenEndedLines();
     805              :     }
     806              : 
     807              :     // Partial dashes after a repeat
     808          334 :     if (!melisma && hasPrecedingJump && item->isPartialLyricsLineSegment()) {
     809            0 :         if (endLyrics) {
     810            0 :             const double lyricsLeftEdge = endLyrics->pageX() - systemPageX + endLyrics->ldata()->bbox().left();
     811            0 :             return lyricsLeftEdge - style.styleAbsolute(Sid::lyricsDashPad);
     812            0 :         } else if (endChordRest->isChord()) {
     813            0 :             const Chord* endChord = toChord(endChordRest);
     814            0 :             const Note* note = endChord->up() ? endChord->downNote() : endChord->upNote();
     815            0 :             return note->pageX() - systemPageX + note->headWidth();
     816              :         }
     817              :     }
     818              : 
     819              :     // Full dashes or melisma before a repeat - possibly with a partial dash/repeat on the other side of the repeat
     820          334 :     if (endChordRest && hasFollowingJump && !lyricsLine->nextLyrics()) {
     821            3 :         Measure* endMeasure = lyricsLine->findEndMeasure();
     822              :         // check if there is a partial lyrics line in a following measure
     823            3 :         if (repeatHasPartialLyricLine(endMeasure) || endChordRest == lyricsLine->startElement()) {
     824            3 :             return endMeasure->endingXForOpenEndedLines();
     825              :         }
     826              :     }
     827              : 
     828              :     // Full or partial dashes in middle of system
     829          331 :     if (!melisma && endLyrics) {
     830          231 :         const double lyricsLeftEdge = endLyrics->pageX() - systemPageX + endLyrics->ldata()->bbox().left();
     831          231 :         return lyricsLeftEdge - style.styleAbsolute(Sid::lyricsDashPad);
     832              :     }
     833              : 
     834              :     // Full or partial melisma in middle of system
     835          100 :     if (endChordRest->isChord()) {
     836          100 :         const Chord* endChord = toChord(endChordRest);
     837          100 :         const Note* note = endChord->up() ? endChord->downNote() : endChord->upNote();
     838          100 :         return note->pageX() - systemPageX + note->headWidth();
     839              :     }
     840              : 
     841            0 :     return endChordRest->pageX() - systemPageX + endChordRest->rightEdge();
     842              : }
     843              : 
     844          348 : void LyricsLayout::adjustLyricsLineYOffset(LyricsLineSegment* item, const Lyrics* endLyrics)
     845              : {
     846          348 :     const LyricsLine* lyricsLine = item->lyricsLine();
     847          348 :     ChordRest* endChordRest = lyricsLine->endElement()
     848          348 :                               && lyricsLine->endElement()->isChordRest() ? toChordRest(lyricsLine->endElement()) : nullptr;
     849          348 :     const Lyrics* startLyrics = lyricsLine->lyrics();
     850          348 :     const bool melisma = lyricsLine->isEndMelisma();
     851              : 
     852          348 :     LyricsLineSegment::LayoutData* ldata = item->mutldata();
     853              : 
     854              :     // Partial melisma or dashes
     855          348 :     if (lyricsLine->isPartialLyricsLine()) {
     856              :         // A partial lyrics line's placement is independent of the lyrics around it, so only
     857              :         // take the Y from a lyric which is on the same side of the staff:
     858            6 :         Lyrics* nextLyrics = findNextLyrics(endChordRest, item->verse(), lyricsLine->placement());
     859            6 :         if (nextLyrics) {
     860            3 :             PointF nextLyricsDefaultPos = nextLyrics->defaultPos();
     861            3 :             ldata->setPosY(nextLyrics->offset().y() + nextLyricsDefaultPos.y());
     862              :         } else {
     863            6 :             PointF lyricsOffset = item->styleValue(Pid::OFFSET,
     864            6 :                                                    item->placeBelow() ? Sid::lyricsPosBelow : Sid::lyricsPosAbove).value<PointF>();
     865            3 :             ldata->setPosY(lyricsOffset.y());
     866              :         }
     867          348 :         return;
     868              :     }
     869              : 
     870          342 :     if (item->isSingleBeginType()) {
     871          328 :         PointF startLyricsDefaultPos = startLyrics->defaultPos();
     872          328 :         ldata->setPosY(startLyrics->offset().y() + startLyricsDefaultPos.y());
     873          328 :         return;
     874              :     }
     875              : 
     876           14 :     if (melisma || !endLyrics) {
     877           14 :         const Lyrics* nextLyrics = findNextLyrics(endChordRest, item->verse());
     878              : 
     879           14 :         const Lyrics* refLyrics = nextLyrics ? nextLyrics : startLyrics;
     880           14 :         PointF refLyricsDefaultPos = refLyrics->defaultPos();
     881              : 
     882           14 :         ldata->setPosY(refLyrics->offset().y() + refLyricsDefaultPos.y());
     883           14 :         return;
     884              :     }
     885              : 
     886            0 :     PointF endLyricsDefaultPos = endLyrics->defaultPos();
     887              : 
     888            0 :     ldata->setPosY(endLyrics->offset().y() + endLyricsDefaultPos.y());
     889              : }
        

Generated by: LCOV version 2.0-1