Skip to content

Commit

Permalink
#199: ChunkListView now uses all available space
Browse files Browse the repository at this point in the history
  • Loading branch information
rladstaetter committed Jul 30, 2024
1 parent 87cfec1 commit 2850607
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .run/LogoRRRApp.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
<configuration default="false" name="LogoRRRApp" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="app.logorrr.LogoRRRApp" />
<module name="app" />
<option name="VM_PARAMETERS" value="-Xmx8g -Djava.util.logging.config.file=develop-logging.properties -Djava.library.path=$PROJECT_DIR$/native/native-osx/target -Duser.language=en --module-path $PROJECT_DIR$/env/target/javafx-sdk-22.0.2/lib --add-modules javafx.controls,javafx.fxml --add-exports javafx.base/com.sun.javafx.binding=ALL-UNNAMED" />
<option name="VM_PARAMETERS" value="-Xmx2g -XX:+UseZGC -Djava.util.logging.config.file=develop-logging.properties -Djava.library.path=$PROJECT_DIR$/native/native-osx/target -Duser.language=en --module-path $PROJECT_DIR$/env/target/javafx-sdk-22.0.2/lib --add-modules javafx.controls,javafx.fxml --add-exports javafx.base/com.sun.javafx.binding=ALL-UNNAMED" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="app.logorrr.*" />
Expand Down
33 changes: 4 additions & 29 deletions app/src/main/scala/app/logorrr/views/block/ChunkImage.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package app.logorrr.views.block

