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 :
23 : #include "harmonylayout.h"
24 :
25 : #include "draw/fontmetrics.h"
26 :
27 : #include "dom/factory.h"
28 : #include "dom/fret.h"
29 : #include "dom/harmony.h"
30 : #include "dom/staff.h"
31 :
32 : #include "parenthesislayout.h"
33 : #include "textlayout.h"
34 : #include "tlayout.h"
35 :
36 : using namespace muse::draw;
37 : using namespace mu::engraving;
38 : using namespace mu::engraving::rendering::score;
39 :
40 3654 : void HarmonyLayout::layoutHarmony(Harmony* item, Harmony::LayoutData* ldata,
41 : const LayoutContext& ctx)
42 : {
43 3654 : if (!item->ownershipParent()) {
44 0 : ldata->setPos(0.0, 0.0);
45 0 : const_cast<Harmony*>(item)->setOffset(0.0, 0.0);
46 : }
47 :
48 3654 : if (!item->cursor()->editing()) {
49 3654 : render(item, ldata, ctx);
50 : }
51 :
52 3654 : if (ldata->layoutInvalid) {
53 1158 : item->createBlocks(ldata);
54 : }
55 :
56 3654 : if (ldata->blocks.empty()) {
57 0 : ldata->blocks.push_back(TextBlock());
58 : }
59 :
60 3654 : PointF positionPoint = calculateBoundingRect(item, ldata, ctx);
61 :
62 3654 : if (item->isPolychord()) {
63 62 : for (LineF& line : ldata->polychordDividerLines.mut_value()) {
64 32 : line.setP1(PointF(ldata->bbox().left(), line.y1()));
65 32 : line.setP2(PointF(ldata->bbox().right(), line.y2()));
66 : }
67 : }
68 :
69 3654 : if (item->hasFrame()) {
70 0 : item->layoutFrame(ldata);
71 : }
72 :
73 3654 : ldata->setPos(positionPoint);
74 :
75 3654 : if (!item->cursor()->editing() && !ldata->renderItemList.value().empty()) {
76 3560 : ParenthesisLayout::layoutParentheses(item, ctx);
77 : }
78 3654 : }
79 :
80 3654 : PointF HarmonyLayout::calculateBoundingRect(const Harmony* item, Harmony::LayoutData* ldata, const LayoutContext& ctx)
81 : {
82 3654 : const double ypos = (item->placeBelow() && item->staff()) ? item->staff()->staffHeight(item->tick()) : 0.0;
83 7308 : const FretDiagram* fd = (item->ownershipParent() && item->ownershipParent()->isFretDiagram())
84 7308 : ? toFretDiagram(item->ownershipParent())
85 3654 : : nullptr;
86 3654 : const bool alignToFretDiagram = fd && fd->visible();
87 :
88 3654 : const double standardNoteWidth = item->symWidth(SymId::noteheadBlack);
89 :
90 3654 : double newPosX = 0.0;
91 3654 : double newPosY = 0.0;
92 :
93 3654 : if (item->ldata()->renderItemList().empty()) {
94 94 : TextLayout::layoutBaseTextBase1(item, ldata);
95 :
96 94 : if (alignToFretDiagram) {
97 42 : newPosY = ldata->pos().y();
98 : } else {
99 52 : newPosY = ypos - ((item->align() == AlignV::BOTTOM) ? -ldata->bbox().height() : 0.0);
100 : }
101 : } else {
102 3560 : layoutModifierParentheses(item, ctx);
103 3560 : RectF bb;
104 3560 : RectF hAlignBox;
105 14306 : for (HarmonyRenderItem* renderItem : item->ldata()->renderItemList()) {
106 10746 : RectF tsBbox = renderItem->tightBoundingRect().translated(renderItem->x(), renderItem->y());
107 10746 : bb.unite(tsBbox);
108 :
109 10746 : if (renderItem->align()) {
110 10746 : hAlignBox.unite(tsBbox);
111 : }
112 : }
113 :
114 3560 : double xx = 0.0;
115 3560 : if (alignToFretDiagram) {
116 718 : switch (ctx.conf().styleV(Sid::chordAlignmentToFretboard).value<AlignH>()) {
117 0 : case AlignH::LEFT:
118 : case AlignH::JUSTIFY:
119 0 : xx = -hAlignBox.left();
120 0 : break;
121 718 : case AlignH::HCENTER:
122 718 : xx = -(hAlignBox.center().x());
123 718 : break;
124 0 : case AlignH::RIGHT:
125 0 : xx = -hAlignBox.right();
126 0 : break;
127 : }
128 : } else {
129 2842 : switch (item->position()) {
130 1659 : case AlignH::LEFT:
131 : case AlignH::JUSTIFY:
132 1659 : xx = -hAlignBox.left();
133 1659 : break;
134 1183 : case AlignH::HCENTER:
135 1183 : xx = -(hAlignBox.center().x());
136 1183 : break;
137 0 : case AlignH::RIGHT:
138 0 : xx = -hAlignBox.right();
139 0 : break;
140 : }
141 : }
142 :
143 3560 : double yy = -bb.y(); // Align::TOP
144 3560 : if (item->align() == AlignV::VCENTER) {
145 0 : yy = -bb.y() / 2.0;
146 3560 : } else if (item->align() == AlignV::BASELINE) {
147 3548 : yy = item->baseLine();
148 12 : } else if (item->align() == AlignV::BOTTOM) {
149 0 : yy = -bb.height() - bb.y();
150 : }
151 :
152 3560 : if (alignToFretDiagram) {
153 718 : double nutLineWidth = fd->ldata()->nutLineWidth;
154 718 : newPosY = yy - ctx.conf().styleAbsolute(Sid::harmonyFretDist) - nutLineWidth;
155 : } else {
156 2842 : newPosY = ypos;
157 : }
158 :
159 14306 : for (HarmonyRenderItem* renderItem : item->ldata()->renderItemList()) {
160 10746 : renderItem->setOffset(PointF(xx, yy));
161 : }
162 :
163 3560 : ldata->polychordDividerOffset = yy;
164 :
165 3560 : ldata->setBbox(bb.translated(xx, yy));
166 3560 : ldata->harmonyHeight = ldata->bbox().height();
167 : }
168 :
169 3654 : if (alignToFretDiagram) {
170 760 : switch (ctx.conf().styleV(Sid::chordAlignmentToFretboard).value<AlignH>()) {
171 0 : case AlignH::LEFT:
172 : case AlignH::JUSTIFY:
173 0 : newPosX = 0.0;
174 0 : break;
175 760 : case AlignH::HCENTER:
176 760 : newPosX = 0.5 * fd->mainWidth();
177 760 : break;
178 0 : case AlignH::RIGHT:
179 0 : newPosX = fd->mainWidth();
180 0 : break;
181 : }
182 : } else {
183 2894 : switch (item->position()) {
184 1659 : case AlignH::LEFT:
185 : case AlignH::JUSTIFY:
186 1659 : newPosX = 0.0;
187 1659 : break;
188 1235 : case AlignH::HCENTER:
189 1235 : newPosX = standardNoteWidth * 0.5;
190 1235 : break;
191 0 : case AlignH::RIGHT:
192 0 : newPosX = standardNoteWidth;
193 0 : break;
194 : }
195 : }
196 :
197 3654 : if (fd && !fd->visible()) {
198 : // Translate to base position around note
199 0 : newPosX -= fd->pos().x();
200 : }
201 :
202 3654 : PointF newPos = PointF(newPosX, newPosY);
203 :
204 3654 : if (!alignToFretDiagram) {
205 2894 : PointF defaultPos = item->defaultPos();
206 2894 : newPos += defaultPos;
207 : }
208 :
209 3654 : return newPos;
210 : }
211 :
212 3560 : void HarmonyLayout::layoutModifierParentheses(const Harmony* item, const LayoutContext& ctx)
213 : {
214 3560 : const double spatium = item->spatium();
215 3560 : const std::vector<HarmonyRenderItem*>& itemList = item->ldata()->renderItemList();
216 3560 : const double smuflParens = ctx.conf().styleB(Sid::harmonyParenUseSmuflSym);
217 : // Layout parentheses
218 3560 : std::vector<ChordSymbolParen*> openingParenStack;
219 3560 : double lastTextSegHeight = 0.0;
220 3560 : double lastTextSegTop = 0.0;
221 3560 : double rootRefHeight = 0.0;
222 3560 : double rootCapHeight = 0.0;
223 3560 : double parenExtension = 0.1 * spatium * (item->size() / 10.0);
224 14306 : for (HarmonyRenderItem* renderItem : itemList) {
225 10746 : if (ChordSymbolParen* curParen = dynamic_cast<ChordSymbolParen*>(renderItem)) {
226 64 : if (curParen->parenItem->direction() == DirectionH::LEFT) {
227 : // Opening paren
228 32 : openingParenStack.push_back(curParen);
229 32 : if (ctx.conf().styleB(Sid::harmonyParenUseSmuflSym)) {
230 0 : curParen->parenItem->mutldata()->symId = SymId::csymParensLeftTall;
231 : }
232 : } else {
233 : // Closing paren
234 32 : ChordSymbolParen* openingParen = openingParenStack.empty() ? nullptr : openingParenStack.back();
235 32 : if (!openingParen) {
236 0 : continue;
237 : }
238 32 : openingParenStack.pop_back();
239 32 : curParen->top = openingParen->top;
240 32 : curParen->bottom = openingParen->bottom;
241 :
242 : // Layout parenthesis pair
243 32 : double startY = openingParen->top - parenExtension;
244 32 : double height = (openingParen->bottom - openingParen->top) + 2 * parenExtension;
245 32 : if (std::isinf(height)) {
246 0 : height = lastTextSegHeight;
247 : }
248 32 : if (muse::RealIsEqual(DBL_MAX, startY)) {
249 0 : startY = lastTextSegTop;
250 : }
251 :
252 32 : if (smuflParens) {
253 0 : double symHeight = item->symHeight(SymId::csymParensRightTall);
254 0 : double scale = height / symHeight;
255 0 : curParen->parenItem->mutldata()->symId = SymId::csymParensRightTall;
256 0 : curParen->parenItem->mutldata()->symScale = scale;
257 0 : openingParen->parenItem->mutldata()->symScale = scale;
258 :
259 0 : curParen->sety(startY + height);
260 0 : openingParen->sety(startY + height);
261 :
262 0 : ParenthesisLayout::createSmuflShape(openingParen->parenItem, openingParen->parenItem->mutldata());
263 0 : ParenthesisLayout::createSmuflShape(curParen->parenItem, curParen->parenItem->mutldata());
264 : } else {
265 32 : const double scale = (height - 2 * parenExtension) / rootCapHeight;
266 32 : const double mag = openingParen->parenItem->ldata()->mag();
267 : static constexpr double HEIGHT_TO_WIDTH_RATIO = 20;
268 32 : const double midPointThickness = height / HEIGHT_TO_WIDTH_RATIO * mag * 1 / std::sqrt(scale);
269 32 : const double endPointThickness = 0.03;
270 32 : const double shoulder = 0.2 * height * std::pow(mag, 0.1) * 1 / std::sqrt(scale);
271 :
272 32 : openingParen->parenItem->mutldata()->startY = startY;
273 32 : openingParen->sety(startY);
274 32 : openingParen->parenItem->mutldata()->height = height;
275 32 : openingParen->parenItem->mutldata()->midPointThickness.set_value(midPointThickness);
276 32 : openingParen->parenItem->mutldata()->endPointThickness.set_value(endPointThickness);
277 32 : openingParen->parenItem->mutldata()->shoulderWidth = shoulder;
278 :
279 32 : curParen->parenItem->mutldata()->startY = startY;
280 32 : curParen->sety(startY);
281 32 : curParen->parenItem->mutldata()->height = height;
282 32 : curParen->parenItem->mutldata()->midPointThickness.set_value(midPointThickness);
283 32 : curParen->parenItem->mutldata()->endPointThickness.set_value(endPointThickness);
284 32 : curParen->parenItem->mutldata()->shoulderWidth = shoulder;
285 :
286 32 : ParenthesisLayout::createPathAndShape(openingParen->parenItem, openingParen->parenItem->mutldata());
287 32 : ParenthesisLayout::createPathAndShape(curParen->parenItem, curParen->parenItem->mutldata());
288 : }
289 :
290 32 : double closingPos = openingParen->closingParenPos;
291 32 : if (muse::RealIsEqual(-DBL_MAX, closingPos)) {
292 0 : closingPos = openingParen->x() + openingParen->boundingRect().width();
293 : }
294 32 : curParen->setx(closingPos);
295 :
296 : // Outer parens must always be the same length or longer than inner parens
297 32 : for (ChordSymbolParen* outerParen : openingParenStack) {
298 0 : outerParen->top = std::min(openingParen->top, outerParen->top);
299 0 : outerParen->bottom = std::max(openingParen->bottom, outerParen->bottom);
300 0 : outerParen->closingParenPos = std::max(openingParen->closingParenPos, outerParen->closingParenPos);
301 : }
302 : }
303 10682 : } else if (TextSegment* textSeg = dynamic_cast<TextSegment*>(renderItem)) {
304 : // Set top paren height
305 10682 : lastTextSegHeight = textSeg->height();
306 10682 : lastTextSegTop = textSeg->tightBoundingRect().translated(textSeg->pos()).y();
307 10682 : if (muse::RealIsNull(rootRefHeight)) {
308 3560 : rootRefHeight = textSeg->height();
309 3560 : double top = textSeg->tightBoundingRect().translated(textSeg->pos()).y();
310 3560 : double bottom = textSeg->pos().y();
311 3560 : double height = (bottom - top) + 2 * parenExtension;
312 :
313 3560 : rootRefHeight = height;
314 3560 : rootCapHeight = textSeg->capHeight();
315 : }
316 10682 : if (!openingParenStack.empty()) {
317 96 : ChordSymbolParen* topParen = openingParenStack.back();
318 96 : if (textSeg->font().type() != Font::Type::MusicSymbolText) {
319 96 : topParen->top = std::min(topParen->top, textSeg->tightBoundingRect().translated(textSeg->pos()).y());
320 96 : topParen->bottom = std::max(topParen->bottom, textSeg->pos().y());
321 : }
322 96 : topParen->closingParenPos = std::max(topParen->closingParenPos, textSeg->x() + textSeg->width());
323 96 : continue;
324 96 : }
325 : }
326 : }
327 :
328 7186 : auto computePadding = [](HarmonyRenderItem* item1, HarmonyRenderItem* item2) {
329 7186 : bool item1Paren = item1->type() == HarmonyRenderItemType::PAREN;
330 7186 : bool item2Paren = item2->type() == HarmonyRenderItemType::PAREN;
331 :
332 7186 : double padding = 0.0;
333 7186 : if (item1Paren && item2Paren) {
334 0 : padding = std::min(item1->rightPadding(), item2->leftPadding());
335 : } else {
336 7186 : padding = std::max(item1->rightPadding(), item2->leftPadding());
337 : }
338 :
339 7186 : double scaling = (item1->height() + item2->height()) / 2;
340 7186 : padding *= scaling;
341 :
342 7186 : return padding;
343 : };
344 :
345 : // Create space in text for parentheses
346 : // TODO - STACKED C7((#11)#13)
347 : // C7(#11(#13))
348 3560 : double additionalSpace = 0.0;
349 3560 : bool prevParen = false;
350 :
351 14306 : for (size_t i = 0; i < itemList.size(); i++) {
352 10746 : HarmonyRenderItem* renderItem = itemList.at(i);
353 10746 : double padding = i != 0 ? computePadding(itemList.at(i - 1), renderItem) : 0.0;
354 :
355 10746 : if (ChordSymbolParen* paren = dynamic_cast<ChordSymbolParen*>(renderItem)) {
356 64 : if (paren->parenItem->direction() == DirectionH::LEFT) {
357 32 : double heightToRefRatio = std::max(paren->height(), rootRefHeight) / rootRefHeight;
358 32 : bool scaleParenWidth = !smuflParens && !prevParen && !muse::RealIsEqual(heightToRefRatio, 1.0);
359 32 : const double PAREN_PADDING_SCALER = 0.6;
360 32 : double scaledParenWidth = paren->parenItem->width() * PAREN_PADDING_SCALER / heightToRefRatio;
361 :
362 32 : double parenWidth = scaleParenWidth ? scaledParenWidth : paren->parenItem->width();
363 :
364 32 : double parenAdjust = additionalSpace + padding + (smuflParens ? 0.0 : parenWidth);
365 32 : paren->movex(parenAdjust);
366 32 : additionalSpace += parenWidth + padding;
367 32 : } else if (paren->parenItem->direction() == DirectionH::RIGHT) {
368 32 : paren->movex(additionalSpace + padding);
369 32 : additionalSpace += paren->parenItem->width() + padding;
370 : }
371 64 : prevParen = true;
372 10682 : } else if (TextSegment* ts = dynamic_cast<TextSegment*>(renderItem)) {
373 10682 : additionalSpace += padding;
374 10682 : ts->movex(additionalSpace);
375 10682 : prevParen = false;
376 : }
377 : }
378 3560 : }
379 :
380 : //---------------------------------------------------------
381 : // render
382 : // construct Chord Symbol
383 : //---------------------------------------------------------
384 :
385 3654 : void HarmonyLayout::render(Harmony* item, Harmony::LayoutData* ldata, const LayoutContext& ctx)
386 : {
387 10902 : for (const HarmonyRenderItem* renderItem : ldata->renderItemList()) {
388 7248 : delete renderItem;
389 : }
390 3654 : ldata->renderItemList.mut_value().clear();
391 3654 : if (item->harmonyType() == HarmonyType::ROMAN) {
392 56 : renderRomanNumeral(item, ldata);
393 1889 : return;
394 : }
395 :
396 : // Render standard or Nashville chords
397 :
398 3598 : ChordList* chordList = item->score()->chordList();
399 :
400 3598 : ldata->fontList.mut_value().clear();
401 19862 : for (const ChordFont& cf : chordList->fonts) {
402 16264 : Font ff(item->font());
403 16264 : ff.setFamily(item->family(), Font::Type::Harmony);
404 :
405 16264 : double mag = item->mag() * cf.mag;
406 16264 : ff.setPointSizeF(ff.pointSizeF() * mag);
407 16264 : if (cf.musicSymbolText) {
408 3582 : ff.setFamily(cf.family, Font::Type::MusicSymbolText);
409 12682 : } else if (!(cf.family.isEmpty() || cf.family == "default")) {
410 8 : ff.setFamily(cf.family, Font::Type::Harmony);
411 : }
412 16264 : ldata->fontList.mut_value().push_back(ff);
413 16264 : }
414 3598 : if (ldata->fontList.mut_value().empty()) {
415 727 : Font ff(item->font());
416 727 : ff.setFamily(item->family(), Font::Type::Harmony);
417 :
418 727 : ldata->fontList.mut_value().push_back(ff);
419 727 : }
420 :
421 3598 : ldata->polychordDividerLines.reset();
422 3598 : HarmonyRenderCtx harmonyCtx;
423 :
424 : // Map of text segments and their final width
425 3598 : std::multimap<double, std::vector<HarmonyRenderItem*> > chordTextSegments;
426 :
427 3630 : for (size_t i = item->chords().size(); i > 0; i--) {
428 3630 : harmonyCtx.info = item->chords().at(i - 1);
429 3630 : renderSingleHarmony(item, ldata, harmonyCtx, ctx);
430 :
431 3630 : chordTextSegments.emplace(std::pair<double, std::vector<HarmonyRenderItem*> > { harmonyCtx.x(), harmonyCtx.renderItemList });
432 3630 : ldata->renderItemList.mut_value().insert(ldata->renderItemList.mut_value().end(),
433 : harmonyCtx.renderItemList.begin(), harmonyCtx.renderItemList.end());
434 :
435 : // Measure divider spacing from lowest baseline and highest cap-height in segments
436 3630 : double rootBaseline = harmonyCtx.renderItemList.empty() ? -DBL_MAX : harmonyCtx.renderItemList.front()->y();
437 3630 : double bottomBaseline = -DBL_MAX;
438 14320 : for (const HarmonyRenderItem* renderItem : harmonyCtx.renderItemList) {
439 10690 : if (const TextSegment* ts = dynamic_cast<const TextSegment*>(renderItem)) {
440 10626 : bottomBaseline = std::max(bottomBaseline, ts->y());
441 : }
442 : }
443 :
444 3630 : double diff = rootBaseline - bottomBaseline;
445 3630 : if (bottomBaseline > rootBaseline) {
446 36 : for (HarmonyRenderItem* renderItem : harmonyCtx.renderItemList) {
447 30 : if (TextSegment* ts = dynamic_cast<TextSegment*>(renderItem)) {
448 30 : ts->movey(diff);
449 : }
450 : }
451 : }
452 3630 : if (i == item->chords().size()) {
453 : // Set baseline for bottom chord
454 3598 : ldata->baseline = -diff;
455 : }
456 :
457 3630 : double topCapHeight = DBL_MAX;
458 14320 : for (const HarmonyRenderItem* renderItem : harmonyCtx.renderItemList) {
459 10690 : if (const TextSegment* ts = dynamic_cast<const TextSegment*>(renderItem)) {
460 10626 : topCapHeight = std::min(topCapHeight, ts->y() - ts->capHeight());
461 : }
462 : }
463 :
464 3630 : harmonyCtx.renderItemList.clear();
465 3630 : if (item->chords().size() == 1 || i == 1) {
466 3598 : break;
467 : }
468 :
469 32 : assert(!muse::RealIsEqual(topCapHeight, DBL_MAX));
470 :
471 32 : harmonyCtx.setx(0);
472 32 : harmonyCtx.sety(topCapHeight);
473 :
474 32 : double lineY = harmonyCtx.y() - item->absoluteFromSpatium(ctx.conf().style().styleS(Sid::polychordDividerSpacing))
475 32 : - item->absoluteFromSpatium(ctx.conf().style().styleS(Sid::polychordDividerThickness)) / 2;
476 32 : LineF line = LineF(PointF(0.0, lineY), PointF(0.0, lineY));
477 32 : ldata->polychordDividerLines.mut_value().push_back(line);
478 :
479 32 : harmonyCtx.movey(item->absoluteFromSpatium(-ctx.conf().style().styleS(Sid::polychordDividerSpacing)) * 2.0);
480 32 : harmonyCtx.movey(item->absoluteFromSpatium(-ctx.conf().style().styleS(Sid::polychordDividerThickness)));
481 : }
482 :
483 : // Align polychords
484 :
485 3598 : if (item->align() == AlignH::LEFT) {
486 1833 : return;
487 : }
488 :
489 1765 : double longestLine = 0.0;
490 :
491 3562 : for (const auto& [width, _] : chordTextSegments) {
492 1797 : if (width > longestLine) {
493 1719 : longestLine = width;
494 : }
495 : }
496 :
497 3562 : for (auto& textSegs : chordTextSegments) {
498 1797 : double width = textSegs.first;
499 1797 : std::vector<HarmonyRenderItem*>& segs = textSegs.second;
500 :
501 1797 : double diff = longestLine - width;
502 :
503 1797 : if (muse::RealIsNull(diff)) {
504 1765 : continue;
505 : }
506 :
507 : // For centre align adjust by .5* difference, for right align adjust by full difference
508 32 : if (item->align() == AlignH::HCENTER) {
509 32 : diff *= 0.5;
510 : }
511 :
512 88 : for (HarmonyRenderItem* seg : segs) {
513 56 : seg->movex(diff);
514 : }
515 : }
516 5431 : }
517 :
518 56 : void HarmonyLayout::renderRomanNumeral(Harmony* item, Harmony::LayoutData* ldata)
519 : {
520 56 : HarmonyRenderCtx harmonyCtx;
521 56 : if (item->chords().empty()) {
522 0 : return;
523 : }
524 56 : HarmonyInfo* info = item->chords().front();
525 :
526 56 : render(item, ldata, info->textName(), harmonyCtx);
527 56 : ldata->renderItemList.mut_value().insert(ldata->renderItemList.mut_value().end(), harmonyCtx.renderItemList.begin(),
528 : harmonyCtx.renderItemList.end());
529 56 : ldata->baseline = 0.0;
530 56 : }
531 :
532 3630 : void HarmonyLayout::doRenderSingleHarmony(Harmony* item, Harmony::LayoutData* ldata, HarmonyRenderCtx& harmonyCtx, int rootTpc, int bassTpc,
533 : const LayoutContext& ctx)
534 : {
535 3630 : HarmonyInfo* info = harmonyCtx.info;
536 3630 : ChordList* chordList = info->chordList();
537 3630 : if (!chordList) {
538 0 : return;
539 : }
540 :
541 3630 : const MStyle& style = ctx.conf().style();
542 :
543 3630 : NoteCaseType rootCase = item->rootRenderCase(info);
544 3630 : NoteCaseType bassCase = item->bassRenderCase();
545 :
546 3630 : NoteSpellingType spelling = style.styleV(Sid::chordSymbolSpelling).value<NoteSpellingType>();
547 :
548 3630 : const ChordDescription* cd = info->getDescription();
549 3630 : const bool stackModifiers = style.styleB(Sid::verticallyStackModifiers) && !item->doNotStackModifiers();
550 :
551 3630 : if (item->harmonyType() == HarmonyType::STANDARD && tpcIsValid(rootTpc)) {
552 : // render root
553 3438 : render(item, ldata, chordList->renderListRoot, harmonyCtx, ctx, rootTpc, spelling, rootCase);
554 : // render extension
555 3438 : if (cd) {
556 3438 : render(item, ldata, stackModifiers ? cd->renderListStacked : cd->renderList, harmonyCtx, ctx, 0);
557 : }
558 192 : } else if (item->harmonyType() == HarmonyType::NASHVILLE && tpcIsValid(rootTpc)) {
559 : // render function
560 162 : render(item, ldata, chordList->renderListFunction, harmonyCtx, ctx, rootTpc, spelling, bassCase);
561 162 : double adjust = chordList->nominalAdjust();
562 162 : harmonyCtx.movey(adjust * item->magS() * item->spatium() * .2);
563 : // render extension
564 162 : if (cd) {
565 162 : render(item, ldata, stackModifiers ? cd->renderListStacked : cd->renderList, harmonyCtx, ctx, 0);
566 : }
567 : } else {
568 30 : render(item, ldata, info->textName(), harmonyCtx);
569 : }
570 :
571 : // render bass
572 3630 : if (tpcIsValid(bassTpc)) {
573 : const std::vector<RenderActionPtr>& bassNoteChordList
574 55 : = style.styleB(Sid::chordBassNoteStagger) ? chordList->renderListBassOffset : chordList->renderListBass;
575 :
576 55 : static const std::wregex PATTERN_69 = std::wregex(L"6[,/]?9");
577 55 : const bool is69 = info->textName().contains(PATTERN_69);
578 55 : const bool hasModifierStack = stackModifiers && (info->parsedChord() ? info->parsedChord()->modifierList().size() > 1 : false);
579 :
580 55 : if (hasModifierStack || is69) {
581 0 : render(item, ldata, { std::make_shared<RenderActionMove>(0.05, 0.0) }, harmonyCtx, ctx,
582 : bassTpc, spelling, bassCase, item->bassScale());
583 : }
584 55 : render(item, ldata, bassNoteChordList, harmonyCtx, ctx, bassTpc, spelling, bassCase, item->bassScale());
585 : }
586 0 : }
587 :
588 3630 : void HarmonyLayout::renderSingleHarmony(Harmony* item, Harmony::LayoutData* ldata, HarmonyRenderCtx& harmonyCtx,
589 : const LayoutContext& ctx)
590 : {
591 3630 : harmonyCtx.hAlign = true;
592 3630 : const MStyle& style = ctx.conf().style();
593 3630 : HarmonyInfo* info = harmonyCtx.info;
594 :
595 3630 : int rootTpc = info->rootTpc();
596 3630 : int bassTpc = info->bassTpc();
597 :
598 3630 : DisplayCapoChordType displayCapo = style.styleV(Sid::displayCapoChords).value<DisplayCapoChordType>();
599 :
600 : // render single if no capo or both
601 :
602 3630 : if (displayCapo == DisplayCapoChordType::BOTH || displayCapo == DisplayCapoChordType::CONCERT) {
603 3630 : doRenderSingleHarmony(item, ldata, harmonyCtx, rootTpc, bassTpc, ctx);
604 : }
605 :
606 3630 : int capo = style.styleI(Sid::capoPosition);
607 3630 : if (capo == 0) {
608 3630 : return;
609 : }
610 :
611 0 : int capoRootTpc = Tpc::TPC_INVALID;
612 0 : int capoBassTpc = Tpc::TPC_INVALID;
613 0 : if (tpcIsValid(rootTpc) && capo > 0 && capo < 12) {
614 0 : int tpcOffset[] = { 0, 5, -2, 3, -4, 1, 6, -1, 4, -3, 2, -5 };
615 0 : capoRootTpc = rootTpc + tpcOffset[capo];
616 0 : capoBassTpc = bassTpc;
617 :
618 0 : if (tpcIsValid(capoBassTpc)) {
619 0 : capoBassTpc += tpcOffset[capo];
620 : }
621 :
622 : /*
623 : * For guitarists, avoid x and bb in Root or Bass,
624 : * and also avoid E#, B#, Cb and Fb in Root.
625 : */
626 0 : if (capoRootTpc < 8 || (tpcIsValid(capoBassTpc) && capoBassTpc < 6)) {
627 0 : capoRootTpc += 12;
628 0 : if (tpcIsValid(capoBassTpc)) {
629 0 : capoBassTpc += 12;
630 : }
631 0 : } else if (capoRootTpc > 24 || (tpcIsValid(capoBassTpc) && capoBassTpc > 26)) {
632 0 : capoRootTpc -= 12;
633 0 : if (tpcIsValid(capoBassTpc)) {
634 0 : capoBassTpc -= 12;
635 : }
636 : }
637 : }
638 :
639 : // Render parens if both
640 0 : if (displayCapo == DisplayCapoChordType::BOTH) {
641 0 : RenderActionParenPtr p = std::make_shared<RenderActionParenLeft>();
642 0 : renderActionParen(item, p, harmonyCtx);
643 0 : }
644 0 : if (displayCapo == DisplayCapoChordType::BOTH || displayCapo == DisplayCapoChordType::TRANSPOSED) {
645 : // render capo if both or capo
646 0 : doRenderSingleHarmony(item, ldata, harmonyCtx, capoRootTpc, capoBassTpc, ctx);
647 : }
648 0 : if (displayCapo == DisplayCapoChordType::BOTH) {
649 0 : RenderActionParenPtr p = std::make_shared<RenderActionParenRight>();
650 0 : renderActionParen(item, p, harmonyCtx);
651 0 : }
652 : }
653 :
654 : //---------------------------------------------------------
655 : // render
656 : //---------------------------------------------------------
657 :
658 86 : void HarmonyLayout::render(Harmony* item, Harmony::LayoutData* ldata, const String& s, HarmonyRenderCtx& harmonyCtx)
659 : {
660 86 : if (s.isEmpty()) {
661 0 : return;
662 : }
663 :
664 86 : Font f = item->harmonyType() != HarmonyType::ROMAN ? ldata->fontList.value().front() : item->font();
665 86 : TextSegment* ts = new TextSegment(s, f, harmonyCtx.x(), harmonyCtx.y(), harmonyCtx.hAlign);
666 86 : harmonyCtx.renderItemList.push_back(ts);
667 86 : harmonyCtx.movex(ts->width());
668 86 : }
669 :
670 0 : void HarmonyLayout::render(Harmony* item, Harmony::LayoutData* ldata, SymId sym, HarmonyRenderCtx& harmonyCtx, const LayoutContext& ctx)
671 : {
672 0 : if (sym == SymId::noSym) {
673 0 : return;
674 : }
675 :
676 0 : Font f = item->harmonyType() != HarmonyType::ROMAN ? ldata->fontList.value().front() : item->font();
677 0 : f.setFamily(ctx.conf().style().styleSt(Sid::musicalTextFont), Font::Type::MusicSymbolText);
678 :
679 0 : String s = ctx.conf().engravingFont()->toString(sym);
680 :
681 0 : TextSegment* ts = new TextSegment(s, f, harmonyCtx.x(), harmonyCtx.y(), harmonyCtx.hAlign);
682 0 : harmonyCtx.renderItemList.push_back(ts);
683 0 : harmonyCtx.movex(ts->width());
684 0 : }
685 :
686 7255 : void HarmonyLayout::render(Harmony* item, Harmony::LayoutData* ldata, const std::vector<RenderActionPtr>& renderList,
687 : HarmonyRenderCtx& harmonyCtx, const LayoutContext& ctx,
688 : int tpc,
689 : NoteSpellingType noteSpelling,
690 : NoteCaseType noteCase, double noteMag)
691 : {
692 7255 : harmonyCtx.stack = {};
693 7255 : harmonyCtx.tpc = tpc;
694 7255 : harmonyCtx.noteSpelling = noteSpelling;
695 7255 : harmonyCtx.noteCase = noteCase;
696 7255 : harmonyCtx.scale = noteMag;
697 :
698 23099 : for (const RenderActionPtr& a : renderList) {
699 15844 : renderAction(item, ldata, a, harmonyCtx, ctx);
700 : }
701 7255 : }
702 :
703 16258 : void HarmonyLayout::renderAction(Harmony* item, Harmony::LayoutData* ldata, const RenderActionPtr& a, HarmonyRenderCtx& harmonyCtx,
704 : const LayoutContext& ctx)
705 : {
706 16258 : switch (a->actionType()) {
707 7099 : case RenderAction::RenderActionType::SET:
708 7099 : renderActionSet(item, ldata, std::static_pointer_cast<RenderActionSet>(a), harmonyCtx, ctx);
709 7099 : break;
710 3233 : case RenderAction::RenderActionType::MOVE:
711 3233 : renderActionMove(item, std::static_pointer_cast<RenderActionMove>(a), harmonyCtx);
712 3233 : break;
713 12 : case RenderAction::RenderActionType::MOVEXHEIGHT:
714 12 : renderActionMoveXHeight(item, std::static_pointer_cast<RenderActionMoveXHeight>(a), harmonyCtx);
715 12 : break;
716 16 : case RenderAction::RenderActionType::PUSH:
717 16 : renderActionPush(harmonyCtx);
718 16 : break;
719 16 : case RenderAction::RenderActionType::POP:
720 16 : renderActionPop(std::static_pointer_cast<RenderActionPop>(a), harmonyCtx);
721 16 : break;
722 2897 : case RenderAction::RenderActionType::NOTE:
723 2897 : renderActionNote(item, ldata, harmonyCtx);
724 2897 : break;
725 2897 : case RenderAction::RenderActionType::ACCIDENTAL:
726 2897 : renderActionAcc(item, ldata, harmonyCtx, ctx);
727 2897 : break;
728 0 : case RenderAction::RenderActionType::STOPHALIGN:
729 0 : renderActionAlign(harmonyCtx);
730 0 : break;
731 24 : case RenderAction::RenderActionType::SCALE:
732 24 : renderActionScale(std::static_pointer_cast<RenderActionScale>(a), harmonyCtx);
733 24 : break;
734 64 : case RenderAction::RenderActionType::PAREN:
735 64 : renderActionParen(item, std::static_pointer_cast<RenderActionParen>(a), harmonyCtx);
736 64 : break;
737 0 : default:
738 0 : LOGD("unknown render action %d", static_cast<int>(a->actionType()));
739 : }
740 16258 : }
741 :
742 16 : void HarmonyLayout::renderActionPush(HarmonyRenderCtx& harmonyCtx)
743 : {
744 16 : harmonyCtx.stack.push(harmonyCtx.pos);
745 16 : }
746 :
747 16 : void HarmonyLayout::renderActionPop(const RenderActionPopPtr& a, HarmonyRenderCtx& harmonyCtx)
748 : {
749 16 : if (harmonyCtx.stack.empty()) {
750 0 : LOGD("RenderAction::RenderActionType::POP: stack empty");
751 0 : return;
752 : }
753 :
754 16 : PointF pt = harmonyCtx.stack.top();
755 16 : harmonyCtx.stack.pop();
756 16 : harmonyCtx.pos = PointF(a->popX() ? pt.x() : harmonyCtx.x(), a->popY() ? pt.y() : harmonyCtx.y());
757 : }
758 :
759 2897 : void HarmonyLayout::renderActionNote(Harmony* item, Harmony::LayoutData* ldata, HarmonyRenderCtx& harmonyCtx)
760 : {
761 2897 : if (!tpcIsValid(harmonyCtx.tpc)) {
762 0 : return;
763 : }
764 2897 : const ChordList* chordList = item->score()->chordList();
765 2897 : const Staff* st = item->staff();
766 2897 : const Key key = st ? st->key(item->tick()) : Key::INVALID;
767 :
768 2897 : String c;
769 : AccidentalVal acc;
770 :
771 2897 : if (item->harmonyType() == HarmonyType::STANDARD) {
772 2803 : tpc2name(harmonyCtx.tpc, harmonyCtx.noteSpelling, harmonyCtx.noteCase, c, acc);
773 94 : } else if (item->harmonyType() == HarmonyType::NASHVILLE) {
774 94 : String accStr;
775 94 : tpc2Function(harmonyCtx.tpc, key, accStr, c);
776 94 : }
777 :
778 2897 : if (c.empty()) {
779 0 : return;
780 : }
781 :
782 2897 : String lookup = u"note" + c;
783 2897 : ChordSymbol cs = chordList->symbol(lookup);
784 2897 : if (!cs.isValid()) {
785 2897 : cs = chordList->symbol(c);
786 : }
787 2897 : String text = cs.isValid() ? cs.value : c;
788 2897 : muse::draw::Font font = cs.isValid() ? ldata->fontList.value()[cs.fontIdx] : ldata->fontList.value().front();
789 2897 : font.setPointSizeF(font.pointSizeF() * harmonyCtx.scale);
790 :
791 2897 : TextSegment* ts = new TextSegment(text, font, harmonyCtx.x(), harmonyCtx.y(), harmonyCtx.hAlign);
792 2897 : harmonyCtx.renderItemList.push_back(ts);
793 2897 : harmonyCtx.movex(ts->width());
794 2897 : }
795 :
796 2897 : void HarmonyLayout::renderActionAcc(Harmony* item, Harmony::LayoutData* ldata, HarmonyRenderCtx& harmonyCtx, const LayoutContext& ctx)
797 : {
798 2897 : if (!tpcIsValid(harmonyCtx.tpc)) {
799 2297 : return;
800 : }
801 2897 : const ChordList* chordList = item->score()->chordList();
802 2897 : const Staff* st = item->staff();
803 2897 : const Key key = st ? st->key(item->tick()) : Key::INVALID;
804 :
805 2897 : String c;
806 2897 : String acc;
807 2897 : String context = u"accidental";
808 :
809 2897 : if (item->harmonyType() == HarmonyType::STANDARD) {
810 2803 : tpc2name(harmonyCtx.tpc, harmonyCtx.noteSpelling, harmonyCtx.noteCase, c, acc);
811 94 : } else if (item->harmonyType() == HarmonyType::NASHVILLE) {
812 94 : tpc2Function(harmonyCtx.tpc, key, acc, c);
813 : }
814 :
815 2897 : if (acc.empty()) {
816 2159 : return;
817 : }
818 :
819 : // Try to find token & execute renderlist
820 738 : ChordToken tok = chordList->token(acc, ChordTokenClass::ACCIDENTAL);
821 738 : if (tok.isValid()) {
822 552 : for (const RenderActionPtr& a : tok.renderList) {
823 414 : renderAction(item, ldata, a, harmonyCtx, ctx);
824 : }
825 138 : return;
826 : }
827 :
828 : // No valid token, find symbol
829 :
830 : // German spelling - use special symbol for accidental in TPC_B_B
831 : // to allow it to be rendered as either Bb or B
832 600 : if (harmonyCtx.tpc == Tpc::TPC_B_B && harmonyCtx.noteSpelling == NoteSpellingType::GERMAN) {
833 0 : context = u"german_B";
834 : }
835 600 : String lookup = context + acc;
836 600 : ChordSymbol cs = chordList->symbol(lookup);
837 600 : if (!cs.isValid()) {
838 600 : cs = chordList->symbol(acc);
839 : }
840 600 : String text = cs.isValid() ? cs.value : c;
841 600 : muse::draw::Font font = cs.isValid() ? ldata->fontList.value()[cs.fontIdx] : ldata->fontList.value().front();
842 600 : font.setPointSizeF(font.pointSizeF() * harmonyCtx.scale);
843 :
844 600 : TextSegment* ts = new TextSegment(text, font, harmonyCtx.x(), harmonyCtx.y(), harmonyCtx.hAlign);
845 600 : harmonyCtx.renderItemList.push_back(ts);
846 600 : harmonyCtx.movex(ts->width());
847 7629 : }
848 :
849 0 : void HarmonyLayout::renderActionAlign(HarmonyRenderCtx& harmonyCtx)
850 : {
851 0 : harmonyCtx.hAlign = false;
852 0 : }
853 :
854 24 : void HarmonyLayout::renderActionScale(const RenderActionScalePtr& a, HarmonyRenderCtx& harmonyCtx)
855 : {
856 24 : harmonyCtx.scale *= a->scale();
857 24 : }
858 :
859 64 : void HarmonyLayout::renderActionParen(Harmony* item, const RenderActionParenPtr& a, HarmonyRenderCtx& harmonyCtx)
860 : {
861 64 : Parenthesis* p = Factory::createParenthesis(item);
862 64 : p->setDirection(a->direction());
863 64 : p->setColor(item->color());
864 64 : p->setFollowParentColor(true);
865 64 : p->setGenerated(true);
866 :
867 64 : ChordSymbolParen* parenItem = new ChordSymbolParen(p, harmonyCtx.hAlign, harmonyCtx.x(), harmonyCtx.y());
868 64 : harmonyCtx.renderItemList.push_back(parenItem);
869 64 : }
870 :
871 7099 : void HarmonyLayout::kernCharacters(const Harmony* item, const String& text, HarmonyRenderCtx& harmonyCtx, const LayoutContext& ctx)
872 : {
873 7099 : if (harmonyCtx.renderItemList.empty()) {
874 663 : return;
875 : }
876 : // Character pair and distance to move the second
877 : // Blank strings will apply the kerning no matter the following character
878 : // TODO - move this information to the XML file
879 : static const std::map<std::pair<String, String>, double> KERNED_CHARACTERS {
880 0 : { { u"A", u"\uE870" }, -0.4 }, // dim
881 0 : { { u"A", u"\uE871" }, -0.3 }, // half-dim
882 0 : { { u"\uE873", u"\uE870" }, -0.4 }, // triangle - dim
883 0 : { { u"\uE873", u"\uE871" }, -0.3 }, // triangle - half-dim
884 0 : { { u"A", u"/" }, 0.1 },
885 :
886 0 : { { u"A", u"\uE18E" }, -0.15 }, // dim JAZZ
887 0 : { { u"A", u"\uE18F" }, -0.15 }, // half-dim JAZZ
888 0 : { { u"\uE18A", u"\uE18E" }, -0.15 }, // triangle - dim JAZZ
889 0 : { { u"\uE18A", u"\uE18F" }, -0.15 }, // triangle - half-dim JAZZ
890 :
891 0 : { { u"\u266D", u"" }, -0.15 }, // b JAZZ
892 0 : { { u"\u266E", u"" }, -0.15 }, // natural JAZZ
893 0 : { { u"\u266F", u"" }, -0.15 }, // # JAZZ
894 0 : { { u"\u1D12A", u"" }, -0.15 }, // ## JAZZ
895 0 : { { u"\u1D12B", u"" }, -0.15 }, // bb JAZZ
896 6516 : };
897 :
898 6436 : HarmonyRenderItem* prevSeg = harmonyCtx.renderItemList.back();
899 6436 : TextSegment* ts = dynamic_cast<TextSegment*>(prevSeg);
900 6436 : if (!ts) {
901 32 : return;
902 : }
903 :
904 96060 : for (auto& kernInfo : KERNED_CHARACTERS) {
905 89656 : const std::pair<String, String> kernPair = kernInfo.first;
906 89656 : bool endChar = ts->text().endsWith(kernPair.first);
907 89656 : bool startChar = text.startsWith(kernPair.second);
908 : // VERY DIRTY HACK ALERT
909 : // "Match any" should only be applied to the Jazz preset currently.
910 : // Remove the jazz condition when these kern values are moved to the XML files
911 89656 : if (kernPair.second.isEmpty() && ctx.conf().styleV(Sid::chordStyle).value<ChordStylePreset>() != ChordStylePreset::JAZZ) {
912 32020 : continue;
913 : }
914 57636 : bool startMatchAny = kernPair.second.isEmpty();
915 57636 : if ((endChar && startChar) || (endChar && startMatchAny)) {
916 0 : const FontMetrics fm = FontMetrics(item->font());
917 0 : const double scale = harmonyCtx.scale * item->mag();
918 0 : harmonyCtx.pos = harmonyCtx.pos + PointF(kernInfo.second, 0.0) * FontMetrics::capHeight(item->font()) * scale;
919 0 : break;
920 0 : }
921 89656 : }
922 5 : }
923 :
924 7099 : void HarmonyLayout::renderActionSet(Harmony* item, Harmony::LayoutData* ldata, const RenderActionSetPtr& a, HarmonyRenderCtx& harmonyCtx,
925 : const LayoutContext& ctx)
926 : {
927 7099 : const ChordList* chordList = item->score()->chordList();
928 7099 : const ChordSymbol cs = chordList->symbol(a->text());
929 7099 : const String text = cs.isValid() ? cs.value : a->text();
930 7099 : muse::draw::Font font = cs.isValid() ? ldata->fontList.value()[cs.fontIdx] : ldata->fontList.value().front();
931 7099 : font.setPointSizeF(font.pointSizeF() * harmonyCtx.scale);
932 7099 : if (item->harmonyType() == HarmonyType::NASHVILLE) {
933 142 : double nmag = chordList->nominalMag();
934 142 : font.setPointSizeF(font.pointSizeF() * nmag);
935 : }
936 :
937 7099 : kernCharacters(item, text, harmonyCtx, ctx);
938 :
939 7099 : TextSegment* ts = new TextSegment(text, font, harmonyCtx.x(), harmonyCtx.y(), harmonyCtx.hAlign);
940 7099 : harmonyCtx.movex(ts->width());
941 :
942 7099 : if (a->renderText()) {
943 7099 : harmonyCtx.renderItemList.push_back(ts);
944 7099 : return;
945 : }
946 :
947 0 : delete ts;
948 21297 : }
949 :
950 3233 : void HarmonyLayout::renderActionMove(Harmony* item, const RenderActionMovePtr& a, HarmonyRenderCtx& harmonyCtx)
951 : {
952 3233 : const double scale = (a->scaled() ? harmonyCtx.scale : 1.0) * item->mag();
953 3233 : harmonyCtx.pos = harmonyCtx.pos + a->vec() * FontMetrics::capHeight(item->font()) * scale;
954 3233 : }
955 :
956 12 : void HarmonyLayout::renderActionMoveXHeight(Harmony* item, const RenderActionMoveXHeightPtr& a, HarmonyRenderCtx& harmonyCtx)
957 : {
958 12 : const int direction = a->up() ? -1 : 1;
959 12 : const double scale = a->scaled() ? harmonyCtx.scale : 1.0;
960 12 : const FontMetrics fm = FontMetrics(item->font());
961 12 : harmonyCtx.movey(direction * fm.xHeight() * scale);
962 12 : }
|