Skip to content

Commit

Permalink
Fixed PurpleKingdomGames#617: Text letter space + line height
Browse files Browse the repository at this point in the history
  • Loading branch information
davesmith00000 committed Nov 18, 2023
1 parent 93accaa commit b7b810a
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,24 @@ object BoundaryLocatorBenchmarks:
Suite("BoundaryLocator Benchmarks")(
Benchmark("(text) textLineBounds (purge)") {
boundaryLocator.purgeCache()
boundaryLocator.textLineBounds(TextSamples.textValue, TextSamples.fontInfo)
boundaryLocator.textLineBounds(TextSamples.textValue, TextSamples.fontInfo, 0, 0)
},
Benchmark("(text) textLineBounds (no purge)") {
boundaryLocator.textLineBounds(TextSamples.textValue, TextSamples.fontInfo)
boundaryLocator.textLineBounds(TextSamples.textValue, TextSamples.fontInfo, 0, 0)
},
Benchmark("(text) textAsLinesWithBounds (purge)") {
boundaryLocator.purgeCache()
boundaryLocator.textAsLinesWithBounds(TextSamples.textValue, TextSamples.fontKey)
boundaryLocator.textAsLinesWithBounds(TextSamples.textValue, TextSamples.fontKey, 0, 0)
},
Benchmark("(text) textAsLinesWithBounds (no purge)") {
boundaryLocator.textAsLinesWithBounds(TextSamples.textValue, TextSamples.fontKey)
boundaryLocator.textAsLinesWithBounds(TextSamples.textValue, TextSamples.fontKey, 0, 0)
},
Benchmark("(text) textAllLineBounds (purge)") {
boundaryLocator.purgeCache()
boundaryLocator.textAllLineBounds(TextSamples.textValue, TextSamples.fontKey)
boundaryLocator.textAllLineBounds(TextSamples.textValue, TextSamples.fontKey, 0, 0)
},
Benchmark("(text) textAllLineBounds (no purge)") {
boundaryLocator.textAllLineBounds(TextSamples.textValue, TextSamples.fontKey)
boundaryLocator.textAllLineBounds(TextSamples.textValue, TextSamples.fontKey, 0, 0)
},
Benchmark("(text) textBounds (purge)") {
boundaryLocator.purgeCache()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ final case class InputField(

val cursorPositionPoint =
boundaryLocator
.textAsLinesWithBounds(textToCursor, field.fontKey)
.textAsLinesWithBounds(textToCursor, field.fontKey, field.letterSpacing, field.lineHeight)
.reverse
.headOption
.map(_.lineBounds.topRight + position)
Expand Down
35 changes: 19 additions & 16 deletions indigo/indigo/src/main/scala/indigo/shared/BoundaryLocator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -135,28 +135,31 @@ final class BoundaryLocator(

// Text / Fonts

def textLineBounds(lineText: String, fontInfo: FontInfo): Rectangle =
QuickCache(s"""textline-${fontInfo.fontKey}-$lineText""") {
def textLineBounds(lineText: String, fontInfo: FontInfo, letterSpacing: Int, lineHeight: Int): Rectangle =
QuickCache(s"""textline-${fontInfo.fontKey}-$lineText-${letterSpacing.toString()}-${lineHeight.toString()}""") {
if lineText.isEmpty then {
val b = fontInfo.findByCharacter(' ').bounds
b.withSize(Size(0, b.height))
b.withSize(Size(0, b.height + lineHeight))
} else {
lineText
.toCharArray()
.map(c => fontInfo.findByCharacter(c).bounds)
.foldLeft(Rectangle.zero) { (acc, curr) =>
Rectangle(0, 0, acc.width + curr.width, Math.max(acc.height, curr.height))
}
val b =
lineText
.toCharArray()
.map(c => fontInfo.findByCharacter(c).bounds)
.foldLeft(Rectangle.zero) { (acc, curr) =>
Rectangle(0, 0, acc.width + curr.width + letterSpacing, Math.max(acc.height, curr.height + lineHeight))
}
b.withSize(b.size - Size(letterSpacing, 0))
}

}

def textAsLinesWithBounds(text: String, fontKey: FontKey): Batch[TextLine] =
QuickCache(s"""text-lines-$fontKey-$text""") {
def textAsLinesWithBounds(text: String, fontKey: FontKey, letterSpacing: Int, lineHeight: Int): Batch[TextLine] =
QuickCache(s"""text-lines-$fontKey-$text-${letterSpacing.toString()}-${lineHeight.toString()}""") {
fontRegister
.findByFontKey(fontKey)
.map { fontInfo =>
text.linesIterator.toList
.map(lineText => new TextLine(lineText, textLineBounds(lineText, fontInfo)))
.map(lineText => new TextLine(lineText, textLineBounds(lineText, fontInfo, letterSpacing, lineHeight)))
.foldLeft((0, Batch.empty[TextLine])) { case ((yPos, lines), textLine) =>
(yPos + textLine.lineBounds.height, lines ++ Batch(textLine.moveTo(0, yPos)))
}
Expand All @@ -168,13 +171,13 @@ final class BoundaryLocator(
}
}

def textAllLineBounds(text: String, fontKey: FontKey): Array[Rectangle] =
QuickCache(s"""text-all-line-bounds-$fontKey-$text""") {
def textAllLineBounds(text: String, fontKey: FontKey, letterSpacing: Int, lineHeight: Int): Array[Rectangle] =
QuickCache(s"""text-all-line-bounds-$fontKey-$text-${letterSpacing.toString()}-${lineHeight.toString()}""") {
fontRegister
.findByFontKey(fontKey)
.map { fontInfo =>
text.linesIterator.toArray
.map(lineText => textLineBounds(lineText, fontInfo))
.map(lineText => textLineBounds(lineText, fontInfo, letterSpacing, lineHeight))
.foldLeft((0, Array[Rectangle]())) { case ((yPos, lines), lineBounds) =>
(yPos + lineBounds.height, lines ++ Array(lineBounds.moveTo(0, yPos)))
}
Expand All @@ -188,7 +191,7 @@ final class BoundaryLocator(

def textBounds(text: Text[_]): Rectangle =
val unaligned =
textAllLineBounds(text.text, text.fontKey)
textAllLineBounds(text.text, text.fontKey, text.letterSpacing, text.lineHeight)
.fold(Rectangle.zero) { (acc, next) =>
acc.resize(Size(Math.max(acc.width, next.width), acc.height + next.height))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ final class DisplayObjectConversions(

val letters: scalajs.js.Array[DisplayEntity] =
boundaryLocator
.textAsLinesWithBounds(x.text, x.fontKey)
.textAsLinesWithBounds(x.text, x.fontKey, x.letterSpacing, x.lineHeight)
.toJSArray
.foldLeft(0 -> scalajs.js.Array[DisplayEntity]()) { (acc, textLine) =>
(
Expand Down Expand Up @@ -375,7 +375,7 @@ final class DisplayObjectConversions(

val letters: scalajs.js.Array[CloneTileData] =
boundaryLocator
.textAsLinesWithBounds(x.text, x.fontKey)
.textAsLinesWithBounds(x.text, x.fontKey, x.letterSpacing, x.lineHeight)
.toJSArray
.foldLeft(
0 -> scalajs.js.Array[CloneTileData]()
Expand Down Expand Up @@ -722,7 +722,7 @@ final class DisplayObjectConversions(
}

QuickCache(lineHash) {
zipWithCharDetails(line.text.toArray, fontInfo).map { case (fontChar, xPosition) =>
zipWithCharDetails(line.text.toArray, fontInfo, leaf.letterSpacing).map { case (fontChar, xPosition) =>
val frameInfo =
QuickCache(fontChar.bounds.hashCode().toString + "_" + shaderDataHash) {
SpriteSheetFrame.calculateFrameOffset(
Expand Down Expand Up @@ -853,7 +853,7 @@ final class DisplayObjectConversions(
leaf.fontKey.toString

QuickCache(lineHash) {
zipWithCharDetails(line.text.toArray, fontInfo).map { case (fontChar, xPosition) =>
zipWithCharDetails(line.text.toArray, fontInfo, leaf.letterSpacing).map { case (fontChar, xPosition) =>
CloneTileData(
x = leaf.position.x + leaf.ref.x + xPosition + alignmentOffsetX,
y = leaf.position.y + leaf.ref.y + yOffset,
Expand All @@ -872,15 +872,21 @@ final class DisplayObjectConversions(
@SuppressWarnings(Array("scalafix:DisableSyntax.var"))
private var accCharDetails: scalajs.js.Array[(FontChar, Int)] = new scalajs.js.Array()

private def zipWithCharDetails(charList: Array[Char], fontInfo: FontInfo): scalajs.js.Array[(FontChar, Int)] = {
private def zipWithCharDetails(
charList: Array[Char],
fontInfo: FontInfo,
letterSpacing: Int
): scalajs.js.Array[(FontChar, Int)] = {
@tailrec
def rec(remaining: scalajs.js.Array[(Char, FontChar)], nextX: Int): scalajs.js.Array[(FontChar, Int)] =
if remaining.isEmpty then accCharDetails
else
val x = remaining.head
val xs = remaining.tail
(x._2, nextX) +=: accCharDetails
rec(xs, nextX + x._2.bounds.width)

val ls = if xs.isEmpty then 0 else letterSpacing
rec(xs, nextX + x._2.bounds.width + ls)

accCharDetails = new scalajs.js.Array()
rec(charList.toJSArray.map(c => (c, fontInfo.findByCharacter(c))), 0)
Expand Down
14 changes: 14 additions & 0 deletions indigo/indigo/src/main/scala/indigo/shared/scenegraph/Text.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ final case class Text[M <: Material](
text: String,
alignment: TextAlignment,
fontKey: FontKey,
lineHeight: Int,
letterSpacing: Int,
material: M,
eventHandlerEnabled: Boolean,
eventHandler: ((Text[_], GlobalEvent)) => Option[GlobalEvent],
Expand Down Expand Up @@ -98,6 +100,14 @@ final case class Text[M <: Material](
def withFontKey(newFontKey: FontKey): Text[M] =
this.copy(fontKey = newFontKey)

/** Sets the vertical gap between lines of text _in addition_ (relative to) to the actual height of the text. Defaults to 0. */
def withLineHeight(amount: Int): Text[M] =
this.copy(lineHeight = amount)

/** Sets the horiztonal gap between letters in a line of text. Defaults to 0. */
def withLetterSpacing(amount: Int): Text[M] =
this.copy(letterSpacing = amount)

def withEventHandler(f: ((Text[_], GlobalEvent)) => Option[GlobalEvent]): Text[M] =
this.copy(eventHandler = f, eventHandlerEnabled = true)
def onEvent(f: PartialFunction[((Text[_], GlobalEvent)), GlobalEvent]): Text[M] =
Expand All @@ -120,6 +130,8 @@ object Text:
text = text,
alignment = TextAlignment.Left,
fontKey = fontKey,
lineHeight = 0,
letterSpacing = 0,
eventHandlerEnabled = false,
eventHandler = Function.const(None),
material = material
Expand All @@ -136,6 +148,8 @@ object Text:
text = text,
alignment = TextAlignment.Left,
fontKey = fontKey,
lineHeight = 0,
letterSpacing = 0,
eventHandlerEnabled = false,
eventHandler = Function.const(None),
material = material
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,39 +28,67 @@ class BoundaryLocatorTests extends munit.FunSuite {
new BoundaryLocator(new AnimationsRegister, fontRegister, new DynamicText)

test("Text boundary calculations.Text as lines with bounds.empty") {
val actual = boundaryLocator.textAsLinesWithBounds("", fontKey)
val actual = boundaryLocator.textAsLinesWithBounds("", fontKey, 0, 0)
assertEquals(actual.length, 0)
}
test("Text boundary calculations.Text as lines with bounds.abc") {
val actual = boundaryLocator.textAsLinesWithBounds("abc", fontKey)
val actual = boundaryLocator.textAsLinesWithBounds("abc", fontKey, 0, 0)
assertEquals(actual.length, 1)
assertEquals(actual.headOption.get.text, "abc")
assertEquals(actual.headOption.get.lineBounds, Rectangle(0, 0, 42, 20))
}
test("Text boundary calculations.Text as lines with bounds.ab->c") {
val actual = boundaryLocator.textAsLinesWithBounds("ab\nc", fontKey)
val actual = boundaryLocator.textAsLinesWithBounds("ab\nc", fontKey, 0, 0)
assertEquals(actual.length, 2)
assertEquals(actual(0).text, "ab")
assertEquals(actual(0).lineBounds, Rectangle(0, 0, 26, 20))
assertEquals(actual(1).text, "c")
assertEquals(actual(1).lineBounds, Rectangle(0, 20, 16, 16))
}
test("Text boundary calculations.Text as lines with bounds.abc with letterSpacing") {
val actual = boundaryLocator.textAsLinesWithBounds("abc", fontKey, 10, 0)
assertEquals(actual.length, 1)
assertEquals(actual.head.text, "abc")
assertEquals(actual.head.lineBounds, Rectangle(0, 0, 42 + 20, 20))
}

test("Text boundary calculations.textAllLineBounds with bounds.empty") {
val actual = boundaryLocator.textAllLineBounds("", fontKey)
val actual = boundaryLocator.textAllLineBounds("", fontKey, 0, 0)
assertEquals(actual.length, 0)
}
test("Text boundary calculations.textAllLineBounds with bounds.abc") {
val actual = boundaryLocator.textAllLineBounds("abc", fontKey)
val actual = boundaryLocator.textAllLineBounds("abc", fontKey, 0, 0)
assertEquals(actual.length, 1)
assertEquals(actual.headOption.get, Rectangle(0, 0, 42, 20))
}
test("Text boundary calculations.textAllLineBounds with bounds.ab->c") {
val actual = boundaryLocator.textAllLineBounds("ab\nc", fontKey)
val actual = boundaryLocator.textAllLineBounds("ab\nc", fontKey, 0, 0)
assertEquals(actual.length, 2)
assertEquals(actual(0), Rectangle(0, 0, 26, 20))
assertEquals(actual(1), Rectangle(0, 20, 16, 16))
}
test("Text boundary calculations.textAllLineBounds with bounds.ab->c with lineHeight") {
val lineHeight = 10
val actual = boundaryLocator.textAllLineBounds("ab\nc", fontKey, 0, lineHeight)
assertEquals(actual.length, 2)
assertEquals(actual(0), Rectangle(0, 0, 26, 20 + lineHeight))
assertEquals(actual(1), Rectangle(0, 20 + lineHeight, 16, 16 + lineHeight))
}
test("Text boundary calculations.textAllLineBounds with bounds.ab->c with letterSpacing") {
val letterSpacing = 10
val actual = boundaryLocator.textAllLineBounds("ab\nc", fontKey, letterSpacing, 0)
assertEquals(actual.length, 2)
assertEquals(actual(0), Rectangle(0, 0, 26 + letterSpacing, 20))
assertEquals(actual(1), Rectangle(0, 20, 16, 16))
}
test("Text boundary calculations.textAllLineBounds with bounds.ab->c with lineHeight and letterSpacing") {
val lineHeight = 10
val letterSpacing = 10
val actual = boundaryLocator.textAllLineBounds("ab\nc", fontKey, letterSpacing, lineHeight)
assertEquals(actual.length, 2)
assertEquals(actual(0), Rectangle(0, 0, 26 + letterSpacing, 20 + lineHeight))
assertEquals(actual(1), Rectangle(0, 20 + lineHeight, 16, 16 + lineHeight))
}

// These should be identical, regardless of alignment.
// The lines move around within the same bounding area.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,6 @@ object SandboxView:
case _ =>
None
}
.withLineHeight(20)
.withLetterSpacing(10)
)

0 comments on commit b7b810a

Please sign in to comment.