feat: anchor footer elements to bottom of page (Closes #31)
Footer elements (notes, Worte/Weise metadata, reference book table) on song pages now render at fixed positions anchored to the bottom margin instead of flowing after song content. This ensures consistent vertical positioning across all pages regardless of content length. The footer area is computed from bottomMargin + footerReservation and elements render top-down within it: notes -> metadata -> references. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -424,60 +424,68 @@ class PdfBookRenderer : BookRenderer {
|
|||||||
y -= config.layout.verseSpacing / 0.3528f
|
y -= config.layout.verseSpacing / 0.3528f
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render notes on the last page
|
// Render footer elements (notes, metadata, references) anchored to the bottom of the page.
|
||||||
if (isLastPage && song.notes.isNotEmpty()) {
|
// Instead of flowing from the current y position after song content, we compute a fixed
|
||||||
y -= 4f
|
// starting Y at the top of the footer area (bottomMargin + footerReservation) and render
|
||||||
|
// top-down: notes -> metadata -> references. This ensures footer elements always appear
|
||||||
|
// at the same vertical position regardless of how much song content is on the page.
|
||||||
|
if (isLastPage && footerReservation > 0f) {
|
||||||
val metaFont = fontMetrics.getBaseFont(config.fonts.metadata)
|
val metaFont = fontMetrics.getBaseFont(config.fonts.metadata)
|
||||||
val metaSize = config.fonts.metadata.size
|
val metaSize = config.fonts.metadata.size
|
||||||
val noteLineHeight = metaSize * 1.5f
|
|
||||||
|
|
||||||
for ((idx, note) in song.notes.withIndex()) {
|
// The footer area spans from bottomMargin to bottomMargin + footerReservation.
|
||||||
val wrappedLines = wrapText(note, metaFont, metaSize, contentWidth)
|
// Start rendering from the top of this area, flowing downward.
|
||||||
for (wrappedLine in wrappedLines) {
|
var footerY = bottomMargin + footerReservation
|
||||||
cb.beginText()
|
|
||||||
cb.setFontAndSize(metaFont, metaSize)
|
|
||||||
cb.setColorFill(Color.GRAY)
|
|
||||||
cb.setTextMatrix(leftMargin, y - metaSize)
|
|
||||||
cb.showText(wrappedLine)
|
|
||||||
cb.endText()
|
|
||||||
y -= noteLineHeight
|
|
||||||
}
|
|
||||||
// Add paragraph spacing between note paragraphs
|
|
||||||
if (idx < song.notes.size - 1) {
|
|
||||||
y -= noteLineHeight * 0.3f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render metadata at bottom of song page (if configured) - on the last page only
|
// Render notes (topmost footer element)
|
||||||
if (renderMetaAtBottom && isLastPage) {
|
if (song.notes.isNotEmpty()) {
|
||||||
val metaParts = buildMetadataLines(song, config)
|
footerY -= 4f // gap before notes
|
||||||
if (metaParts.isNotEmpty()) {
|
val noteLineHeight = metaSize * 1.5f
|
||||||
y -= 4f
|
for ((idx, note) in song.notes.withIndex()) {
|
||||||
val metaFont = fontMetrics.getBaseFont(config.fonts.metadata)
|
val wrappedLines = wrapText(note, metaFont, metaSize, contentWidth)
|
||||||
val metaSize = config.fonts.metadata.size
|
|
||||||
for (metaLine in metaParts) {
|
|
||||||
val wrappedLines = wrapText(metaLine, metaFont, metaSize, contentWidth)
|
|
||||||
for (wrappedLine in wrappedLines) {
|
for (wrappedLine in wrappedLines) {
|
||||||
cb.beginText()
|
cb.beginText()
|
||||||
cb.setFontAndSize(metaFont, metaSize)
|
cb.setFontAndSize(metaFont, metaSize)
|
||||||
cb.setColorFill(Color.GRAY)
|
cb.setColorFill(Color.GRAY)
|
||||||
cb.setTextMatrix(leftMargin, y - metaSize)
|
cb.setTextMatrix(leftMargin, footerY - metaSize)
|
||||||
cb.showText(wrappedLine)
|
cb.showText(wrappedLine)
|
||||||
cb.endText()
|
cb.endText()
|
||||||
y -= metaSize * 1.5f
|
footerY -= noteLineHeight
|
||||||
|
}
|
||||||
|
if (idx < song.notes.size - 1) {
|
||||||
|
footerY -= noteLineHeight * 0.3f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Render reference book footer on the last page of the song
|
// Render metadata (Worte/Weise) below notes, if configured at bottom
|
||||||
if (config.referenceBooks.isNotEmpty() && song.references.isNotEmpty() && isLastPage) {
|
if (renderMetaAtBottom) {
|
||||||
y -= 4f // gap before reference footer
|
val metaParts = buildMetadataLines(song, config)
|
||||||
renderReferenceFooter(
|
if (metaParts.isNotEmpty()) {
|
||||||
cb, fontMetrics, config, song,
|
footerY -= 4f // gap before metadata
|
||||||
leftMargin, y, contentWidth
|
for (metaLine in metaParts) {
|
||||||
)
|
val wrappedLines = wrapText(metaLine, metaFont, metaSize, contentWidth)
|
||||||
|
for (wrappedLine in wrappedLines) {
|
||||||
|
cb.beginText()
|
||||||
|
cb.setFontAndSize(metaFont, metaSize)
|
||||||
|
cb.setColorFill(Color.GRAY)
|
||||||
|
cb.setTextMatrix(leftMargin, footerY - metaSize)
|
||||||
|
cb.showText(wrappedLine)
|
||||||
|
cb.endText()
|
||||||
|
footerY -= metaSize * 1.5f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render reference book footer (bottommost footer element, just above page number)
|
||||||
|
if (config.referenceBooks.isNotEmpty() && song.references.isNotEmpty()) {
|
||||||
|
footerY -= 4f // gap before reference footer
|
||||||
|
renderReferenceFooter(
|
||||||
|
cb, fontMetrics, config, song,
|
||||||
|
leftMargin, footerY, contentWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user