import app.logorrr.model.LogEntry
import app.logorrr.util.MathUtil
import app.logorrr.views.search.Filter
import javafx.beans.property.{ReadOnlyDoubleProperty, SimpleIntegerProperty}
import javafx.collections.ObservableList
Expand All @@ -17,42 +16,18 @@ object ChunkImage {

val MaxHeight = 4096

val DefaultScrollBarWidth = 18
// width of Scrollbars
val ScrollBarWidth = 18
val scrollBarWidthProperty = new SimpleIntegerProperty(DefaultScrollBarWidth)
def setScrollBarWidth(width : Int): Unit = scrollBarWidthProperty.set(width)
def getScrollBarWidth: Int = scrollBarWidthProperty.get()

// assuming we have a grid of rectangles, and x and y give the coordinate of a mouse click
// this function should return the correct index for the surrounding rectangle
def indexOf(x: Int, y: Int, blockWidth: Int, blockViewWidth: Int): Int = {
y / blockWidth * (blockViewWidth / blockWidth) + x / blockWidth
}

/**
* Calculates overall height of virtual canvas
*
* @param blockWidth width of a block
* @param blockHeight height of a block
* @param width width of canvas
* @param nrEntries number of elements
* @return
*/
def calcVirtualHeight(blockWidth: Int
, blockHeight: Int
, width: Int
, nrEntries: Int): Int = {
if (blockHeight == 0 || nrEntries == 0) {
0
} else {
if (width > blockWidth) {
val elemsPerRow = width.toDouble / blockWidth
val nrRows = nrEntries.toDouble / elemsPerRow
val decimal1: BigDecimal = MathUtil.roundUp(nrRows)
val res = decimal1.intValue * blockHeight
res
} else {
0
}
}
}

def apply(chunk: Chunk
, filtersProperty: ObservableList[Filter]
Expand Down
65 changes: 63 additions & 2 deletions app/src/main/scala/app/logorrr/views/block/ChunkListView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,32 @@ import app.logorrr.views.search.Filter
import javafx.beans.property.{SimpleDoubleProperty, SimpleIntegerProperty}
import javafx.beans.value.{ChangeListener, ObservableValue}
import javafx.collections.{FXCollections, ObservableList}
import javafx.scene.control.ListView
import javafx.geometry.Orientation
import javafx.scene.control.skin.VirtualFlow
import javafx.scene.control.{ListView, ScrollBar, Skin, SkinBase}

import java.lang
import scala.jdk.CollectionConverters.CollectionHasAsScala
import scala.util.{Failure, Success, Try}


object ChunkListView {

def lookupVirtualFlow(skin: Skin[_]): Option[VirtualFlow[ChunkListCell]] = {
Option(skin match {
case skinBase: SkinBase[_] =>
skinBase.getChildren.asScala.find(_.getStyleClass.contains("virtual-flow")).orNull.asInstanceOf[VirtualFlow[ChunkListCell]]
case _ =>
null
})
}

def lookupScrollBar(flow: VirtualFlow[ChunkListCell], orientation: Orientation): Option[ScrollBar] = {
Option(flow.getChildrenUnmodifiable.toArray.collectFirst { case sb: ScrollBar if sb.getOrientation == orientation => sb }.orNull)
}

def calcListViewWidth(listViewWidth: Double): Double = {
if (listViewWidth - ChunkImage.ScrollBarWidth >= 0) listViewWidth - ChunkImage.ScrollBarWidth else listViewWidth
if (listViewWidth - ChunkImage.getScrollBarWidth >= 0) listViewWidth - ChunkImage.getScrollBarWidth else listViewWidth
}

def apply(entries: ObservableList[LogEntry]
Expand Down Expand Up @@ -58,6 +75,34 @@ class ChunkListView(val logEntries: ObservableList[LogEntry]
extends ListView[Chunk]
with CanLog {

/**
* What should happen if the scrollbar appears/vanishes
*/
private val scrollBarVisibilityListener = new ChangeListener[lang.Boolean] {
override def changed(observableValue: ObservableValue[_ <: lang.Boolean], t: lang.Boolean, isVisible: lang.Boolean): Unit = {
if (isVisible) {
ChunkImage.setScrollBarWidth(ChunkImage.DefaultScrollBarWidth)
recalculateAndUpdateItems("scrollbar visible")
} else {
ChunkImage.setScrollBarWidth(0)
recalculateAndUpdateItems("scrollbar invisible")
}
}
}

// needed to get access to the scrollbar
private val chunkListViewSkinListener: ChangeListener[Skin[_]] = new ChangeListener[Skin[_]] {
override def changed(observableValue: ObservableValue[_ <: Skin[_]], t: Skin[_], currentSkin: Skin[_]): Unit = {
for {skin <- Option(currentSkin)
flow <- ChunkListView.lookupVirtualFlow(skin)
horizontalScrollbar <- ChunkListView.lookupScrollBar(flow, Orientation.HORIZONTAL)
verticalScrollbar <- ChunkListView.lookupScrollBar(flow, Orientation.VERTICAL)} {
horizontalScrollbar.setVisible(false)
verticalScrollbar.visibleProperty().addListener(scrollBarVisibilityListener)
}
}
}

/**
* If the observable list changes in any way, recalculate the items in the listview.
*/
Expand All @@ -79,6 +124,8 @@ class ChunkListView(val logEntries: ObservableList[LogEntry]
/** if height changes, recalculate */
private val heightRp = mkRecalculateAndUpdateItemListener("height")

// private val changingScrollbarVisibilityRp = mkRecalculateAndUpdateItemListener("scrollbar")

// context variable just here for debugging
private def mkRecalculateAndUpdateItemListener(ctx: String): ChangeListener[Number] = (_: ObservableValue[_ <: Number], oldValue: Number, newValue: Number) => {
if (oldValue != newValue && newValue.doubleValue() != 0.0) {
Expand Down Expand Up @@ -135,8 +182,12 @@ class ChunkListView(val logEntries: ObservableList[LogEntry]
blockSizeProperty.addListener(blockSizeRp)
widthProperty().addListener(widthRp)
heightProperty().addListener(heightRp)
skinProperty().addListener(chunkListViewSkinListener)


}


def removeListeners(): Unit = {
logEntries.removeListener(logEntriesInvalidationListener)

Expand All @@ -146,8 +197,18 @@ class ChunkListView(val logEntries: ObservableList[LogEntry]
blockSizeProperty.removeListener(blockSizeRp)
widthProperty().removeListener(widthRp)
heightProperty().removeListener(heightRp)


for {skin <- Option(getSkin)
flow <- ChunkListView.lookupVirtualFlow(skin)
verticalScrollbar <- ChunkListView.lookupScrollBar(flow, Orientation.VERTICAL)} {
verticalScrollbar.visibleProperty().removeListener(scrollBarVisibilityListener)
}

skinProperty().removeListener(chunkListViewSkinListener)
}


/**
* Recalculates elements of listview depending on width, height and blocksize.
*/
Expand Down
65 changes: 42 additions & 23 deletions app/src/main/scala/app/logorrr/views/block/LPixelBuffer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ object LPixelBuffer extends CanLog {
)
}

// performance sensitive function
// using while loops for more performance
// use benchmark module to perform statistics
// here are the current results (macbook pro m1)
// LogoRRRBenchmark.benchmarkDrawRect thrpt 50 782011,597 ± 605,345 ops/s
// LogoRRRBenchmark.benchmarkDrawRect thrpt 50 781747,451 ± 795,479 ops/s
private def drawRectangle(rawInts: Array[Int]
, blockColor: BlockColor
, x: Int
Expand All @@ -59,40 +65,53 @@ object LPixelBuffer extends CanLog {
val maxHeight = y + height - 1
val length = width - 1
val squareWidth = length - 1
// Calculate start and end indices for updating rawInts
val startIdx = y * canvasWidth + x
val endIdx = maxHeight * canvasWidth + x + length

// Check if start and end indices are within bounds
if (startIdx >= 0 && endIdx < rawInts.length) {
val color = blockColor.color
val upperBorderCol = blockColor.upperBorderCol
val bottomBorderCol = blockColor.bottomBorderCol
val leftBorderCol = blockColor.leftBorderCol
val rightBorderCol = blockColor.rightBorderCol

// Update rawInts directly without array copying
for (ly <- y until maxHeight - 1) {
var ly = y
while (ly < maxHeight) {
val startPos = ly * canvasWidth + x
// Fill the portion of rawInts with lineArray
for (i <- 0 to squareWidth) rawInts(startPos + i) = blockColor.color
var i = 0
while (i <= squareWidth) {
rawInts(startPos + i) = color
i += 1
}
ly += 1
}

// paint highlights & shadows if square is big enough
if ((width >= 2) && (height >= 2)) {
// upper left corner to upper right corner
for (i <- 0 to squareWidth) rawInts(y * canvasWidth + x + i) = blockColor.upperBorderCol
// lower left corner to lower right corner
for (i <- 0 to squareWidth) rawInts((maxHeight - 1) * canvasWidth + x + i) = blockColor.bottomBorderCol

// calculate x positions : starting from (y * canvasWidth + x) being the upper left corner,
// with a step size of canvasWidth we get the coordinates of the left border
for (ly <- y until maxHeight - 1) {
// left border
rawInts(ly * canvasWidth + x) = blockColor.leftBorderCol
// the same for the right border, but with another offset
rawInts(ly * canvasWidth + x + length) = blockColor.rightBorderCol
// Paint highlights & shadows if square is big enough
if (width >= 2 && height >= 2) {
// Paint upper border from upper left corner to upper right corner
var i = 0
while (i <= squareWidth) {
rawInts(y * canvasWidth + x + i) = upperBorderCol
rawInts(maxHeight * canvasWidth + x + i) = bottomBorderCol
i += 1
}

// Calculate x positions for left and right borders
ly = y
while (ly < maxHeight) {
val idx = ly * canvasWidth + x
rawInts(idx) = leftBorderCol
rawInts(idx + length) = rightBorderCol
ly += 1
}
}
} else {
// logWarn(s"tried to paint outside of allowed index. [endIdx = maxHeight * canvasWidth + x + length] ($endIdx = $maxHeight * $canvasWidth + $x + $length) ")
// logWarn(s"tried to paint outside of allowed index. [endIdx = maxHeight * canvasWidth + x + length] ($endIdx = $maxHeight * $canvasWidth + $x + $length) ")
}
}

}


Expand All @@ -111,8 +130,6 @@ case class LPixelBuffer(blockNumber: Int
, IntBuffer.wrap(rawInts)
, PixelFormat.getIntArgbPreInstance) with CanLog {

lazy val background: Array[Int] = Array.fill(shape.size)(LPixelBuffer.defaultBackgroundColor)

/* hardcoded highlight color */
private val highlightedColor = Color.YELLOW
private lazy val (yellow, yellowBright, yellowDark) = LPixelBuffer.calcColors(highlightedColor)
Expand All @@ -124,7 +141,7 @@ case class LPixelBuffer(blockNumber: Int
paint()
}

private def cleanBackground(): Unit = System.arraycopy(background, 0, rawInts, 0, background.length)
private def cleanBackground(): Unit = java.util.Arrays.fill(rawInts, LPixelBuffer.defaultBackgroundColor)

def getBlockSize: Int = blockSizeProperty.get()

Expand Down Expand Up @@ -174,7 +191,9 @@ case class LPixelBuffer(blockNumber: Int
case (true, true) => yellowVisible
}

rawInts(i) = col
if (i < rawInts.length) {
rawInts(i) = col
}

i = i + 1
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,4 @@ class ChunkImageSpec extends AnyWordSpec {
".e" in assert(ChunkImage.indexOf(99, 99, 10, 100) == 99)
}

"ChunkImage.calcVirtualHeight" should {
"sc0" in assert(ChunkImage.calcVirtualHeight(1, 7, 10, 0) == 0)
"sc1" in assert(ChunkImage.calcVirtualHeight(1, 7, 10, 1) == 7)
"sc10" in assert(ChunkImage.calcVirtualHeight(1, 7, 10, 10) == 7)
"sc11" in assert(ChunkImage.calcVirtualHeight(1, 7, 10, 11) == 14)
}
}
2 changes: 1 addition & 1 deletion app/src/test/scala/app/logorrr/views/block/ChunkSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ChunkSpec extends AnyWordSpec {
}
// default chunk size is 4
"test default chunk size" in {
val chunks: Seq[Chunk] = mkTestChunks(1000, 100 + ChunkImage.ScrollBarWidth, 10, 1000)
val chunks: Seq[Chunk] = mkTestChunks(1000, 100 + ChunkImage.getScrollBarWidth, 10, 1000)
assert(chunks.size == Chunk.ChunksPerVisibleViewPort)
assert(chunks.head.entries.size == 176)
assert(chunks(1).entries.size == 176)
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/run-benchmarks.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1 @@
mvn clean verify; java -jar target/benchmarks.jar
mvn install -f ../app/pom.xml;mvn clean verify; java -jar target/benchmarks.jar

0 comments on commit 2850607

Please sign in to comment.