From 7b7c85219ae5a8798ecd18a2e63dccfb6e2be3db Mon Sep 17 00:00:00 2001 From: kulek Date: Sun, 21 Jun 2026 09:30:28 +0200 Subject: [PATCH 1/4] feat: add inline tuning --- .../model/RenderStylesheetSerializer.ts | 5 + .../alphaTex/AlphaTex1EnumMappings.ts | 10 +- .../alphaTex/AlphaTex1LanguageDefinitions.ts | 2 + .../alphaTex/AlphaTex1LanguageHandler.ts | 22 +++- .../alphatab/src/model/RenderStylesheet.ts | 21 ++++ packages/alphatab/src/model/_barrel.ts | 1 + .../src/rendering/layout/ScoreLayout.ts | 7 +- .../src/rendering/staves/StaffSystem.ts | 106 +++++++++++++++++- .../test/importer/AlphaTexImporter.test.ts | 5 +- packages/alphatex/src/definitions.ts | 2 + packages/alphatex/src/enum.ts | 5 + .../src/metadata/score/tuningdisplaymode.ts | 27 +++++ 12 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 packages/alphatex/src/metadata/score/tuningdisplaymode.ts diff --git a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts index 10ac9a9ac..1f3a17c33 100644 --- a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts +++ b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts @@ -6,6 +6,7 @@ import { RenderStylesheet } from "@coderline/alphatab/model/RenderStylesheet"; import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; import { BracketExtendMode } from "@coderline/alphatab/model/RenderStylesheet"; +import { TuningDisplayMode } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNamePolicy } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNameMode } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNameOrientation } from "@coderline/alphatab/model/RenderStylesheet"; @@ -29,6 +30,7 @@ export class RenderStylesheetSerializer { o.set("bracketextendmode", obj.bracketExtendMode as number); o.set("usesystemsignseparator", obj.useSystemSignSeparator); o.set("globaldisplaytuning", obj.globalDisplayTuning); + o.set("tuningdisplaymode", obj.tuningDisplayMode as number); if (obj.perTrackDisplayTuning !== null) { const m = new Map(); o.set("pertrackdisplaytuning", m); @@ -80,6 +82,9 @@ export class RenderStylesheetSerializer { case "globaldisplaytuning": obj.globalDisplayTuning = v! as boolean; return true; + case "tuningdisplaymode": + obj.tuningDisplayMode = JsonHelper.parseEnum(v, TuningDisplayMode)!; + return true; case "pertrackdisplaytuning": obj.perTrackDisplayTuning = new Map(); JsonHelper.forEach(v, (v, k) => { diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts index 598cad9ef..a3fe7fab7 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts @@ -18,7 +18,8 @@ import type { BracketExtendMode, TrackNameMode, TrackNameOrientation, - TrackNamePolicy + TrackNamePolicy, + TuningDisplayMode } from '@coderline/alphatab/model/RenderStylesheet'; import type { SimileMark } from '@coderline/alphatab/model/SimileMark'; import type { TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; @@ -198,6 +199,13 @@ export class AlphaTex1EnumMappings { ['shortname', 1] ]); public static readonly trackNameModeReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.trackNameMode); + public static readonly tuningDisplayMode = new Map([ + ['score', 0], + ['staff', 1] + ]); + public static readonly tuningDisplayModeReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.tuningDisplayMode + ); public static readonly textAlign = new Map([ ['left', 0], ['center', 1], diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts index 8e84fe7ed..520fbf79f 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -181,6 +181,7 @@ export class AlphaTex1LanguageDefinitions { ['showdynamics', null], ['hidedynamics', null], ['usesystemsignseparator', null], + ['tuningdisplaymode', [[[[10, 17], 0, ['score', 'staff']]]]], ['multibarrest', null], ['bracketextendmode', [[[[10, 17], 0, ['nobrackets', 'groupstaves', 'groupsimilarinstruments']]]]], ['singletracktracknamepolicy', [[[[10, 17], 0, ['hidden', 'firstsystem', 'allsystems']]]]], @@ -534,6 +535,7 @@ export class AlphaTex1LanguageDefinitions { ['showdynamics', null], ['hidedynamics', null], ['usesystemsignseparator', null], + ['tuningdisplaymode', null], ['multibarrest', null], ['bracketextendmode', null], ['singletracktracknamepolicy', null], diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index 4b38647d0..1608567fa 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -71,7 +71,7 @@ import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import { BarNumberDisplay, type RenderStylesheet } from '@coderline/alphatab/model/RenderStylesheet'; +import { BarNumberDisplay, type RenderStylesheet, TuningDisplayMode } from '@coderline/alphatab/model/RenderStylesheet'; import { HeaderFooterStyle, Score, ScoreStyle, ScoreSubElement } from '@coderline/alphatab/model/Score'; import { Section } from '@coderline/alphatab/model/Section'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; @@ -188,6 +188,18 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler case 'usesystemsignseparator': score.stylesheet.useSystemSignSeparator = true; return ApplyNodeResult.Applied; + case 'tuningdisplaymode': + const tuningDisplayMode = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.arguments!, + 'tuning display mode', + AlphaTex1EnumMappings.tuningDisplayMode + ); + if (tuningDisplayMode === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.tuningDisplayMode = tuningDisplayMode!; + return ApplyNodeResult.Applied; case 'multibarrest': score.stylesheet.multiTrackMultiBarRest = true; return ApplyNodeResult.Applied; @@ -2562,6 +2574,14 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler if (stylesheet.useSystemSignSeparator) { nodes.push(Atnf.meta('useSystemSignSeparator')); } + if (stylesheet.tuningDisplayMode !== TuningDisplayMode.Score) { + nodes.push( + Atnf.identMeta( + 'tuningDisplayMode', + AlphaTex1EnumMappings.tuningDisplayModeReversed.get(stylesheet.tuningDisplayMode)! + ) + ); + } if (stylesheet.multiTrackMultiBarRest) { nodes.push(Atnf.meta('multiBarRest')); } diff --git a/packages/alphatab/src/model/RenderStylesheet.ts b/packages/alphatab/src/model/RenderStylesheet.ts index 81e4a4ad5..a0ad180f2 100644 --- a/packages/alphatab/src/model/RenderStylesheet.ts +++ b/packages/alphatab/src/model/RenderStylesheet.ts @@ -72,6 +72,22 @@ export enum TrackNameOrientation { Vertical = 1 } +/** + * Lists the different places where string tuning information is displayed. + * @public + */ +export enum TuningDisplayMode { + /** + * Tuning information is displayed above the score. + */ + Score = 0, + + /** + * Tuning note names are displayed beside the corresponding tab staff lines. + */ + Staff = 1 +} + /** * How bar numbers are displayed * @public @@ -119,6 +135,11 @@ export class RenderStylesheet { */ public globalDisplayTuning: boolean = true; + /** + * The place where tuning information is displayed. + */ + public tuningDisplayMode: TuningDisplayMode = TuningDisplayMode.Score; + /** * Whether to show the tuning.(per-track) */ diff --git a/packages/alphatab/src/model/_barrel.ts b/packages/alphatab/src/model/_barrel.ts index c5c0e1a6a..94dea8678 100644 --- a/packages/alphatab/src/model/_barrel.ts +++ b/packages/alphatab/src/model/_barrel.ts @@ -51,6 +51,7 @@ export { TrackNamePolicy, TrackNameMode, TrackNameOrientation, + TuningDisplayMode, BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; export { RepeatGroup } from '@coderline/alphatab/model/RepeatGroup'; diff --git a/packages/alphatab/src/rendering/layout/ScoreLayout.ts b/packages/alphatab/src/rendering/layout/ScoreLayout.ts index cc6585301..b821d1435 100644 --- a/packages/alphatab/src/rendering/layout/ScoreLayout.ts +++ b/packages/alphatab/src/rendering/layout/ScoreLayout.ts @@ -4,6 +4,7 @@ import { Logger } from '@coderline/alphatab/Logger'; import type { Bar } from '@coderline/alphatab/model/Bar'; import { Font, FontStyle, FontWeight } from '@coderline/alphatab/model/Font'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { TuningDisplayMode } from '@coderline/alphatab/model/RenderStylesheet'; import { type Score, ScoreStyle, ScoreSubElement } from '@coderline/alphatab/model/Score'; import type { Staff } from '@coderline/alphatab/model/Staff'; import { type Track, TrackSubElement } from '@coderline/alphatab/model/Track'; @@ -305,7 +306,11 @@ export abstract class ScoreLayout { } } // tuning info - if (stavesWithTuning.length > 0 && score.stylesheet.globalDisplayTuning) { + if ( + stavesWithTuning.length > 0 && + score.stylesheet.globalDisplayTuning && + score.stylesheet.tuningDisplayMode === TuningDisplayMode.Score + ) { this.tuningGlyph = new TuningContainerGlyph(0, 0); this.tuningGlyph.renderer = fakeBarRenderer; for (const staff of stavesWithTuning) { diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index 1fa922a1f..eff3a1c64 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -6,15 +6,18 @@ import { BracketExtendMode, TrackNameMode, TrackNameOrientation, - TrackNamePolicy + TrackNamePolicy, + TuningDisplayMode } from '@coderline/alphatab/model/RenderStylesheet'; import { type Track, TrackSubElement } from '@coderline/alphatab/model/Track'; +import { Tuning } from '@coderline/alphatab/model/Tuning'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import type { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; +import { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; import { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; import { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; @@ -657,6 +660,8 @@ export class StaffSystem { } } + this.accoladeWidth += this._getInlineTuningWidth(this.allStaves, this.layout.renderer.canvas!); + // NOTE: we have a chicken-egg problem when it comes to scaling braces which we try to mitigate here: // - The brace scales with the height of the system // - The height of the system depends on the bars which can be fitted @@ -696,6 +701,63 @@ export class StaffSystem { } } + private _shouldRenderInlineTuning(staff: RenderStaff): boolean { + const score = this.layout.renderer.score!; + if ( + this.index !== 0 || + !staff.isVisible || + staff.staffId !== TabBarRenderer.StaffId || + !this.layout.renderer.settings.notation.isNotationElementVisible(NotationElement.GuitarTuning) || + !score.stylesheet.globalDisplayTuning || + score.stylesheet.tuningDisplayMode !== TuningDisplayMode.Staff + ) { + return false; + } + + const modelStaff = staff.modelStaff; + if ( + modelStaff.isPercussion || + !modelStaff.isStringed || + !modelStaff.showTablature || + modelStaff.stringTuning.tunings.length === 0 + ) { + return false; + } + + const perTrackDisplayTuning = score.stylesheet.perTrackDisplayTuning; + return ( + !perTrackDisplayTuning || + !perTrackDisplayTuning.has(modelStaff.track.index) || + perTrackDisplayTuning.get(modelStaff.track.index) !== false + ); + } + + private _getInlineTuningWidth(staves: Iterable, canvas: ICanvas): number { + const oldFont = canvas.font; + canvas.font = this.layout.renderer.settings.display.resources.elementFonts.get(NotationElement.GuitarTuning)!; + + let maxWidth = 0; + for (const staff of staves) { + if (!this._shouldRenderInlineTuning(staff)) { + continue; + } + + for (const tuning of staff.modelStaff.stringTuning.tunings) { + maxWidth = Math.max(maxWidth, canvas.measureText(Tuning.getTextForTuning(tuning, false)).width); + } + } + canvas.font = oldFont; + + return maxWidth > 0 ? maxWidth + this.layout.renderer.settings.display.inlineTuningPaddingRight : 0; + } + + private _getInlineTuningWidthForBracket(bracket: SystemBracket, canvas: ICanvas): number { + return this._getInlineTuningWidth( + this.allStaves.filter(staff => bracket.includesStaff(staff)), + canvas + ); + } + private _getStaffTrackGroup(track: Track): StaffTrackGroup | null { for (let i: number = 0, j: number = this.staves.length; i < j; i++) { const g: StaffTrackGroup = this.staves[i]; @@ -874,6 +936,7 @@ export class StaffSystem { g.staves[0].x - // left side of the bracket settings.display.accoladeBarPaddingRight - + this._getInlineTuningWidth(g.staves, canvas) - (g.bracket?.width ?? 0) - // padding between label and bracket settings.display.systemLabelPaddingRight; @@ -909,6 +972,8 @@ export class StaffSystem { } } + this._paintInlineTunings(cx, cy, canvas); + const needsSystemBarLine = !this.layout.renderer.score!.stylesheet.extendBarLines; if (this.allStaves.length > 0 && needsSystemBarLine) { let previousStaffInBracket: RenderStaff | null = null; @@ -944,6 +1009,42 @@ export class StaffSystem { } } + private _paintInlineTunings(cx: number, cy: number, canvas: ICanvas): void { + const oldFont = canvas.font; + const oldBaseLine = canvas.textBaseline; + const oldTextAlign = canvas.textAlign; + + canvas.font = this.layout.renderer.settings.display.resources.elementFonts.get(NotationElement.GuitarTuning)!; + canvas.textBaseline = TextBaseline.Middle; + canvas.textAlign = TextAlign.Right; + + for (const staff of this.allStaves) { + if (!this._shouldRenderInlineTuning(staff)) { + continue; + } + + const renderer = staff.barRenderers[0]; + if (!(renderer instanceof TabBarRenderer)) { + continue; + } + const textEndX = cx + staff.x - this.layout.renderer.settings.display.inlineTuningPaddingRight; + + using _ = ElementStyleHelper.track(canvas, TrackSubElement.StringTuning, staff.modelStaff.track, true); + + for (let i = 0, j = staff.modelStaff.stringTuning.tunings.length; i < j; i++) { + canvas.fillText( + Tuning.getTextForTuning(staff.modelStaff.stringTuning.tunings[i], false), + textEndX, + cy + staff.y + renderer.y + renderer.getLineY(i) + ); + } + } + + canvas.font = oldFont; + canvas.textBaseline = oldBaseLine; + canvas.textAlign = oldTextAlign; + } + private _paintBrackets(cx: number, cy: number, canvas: ICanvas) { const settings = this.layout.renderer.settings; @@ -951,7 +1052,8 @@ export class StaffSystem { if (bracket.canPaint) { const barStartX: number = cx + bracket.firstVisibleStaffInBracket!.x; const barSize: number = bracket.width; - const barOffset: number = settings.display.accoladeBarPaddingRight; + const barOffset: number = + settings.display.accoladeBarPaddingRight + this._getInlineTuningWidthForBracket(bracket, canvas); const firstStart: number = cy + bracket.firstVisibleStaffInBracket!.contentTop; const lastEnd: number = cy + bracket.lastVisibleStaffInBracket!.contentBottom; let accoladeStart: number = firstStart; diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index 39a76e468..ace28cc66 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -32,7 +32,8 @@ import { BracketExtendMode, TrackNameMode, TrackNameOrientation, - TrackNamePolicy + TrackNamePolicy, + TuningDisplayMode } from '@coderline/alphatab/model/RenderStylesheet'; import { type Score, ScoreSubElement } from '@coderline/alphatab/model/Score'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; @@ -1856,6 +1857,7 @@ describe('AlphaTexImporterTest', () => { \\hideDynamics \\bracketExtendMode nobrackets \\useSystemSignSeparator + \\tuningDisplayMode staff \\singleTrackTrackNamePolicy allsystems \\multiTrackTrackNamePolicy Hidden \\firstSystemTrackNameMode fullname @@ -1873,6 +1875,7 @@ describe('AlphaTexImporterTest', () => { expect(score.stylesheet.hideDynamics).toBe(true); expect(score.stylesheet.bracketExtendMode).toBe(BracketExtendMode.NoBrackets); expect(score.stylesheet.useSystemSignSeparator).toBe(true); + expect(score.stylesheet.tuningDisplayMode).toBe(TuningDisplayMode.Staff); expect(score.stylesheet.singleTrackTrackNamePolicy).toBe(TrackNamePolicy.AllSystems); expect(score.stylesheet.multiTrackTrackNamePolicy).toBe(TrackNamePolicy.Hidden); expect(score.stylesheet.firstSystemTrackNameMode).toBe(TrackNameMode.FullName); diff --git a/packages/alphatex/src/definitions.ts b/packages/alphatex/src/definitions.ts index 1e721ca30..14b425f06 100644 --- a/packages/alphatex/src/definitions.ts +++ b/packages/alphatex/src/definitions.ts @@ -43,6 +43,7 @@ import { subtitle } from '@coderline/alphatab-alphatex//metadata/score/subtitle' import { systemsLayout } from '@coderline/alphatab-alphatex//metadata/score/systemslayout'; import { tab } from '@coderline/alphatab-alphatex//metadata/score/tab'; import { title } from '@coderline/alphatab-alphatex//metadata/score/title'; +import { tuningDisplayMode } from '@coderline/alphatab-alphatex//metadata/score/tuningdisplaymode'; import { useSystemSignSeparator } from '@coderline/alphatab-alphatex//metadata/score/usesystemsignseparator'; import { words } from '@coderline/alphatab-alphatex//metadata/score/words'; import { wordsAndMusic } from '@coderline/alphatab-alphatex//metadata/score/wordsandmusic'; @@ -178,6 +179,7 @@ export const scoreMetaData = metadata( showDynamics, hideDynamics, useSystemSignSeparator, + tuningDisplayMode, multiBarRest, bracketExtendMode, singleTrackTrackNamePolicy, diff --git a/packages/alphatex/src/enum.ts b/packages/alphatex/src/enum.ts index 014c0516e..65331f264 100644 --- a/packages/alphatex/src/enum.ts +++ b/packages/alphatex/src/enum.ts @@ -19,6 +19,7 @@ export const alphaTexMappedEnumLookup = { TrackNamePolicy: alphaTab.model.TrackNamePolicy, TrackNameOrientation: alphaTab.model.TrackNameOrientation, TrackNameMode: alphaTab.model.TrackNameMode, + TuningDisplayMode: alphaTab.model.TuningDisplayMode, TextAlign: alphaTab.platform.TextAlign, BendType: alphaTab.model.BendType, KeySignature: alphaTab.model.KeySignature, @@ -231,6 +232,10 @@ export const alphaTexMappedEnumMapping: { FullName: { snippet: 'fullName', shortDescription: 'Short track names (abbreviations) are displayed.' }, ShortName: { snippet: 'shortName', shortDescription: 'Full track names are displayed.' } }, + TuningDisplayMode: { + Score: { snippet: 'score', shortDescription: 'Display tuning information above the score.' }, + Staff: { snippet: 'staff', shortDescription: 'Display tuning note names beside tab staff lines.' } + }, TextAlign: { Left: { snippet: 'left', diff --git a/packages/alphatex/src/metadata/score/tuningdisplaymode.ts b/packages/alphatex/src/metadata/score/tuningdisplaymode.ts new file mode 100644 index 000000000..5fe8276f7 --- /dev/null +++ b/packages/alphatex/src/metadata/score/tuningdisplaymode.ts @@ -0,0 +1,27 @@ +import * as alphaTab from '@coderline/alphatab'; +import { enumParameter } from '@coderline/alphatab-alphatex/enum'; +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const tuningDisplayMode: MetadataTagDefinition = { + tag: '\\tuningDisplayMode', + snippet: '\\tuningDisplayMode ${1:score}$0', + shortDescription: 'Sets where string tuning information is displayed.', + signatures: [ + { + parameters: [ + { + name: 'mode', + shortDescription: 'The mode to use', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter('TuningDisplayMode') + } + ] + } + ], + examples: ` + \\tuningDisplayMode staff + \\track "Guitar" + \\staff { score tabs } + 0.6.4 2.6.4 3.6.4 0.5.4 + ` +}; From f0185d011665b9217de21e704a7137fbd7f44fa5 Mon Sep 17 00:00:00 2001 From: kulek Date: Sun, 21 Jun 2026 09:30:48 +0200 Subject: [PATCH 2/4] feat: allow to adjust tuning padding --- packages/alphatab/src/DisplaySettings.ts | 8 ++++++++ packages/alphatab/src/generated/DisplaySettingsJson.ts | 7 +++++++ .../alphatab/src/generated/DisplaySettingsSerializer.ts | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/packages/alphatab/src/DisplaySettings.ts b/packages/alphatab/src/DisplaySettings.ts index 4fce2772d..52198ecd2 100644 --- a/packages/alphatab/src/DisplaySettings.ts +++ b/packages/alphatab/src/DisplaySettings.ts @@ -248,6 +248,14 @@ export class DisplaySettings { */ public accoladeBarPaddingRight: number = 3; + /** + * The padding between inline tuning labels and the start of the tab staff. + * @since 1.9.0 + * @category Display + * @defaultValue `5` + */ + public inlineTuningPaddingRight: number = 5; + // Staff padding /** diff --git a/packages/alphatab/src/generated/DisplaySettingsJson.ts b/packages/alphatab/src/generated/DisplaySettingsJson.ts index 20cb54ddb..4a9a493b4 100644 --- a/packages/alphatab/src/generated/DisplaySettingsJson.ts +++ b/packages/alphatab/src/generated/DisplaySettingsJson.ts @@ -218,6 +218,13 @@ export interface DisplaySettingsJson { * @defaultValue `3` */ accoladeBarPaddingRight?: number; + /** + * The padding between inline tuning labels and the start of the tab staff. + * @since 1.9.0 + * @category Display + * @defaultValue `5` + */ + inlineTuningPaddingRight?: number; /** * The top padding applied to the first main notation staff (standard, tabs, numbered, slash). * @since 1.8.0 diff --git a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts index c07b064b7..d715e8f1e 100644 --- a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts +++ b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts @@ -42,6 +42,7 @@ export class DisplaySettingsSerializer { o.set("systemlabelpaddingleft", obj.systemLabelPaddingLeft); o.set("systemlabelpaddingright", obj.systemLabelPaddingRight); o.set("accoladebarpaddingright", obj.accoladeBarPaddingRight); + o.set("inlinetuningpaddingright", obj.inlineTuningPaddingRight); o.set("firstnotationstaffpaddingtop", obj.firstNotationStaffPaddingTop); o.set("lastnotationstaffpaddingbottom", obj.lastNotationStaffPaddingBottom); o.set("notationstaffpaddingtop", obj.notationStaffPaddingTop); @@ -109,6 +110,9 @@ export class DisplaySettingsSerializer { case "accoladebarpaddingright": obj.accoladeBarPaddingRight = v! as number; return true; + case "inlinetuningpaddingright": + obj.inlineTuningPaddingRight = v! as number; + return true; case "firstnotationstaffpaddingtop": obj.firstNotationStaffPaddingTop = v! as number; return true; From ae8bc5720c841d7100a20f30a4e96eca5836a5df Mon Sep 17 00:00:00 2001 From: kulek Date: Sun, 21 Jun 2026 09:32:18 +0200 Subject: [PATCH 3/4] test: cover inline tuning rendering --- .../layout/inline-tuning-first-system.png | Bin 0 -> 14366 bytes .../test/visualTests/features/Layout.test.ts | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 packages/alphatab/test-data/visual-tests/layout/inline-tuning-first-system.png diff --git a/packages/alphatab/test-data/visual-tests/layout/inline-tuning-first-system.png b/packages/alphatab/test-data/visual-tests/layout/inline-tuning-first-system.png new file mode 100644 index 0000000000000000000000000000000000000000..66862796f2de4641532c7bbacdb0c1e3152069d3 GIT binary patch literal 14366 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

tj6qeSKPFRiz-H!}gt|rKQKN$0${< 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 M6{E}f7i}K?4*`%Dw*UYD literal 0 HcmV?d00001 diff --git a/packages/alphatab/test/visualTests/features/Layout.test.ts b/packages/alphatab/test/visualTests/features/Layout.test.ts index c2a12bac7..04e7c44af 100644 --- a/packages/alphatab/test/visualTests/features/Layout.test.ts +++ b/packages/alphatab/test/visualTests/features/Layout.test.ts @@ -124,6 +124,24 @@ describe('LayoutTests', () => { }); }); + it('inline-tuning-first-system', async () => { + const settings: Settings = new Settings(); + settings.display.layoutMode = LayoutMode.Parchment; + await VisualTestHelper.runVisualTestTex( + ` + \\tuningDisplayMode staff + \\track { defaultSystemsLayout 2 } + \\staff { tabs } + 0.6.4 2.6.4 3.6.4 0.5.4 | + 2.5.4 3.5.4 0.4.4 2.4.4 | + 3.4.4 0.3.4 2.3.4 3.3.4 | + 0.2.4 1.2.4 3.2.4 0.1.4 | + `, + 'test-data/visual-tests/layout/inline-tuning-first-system.png', + settings + ); + }); + it('system-layout-tex', async () => { const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Parchment; From fe19a08d8c6d949f209b2e53f00bc89e6a1a513b Mon Sep 17 00:00:00 2001 From: kulek Date: Sun, 21 Jun 2026 19:02:35 +0200 Subject: [PATCH 4/4] refactor: create InlineTuningGlyph --- .../src/rendering/glyphs/InlineTuningGlyph.ts | 72 ++++++++++++ .../src/rendering/staves/StaffSystem.ts | 110 ++++++++---------- 2 files changed, 123 insertions(+), 59 deletions(-) create mode 100644 packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts diff --git a/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts b/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts new file mode 100644 index 000000000..6310e2024 --- /dev/null +++ b/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts @@ -0,0 +1,72 @@ +import { Tuning } from '@coderline/alphatab/model/Tuning'; +import { TrackSubElement } from '@coderline/alphatab/model/Track'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; +import { type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; +import { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; +import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; + +/** + * @internal + */ +export class InlineTuningGlyph extends Glyph { + public readonly staff: RenderStaff; + + private readonly _tabRenderer: TabBarRenderer; + private readonly _tunings: number[]; + + public constructor(staff: RenderStaff, tabRenderer: TabBarRenderer) { + super(0, 0); + this.staff = staff; + this._tabRenderer = tabRenderer; + this._tunings = staff.modelStaff.stringTuning.tunings; + this.renderer = tabRenderer; + } + + public override doLayout(): void { + const canvas = this.renderer.scoreRenderer.canvas!; + const oldFont = canvas.font; + canvas.font = this.renderer.resources.elementFonts.get(NotationElement.GuitarTuning)!; + + let textWidth = 0; + for (const tuning of this._tunings) { + textWidth = Math.max(textWidth, canvas.measureText(Tuning.getTextForTuning(tuning, false)).width); + } + + canvas.font = oldFont; + + this.width = textWidth > 0 ? textWidth + this.renderer.settings.display.inlineTuningPaddingRight : 0; + this.height = this._tabRenderer.height; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + if (this.width === 0) { + return; + } + + const oldFont = canvas.font; + const oldBaseLine = canvas.textBaseline; + const oldTextAlign = canvas.textAlign; + + canvas.font = this.renderer.resources.elementFonts.get(NotationElement.GuitarTuning)!; + canvas.textBaseline = TextBaseline.Middle; + canvas.textAlign = TextAlign.Right; + + const textEndX = cx - this.renderer.settings.display.inlineTuningPaddingRight; + + using _ = ElementStyleHelper.track(canvas, TrackSubElement.StringTuning, this.staff.modelStaff.track, true); + + for (let i = 0, j = this._tunings.length; i < j; i++) { + canvas.fillText( + Tuning.getTextForTuning(this._tunings[i], false), + textEndX, + cy + this._tabRenderer.y + this._tabRenderer.getLineY(i) + ); + } + + canvas.font = oldFont; + canvas.textBaseline = oldBaseLine; + canvas.textAlign = oldTextAlign; + } +} diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index eff3a1c64..81cec2156 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -10,7 +10,6 @@ import { TuningDisplayMode } from '@coderline/alphatab/model/RenderStylesheet'; import { type Track, TrackSubElement } from '@coderline/alphatab/model/Track'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; @@ -18,6 +17,7 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import type { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; +import { InlineTuningGlyph } from '@coderline/alphatab/rendering/glyphs/InlineTuningGlyph'; import { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; import { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; @@ -161,6 +161,8 @@ export class StaffSystem { private _brackets: SystemBracket[] = []; private _staffToBracket = new Map(); + private _inlineTuningGlyphs: InlineTuningGlyph[] = []; + private _inlineTuningWidth = 0; private _contentHeight = 0; private _hasSystemSeparator = false; @@ -660,7 +662,8 @@ export class StaffSystem { } } - this.accoladeWidth += this._getInlineTuningWidth(this.allStaves, this.layout.renderer.canvas!); + this._createInlineTuningGlyphs(); + this.accoladeWidth += this._inlineTuningWidth; // NOTE: we have a chicken-egg problem when it comes to scaling braces which we try to mitigate here: // - The brace scales with the height of the system @@ -701,15 +704,42 @@ export class StaffSystem { } } - private _shouldRenderInlineTuning(staff: RenderStaff): boolean { + private _createInlineTuningGlyphs(): void { + this._inlineTuningGlyphs = []; + this._inlineTuningWidth = 0; + const score = this.layout.renderer.score!; if ( this.index !== 0 || - !staff.isVisible || - staff.staffId !== TabBarRenderer.StaffId || !this.layout.renderer.settings.notation.isNotationElementVisible(NotationElement.GuitarTuning) || !score.stylesheet.globalDisplayTuning || score.stylesheet.tuningDisplayMode !== TuningDisplayMode.Staff + ) { + return; + } + + for (const staff of this.allStaves) { + if (!this._shouldCreateInlineTuningGlyph(staff)) { + continue; + } + + const renderer = staff.barRenderers[0]; + if (!(renderer instanceof TabBarRenderer)) { + continue; + } + + const glyph = new InlineTuningGlyph(staff, renderer); + glyph.doLayout(); + this._inlineTuningGlyphs.push(glyph); + this._inlineTuningWidth = Math.max(this._inlineTuningWidth, glyph.width); + } + } + + private _shouldCreateInlineTuningGlyph(staff: RenderStaff): boolean { + const score = this.layout.renderer.score!; + if ( + !staff.isVisible || + staff.staffId !== TabBarRenderer.StaffId ) { return false; } @@ -732,30 +762,22 @@ export class StaffSystem { ); } - private _getInlineTuningWidth(staves: Iterable, canvas: ICanvas): number { - const oldFont = canvas.font; - canvas.font = this.layout.renderer.settings.display.resources.elementFonts.get(NotationElement.GuitarTuning)!; + private _getInlineTuningWidthForTrackGroup(group: StaffTrackGroup): number { + return this._getInlineTuningWidth(glyph => glyph.staff.staffTrackGroup === group); + } - let maxWidth = 0; - for (const staff of staves) { - if (!this._shouldRenderInlineTuning(staff)) { - continue; - } + private _getInlineTuningWidthForBracket(bracket: SystemBracket): number { + return this._getInlineTuningWidth(glyph => bracket.includesStaff(glyph.staff)); + } - for (const tuning of staff.modelStaff.stringTuning.tunings) { - maxWidth = Math.max(maxWidth, canvas.measureText(Tuning.getTextForTuning(tuning, false)).width); + private _getInlineTuningWidth(includeGlyph: (glyph: InlineTuningGlyph) => boolean): number { + let width = 0; + for (const glyph of this._inlineTuningGlyphs) { + if (includeGlyph(glyph)) { + width = Math.max(width, glyph.width); } } - canvas.font = oldFont; - - return maxWidth > 0 ? maxWidth + this.layout.renderer.settings.display.inlineTuningPaddingRight : 0; - } - - private _getInlineTuningWidthForBracket(bracket: SystemBracket, canvas: ICanvas): number { - return this._getInlineTuningWidth( - this.allStaves.filter(staff => bracket.includesStaff(staff)), - canvas - ); + return width; } private _getStaffTrackGroup(track: Track): StaffTrackGroup | null { @@ -936,7 +958,7 @@ export class StaffSystem { g.staves[0].x - // left side of the bracket settings.display.accoladeBarPaddingRight - - this._getInlineTuningWidth(g.staves, canvas) - + this._getInlineTuningWidthForTrackGroup(g) - (g.bracket?.width ?? 0) - // padding between label and bracket settings.display.systemLabelPaddingRight; @@ -1010,39 +1032,9 @@ export class StaffSystem { } private _paintInlineTunings(cx: number, cy: number, canvas: ICanvas): void { - const oldFont = canvas.font; - const oldBaseLine = canvas.textBaseline; - const oldTextAlign = canvas.textAlign; - - canvas.font = this.layout.renderer.settings.display.resources.elementFonts.get(NotationElement.GuitarTuning)!; - canvas.textBaseline = TextBaseline.Middle; - canvas.textAlign = TextAlign.Right; - - for (const staff of this.allStaves) { - if (!this._shouldRenderInlineTuning(staff)) { - continue; - } - - const renderer = staff.barRenderers[0]; - if (!(renderer instanceof TabBarRenderer)) { - continue; - } - const textEndX = cx + staff.x - this.layout.renderer.settings.display.inlineTuningPaddingRight; - - using _ = ElementStyleHelper.track(canvas, TrackSubElement.StringTuning, staff.modelStaff.track, true); - - for (let i = 0, j = staff.modelStaff.stringTuning.tunings.length; i < j; i++) { - canvas.fillText( - Tuning.getTextForTuning(staff.modelStaff.stringTuning.tunings[i], false), - textEndX, - cy + staff.y + renderer.y + renderer.getLineY(i) - ); - } + for (const glyph of this._inlineTuningGlyphs) { + glyph.paint(cx + glyph.staff.x, cy + glyph.staff.y, canvas); } - - canvas.font = oldFont; - canvas.textBaseline = oldBaseLine; - canvas.textAlign = oldTextAlign; } private _paintBrackets(cx: number, cy: number, canvas: ICanvas) { @@ -1053,7 +1045,7 @@ export class StaffSystem { const barStartX: number = cx + bracket.firstVisibleStaffInBracket!.x; const barSize: number = bracket.width; const barOffset: number = - settings.display.accoladeBarPaddingRight + this._getInlineTuningWidthForBracket(bracket, canvas); + settings.display.accoladeBarPaddingRight + this._getInlineTuningWidthForBracket(bracket); const firstStart: number = cy + bracket.firstVisibleStaffInBracket!.contentTop; const lastEnd: number = cy + bracket.lastVisibleStaffInBracket!.contentBottom; let accoladeStart: number = firstStart;