UI clients: fix narrow vs wide UI
This commit is contained in:
@@ -3,7 +3,10 @@ package com.jaytux.phoebench.clients
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.jaytux.phoebench.clients.ui.RootView
|
import com.jaytux.phoebench.clients.ui.RootView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+4
-1
@@ -133,7 +133,10 @@ class ProjectVM(
|
|||||||
ProjectEvent.Deleted -> back()
|
ProjectEvent.Deleted -> back()
|
||||||
is ProjectEvent.EntryDeleted -> _benchmarks.value = _benchmarks.value.replace({ it.id == event.benchmarkId }) { it.remove(event.id) }
|
is ProjectEvent.EntryDeleted -> _benchmarks.value = _benchmarks.value.replace({ it.id == event.benchmarkId }) { it.remove(event.id) }
|
||||||
is ProjectEvent.VersionChanged -> _labels.value += (event.label.id to Version.fromResponse(event.label))
|
is ProjectEvent.VersionChanged -> _labels.value += (event.label.id to Version.fromResponse(event.label))
|
||||||
is ProjectEvent.VersionDeleted -> _labels.value = _labels.value.filter { it.value.id != event.id }
|
is ProjectEvent.VersionDeleted -> {
|
||||||
|
_labels.value = _labels.value.filter { it.value.id != event.id }
|
||||||
|
_benchmarks.value = _benchmarks.value.map { it.copy(entries = it.entries.filter { e -> e.label.id != event.id }) }
|
||||||
|
}
|
||||||
is ProjectEvent.NewEntry -> {
|
is ProjectEvent.NewEntry -> {
|
||||||
_benchmarks.value = _benchmarks.value.replace({ it.id == event.entry.benchmarkId }) { it.add(Entry.fromResponse(event.entry, _labels.value,
|
_benchmarks.value = _benchmarks.value.replace({ it.id == event.entry.benchmarkId }) { it.add(Entry.fromResponse(event.entry, _labels.value,
|
||||||
{ input -> _inputs.value += input }, { hw -> _hardware.value += hw })) }
|
{ input -> _inputs.value += input }, { hw -> _hardware.value += hw })) }
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
package com.jaytux.phoebench.clients.theme
|
package com.jaytux.phoebench.clients.theme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
val linkColor = Color(0xFF64B5F6)
|
val linkColor = Color(0xFF64B5F6)
|
||||||
|
|
||||||
|
enum class UiMode(val minWidth: Dp) {
|
||||||
|
MOBILE(0.dp),
|
||||||
|
NARROW(0.dp),
|
||||||
|
WIDE(2000.dp);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromDp(width: Dp) = when {
|
||||||
|
width < NARROW.minWidth -> MOBILE
|
||||||
|
width < WIDE.minWidth -> NARROW
|
||||||
|
else -> WIDE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val GlobalUiMode = compositionLocalOf { UiMode.MOBILE }
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
package com.jaytux.phoebench.clients.ui
|
||||||
|
|
||||||
|
import androidx.annotation.FloatRange
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.*
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.composables.icons.lucide.Lucide
|
||||||
|
import com.composables.icons.lucide.PanelRightClose
|
||||||
|
import com.composables.icons.lucide.PanelRightOpen
|
||||||
|
import com.jaytux.phoebench.clients.app
|
||||||
|
|
||||||
|
@DslMarker annotation class NarrowDslMarker
|
||||||
|
@DslMarker annotation class TabPanelDslMarker
|
||||||
|
|
||||||
|
@NarrowDslMarker
|
||||||
|
interface INarrowDsl {
|
||||||
|
fun mainContent(content: @Composable ((isNarrow: Boolean, toggleSide: () -> Unit) -> Unit))
|
||||||
|
fun sideContent(onSurface: Boolean, content: @Composable (isNarrow: Boolean, toggleSide: () -> Unit) -> Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NarrowDslImpl : INarrowDsl {
|
||||||
|
var main: @Composable (isNarrow: Boolean, toggleSide: () -> Unit) -> Unit = { _, _ -> }
|
||||||
|
private set
|
||||||
|
var side: @Composable (isNarrow: Boolean, toggleSide: () -> Unit) -> Unit = { _, _ -> }
|
||||||
|
private set
|
||||||
|
var sideSurface: Boolean = true
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun mainContent(content: @Composable ((isNarrow: Boolean, toggleSide: () -> Unit) -> Unit)) { main = content }
|
||||||
|
override fun sideContent(onSurface: Boolean, content: @Composable ((isNarrow: Boolean, toggleSide: () -> Unit) -> Unit)) { sideSurface = onSurface; side = content }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RowOrPanel(useRow: Boolean, modifier: Modifier = Modifier, @FloatRange(0.0, 1.0) defaultWeight: Float = 0.5f, builder: INarrowDsl.() -> Unit) {
|
||||||
|
val content = NarrowDslImpl().also { it.builder() }
|
||||||
|
|
||||||
|
if(!useRow) {
|
||||||
|
var isOpen by remember { mutableStateOf(false) }
|
||||||
|
Box(modifier) {
|
||||||
|
content.main(true) { isOpen = !isOpen }
|
||||||
|
if(isOpen) {
|
||||||
|
CompositionLocalProvider(LocalIndication provides NoFeedbackIndication()) {
|
||||||
|
Box(Modifier.matchParentSize().background(Color.Black.copy(alpha = 0.5f)).clickable { isOpen = false })
|
||||||
|
}
|
||||||
|
val shape = MaterialTheme.shapes.medium.copy(topEnd = CornerSize(0.dp), bottomEnd = CornerSize(0.dp))
|
||||||
|
Box(Modifier.matchParentSize()) {
|
||||||
|
if(content.sideSurface) {
|
||||||
|
Surface(Modifier.align(Alignment.CenterEnd).fillMaxHeight().fillMaxWidth(0.9f), shape = shape, shadowElevation = 3.dp) {
|
||||||
|
Box(Modifier.padding(10.dp)) {
|
||||||
|
content.side(true) { isOpen = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Box(Modifier.align(Alignment.CenterEnd)) {
|
||||||
|
content.side(true) { isOpen = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Row(modifier) {
|
||||||
|
Box(Modifier.weight(defaultWeight)) { content.main(false) {} }
|
||||||
|
Box(Modifier.weight(1.0f - defaultWeight)) { content.side(false) {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TabPanelShape(val tabWidth: Dp, val tabHeight: Dp = tabWidth, val cornerRadius: CornerSize) : Shape {
|
||||||
|
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
|
||||||
|
val (tabWpx, tabHpx, rpx) = with(density) {
|
||||||
|
tabWidth.toPx() to tabHeight.toPx() app cornerRadius.toPx(size, density)
|
||||||
|
}
|
||||||
|
|
||||||
|
val radius = CornerRadius(rpx, rpx)
|
||||||
|
|
||||||
|
val shape = Path().apply {
|
||||||
|
addRoundRect(RoundRect(
|
||||||
|
Rect(offset = Offset(x = tabWpx, y = 0f), size = Size(width = size.width - tabWpx, height = size.height)),
|
||||||
|
topLeft = CornerRadius.Zero, topRight = CornerRadius.Zero,
|
||||||
|
bottomLeft = radius, bottomRight = CornerRadius.Zero
|
||||||
|
))
|
||||||
|
|
||||||
|
addRoundRect(RoundRect(
|
||||||
|
Rect(offset = Offset(x = 0f, y = 0f), size = Size(size.width, tabHpx)),
|
||||||
|
topLeft = radius, topRight = CornerRadius.Zero,
|
||||||
|
bottomLeft = radius, bottomRight = CornerRadius.Zero
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return Outline.Generic(shape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TabPanel(tabWidth: Dp, tabHeight: Dp = tabWidth, modifier: Modifier = Modifier,
|
||||||
|
cornerRadius: CornerSize = MaterialTheme.shapes.medium.topEnd,
|
||||||
|
color: Color = MaterialTheme.colorScheme.surface, contentColor: Color = contentColorFor(color),
|
||||||
|
tonalElevation: Dp = 0.dp, shadowElevation: Dp = 0.dp, border: BorderStroke? = null,
|
||||||
|
tabContent: @Composable () -> Unit,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Surface(modifier,
|
||||||
|
shape = TabPanelShape(tabWidth, tabHeight, cornerRadius), color = color, contentColor = contentColor,
|
||||||
|
tonalElevation = tonalElevation, shadowElevation = shadowElevation, border = border
|
||||||
|
) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Box(Modifier.size(width = tabWidth, height = tabHeight), propagateMinConstraints = true) { tabContent() }
|
||||||
|
Box(Modifier.padding(start = tabWidth)) { content() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TabPanel(tabWidth: Dp, modifier: Modifier, tabHeight: Dp = tabWidth,
|
||||||
|
cornerRadius: CornerSize = MaterialTheme.shapes.medium.topEnd,
|
||||||
|
color: Color = MaterialTheme.colorScheme.surface, contentColor: Color = contentColorFor(color),
|
||||||
|
tonalElevation: Dp = 0.dp, shadowElevation: Dp = 0.dp, border: BorderStroke? = null,
|
||||||
|
tabContent: @Composable () -> Unit, content: @Composable () -> Unit) =
|
||||||
|
TabPanel(tabWidth, tabHeight, modifier, cornerRadius, color, contentColor, tonalElevation, shadowElevation, border, tabContent, content)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandableSidePanel(
|
||||||
|
modifier: Modifier = Modifier, tabWidth: Dp = 40.dp, tabHeight: Dp = tabWidth,
|
||||||
|
radius: CornerSize = MaterialTheme.shapes.medium.topEnd, elevation: Dp = 5.dp,
|
||||||
|
color: Color = MaterialTheme.colorScheme.surface, toggleSide: () -> Unit
|
||||||
|
) = TabPanel(tabWidth, tabHeight, modifier, radius, color, shadowElevation = elevation, tonalElevation = elevation,
|
||||||
|
tabContent = {
|
||||||
|
Surface(Modifier.padding(5.dp), shape = MaterialTheme.shapes.medium) {
|
||||||
|
Icon(Lucide.PanelRightOpen, "Open side panel",
|
||||||
|
Modifier.aspectRatio(1f).fillMaxSize(0.66f).clickable { toggleSide() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandedSidePanel(
|
||||||
|
modifier: Modifier = Modifier, tabWidth: Dp = 40.dp, tabHeight: Dp = tabWidth,
|
||||||
|
radius: CornerSize = MaterialTheme.shapes.medium.topEnd, elevation: Dp = 5.dp,
|
||||||
|
color: Color = MaterialTheme.colorScheme.surface, toggleSide: () -> Unit, content: @Composable () -> Unit
|
||||||
|
) = TabPanel(tabWidth, tabHeight, modifier, radius, color, shadowElevation = elevation, tonalElevation = elevation,
|
||||||
|
tabContent = {
|
||||||
|
Surface(Modifier.padding(5.dp), shape = MaterialTheme.shapes.medium) {
|
||||||
|
Icon(Lucide.PanelRightClose, "Close side panel",
|
||||||
|
Modifier.aspectRatio(1f).fillMaxSize(0.66f).clickable { toggleSide() })
|
||||||
|
}
|
||||||
|
}, content = content
|
||||||
|
)
|
||||||
+248
-119
@@ -20,6 +20,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -29,6 +30,9 @@ import com.jaytux.phoebench.clients.*
|
|||||||
import com.jaytux.phoebench.clients.data.ProjectVM
|
import com.jaytux.phoebench.clients.data.ProjectVM
|
||||||
import com.jaytux.phoebench.clients.data.mutableStateSetFrom
|
import com.jaytux.phoebench.clients.data.mutableStateSetFrom
|
||||||
import com.jaytux.phoebench.clients.data.mutableStateSetOf
|
import com.jaytux.phoebench.clients.data.mutableStateSetOf
|
||||||
|
import com.jaytux.phoebench.clients.theme.GlobalUiMode
|
||||||
|
import com.jaytux.phoebench.clients.theme.UiMode
|
||||||
|
import com.jaytux.phoebench.clients.theme.linkColor
|
||||||
import com.jaytux.phoebench.common.*
|
import com.jaytux.phoebench.common.*
|
||||||
import com.kborowy.colorpicker.KolorPicker
|
import com.kborowy.colorpicker.KolorPicker
|
||||||
import io.github.koalaplot.core.bar.DefaultBar
|
import io.github.koalaplot.core.bar.DefaultBar
|
||||||
@@ -150,7 +154,7 @@ fun ProjectPanel(vm: ProjectVM, editable: Boolean, versions: Map<Uuid, ProjectVM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(Modifier.fillMaxSize().padding(15.dp)) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
ProjectPlotArea(vm, currentBenchmark)
|
ProjectPlotArea(vm, currentBenchmark)
|
||||||
|
|
||||||
if(editable) {
|
if(editable) {
|
||||||
@@ -160,7 +164,7 @@ fun ProjectPanel(vm: ProjectVM, editable: Boolean, versions: Map<Uuid, ProjectVM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.align(Alignment.BottomEnd).padding(25.dp), horizontalAlignment = Alignment.End) {
|
Column(Modifier.align(Alignment.BottomEnd).padding(40.dp), horizontalAlignment = Alignment.End) {
|
||||||
if (addOpen) {
|
if (addOpen) {
|
||||||
var lblWidth by mutableStateOf(1000.dp)
|
var lblWidth by mutableStateOf(1000.dp)
|
||||||
var dataWidth by mutableStateOf(1000.dp)
|
var dataWidth by mutableStateOf(1000.dp)
|
||||||
@@ -349,13 +353,7 @@ fun QuickVersion(lbl: ProjectVM.Version, hollow: Boolean = false) {
|
|||||||
}
|
}
|
||||||
Spacer(Modifier.width(15.dp))
|
Spacer(Modifier.width(15.dp))
|
||||||
Column {
|
Column {
|
||||||
Row {
|
Text(lbl.name, overflow = TextOverflow.Ellipsis, maxLines = 1)
|
||||||
Text(lbl.name)
|
|
||||||
Text(" ${lbl.colorStr}", Modifier.align(Alignment.Bottom),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = LocalContentColor.current.copy(alpha = 0.75f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(lbl.meta.isNotBlank())
|
if(lbl.meta.isNotBlank())
|
||||||
Text(lbl.meta, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall,
|
Text(lbl.meta, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall,
|
||||||
color = LocalContentColor.current.copy(0.75f))
|
color = LocalContentColor.current.copy(0.75f))
|
||||||
@@ -536,7 +534,9 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
val allInputs by vm.inputs
|
val allInputs by vm.inputs
|
||||||
val allHardware by vm.hardware
|
val allHardware by vm.hardware
|
||||||
|
|
||||||
val visibleInputs by remember(entries, allInputs) { mutableStateOf(allInputs intersect entries.map { it.input }.toSet()) }
|
val visibleInputs by remember(entries, allInputs) {
|
||||||
|
mutableStateOf(allInputs intersect entries.map { it.input }.toSet())
|
||||||
|
}
|
||||||
|
|
||||||
var filteredEntries by remember { mutableStateOf(entries) }
|
var filteredEntries by remember { mutableStateOf(entries) }
|
||||||
var timeMin by remember { mutableStateOf(Instant.DISTANT_PAST) }
|
var timeMin by remember { mutableStateOf(Instant.DISTANT_PAST) }
|
||||||
@@ -552,16 +552,26 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
val enabledHardware = remember(allHardware) { mutableStateSetOf<String>() }
|
val enabledHardware = remember(allHardware) { mutableStateSetOf<String>() }
|
||||||
val enabledInputs = remember(allInputs) { mutableStateSetOf<String>() }
|
val enabledInputs = remember(allInputs) { mutableStateSetOf<String>() }
|
||||||
|
|
||||||
LaunchedEffect(entries, allInputs, allHardware, timeFilter, timeMin, timeMax, versionFilterRev, inputFilterRev, filterHardware) {
|
LaunchedEffect(
|
||||||
|
entries,
|
||||||
|
allInputs,
|
||||||
|
allHardware,
|
||||||
|
timeFilter,
|
||||||
|
timeMin,
|
||||||
|
timeMax,
|
||||||
|
versionFilterRev,
|
||||||
|
inputFilterRev,
|
||||||
|
filterHardware
|
||||||
|
) {
|
||||||
val timeRange = dualLerp(timeMin, timeMax, timeFilter.start, timeFilter.endInclusive)
|
val timeRange = dualLerp(timeMin, timeMax, timeFilter.start, timeFilter.endInclusive)
|
||||||
enabledHardware.clear()
|
enabledHardware.clear()
|
||||||
enabledInputs.clear()
|
enabledInputs.clear()
|
||||||
|
|
||||||
filteredEntries = entries.filter { entry ->
|
filteredEntries = entries.filter { entry ->
|
||||||
if(entry.label !in versionFilter) return@filter false
|
if (entry.label !in versionFilter) return@filter false
|
||||||
if(!(entry.timeStamp inRange timeRange)) return@filter false
|
if (!(entry.timeStamp inRange timeRange)) return@filter false
|
||||||
if(entry.input !in inputFilter) return@filter false
|
if (entry.input !in inputFilter) return@filter false
|
||||||
if(filterHardware != null && entry.hardware != filterHardware) return@filter false
|
if (filterHardware != null && entry.hardware != filterHardware) return@filter false
|
||||||
|
|
||||||
enabledHardware.add(entry.hardware)
|
enabledHardware.add(entry.hardware)
|
||||||
enabledInputs.add(entry.input)
|
enabledInputs.add(entry.input)
|
||||||
@@ -582,7 +592,7 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
var maxY = 0f
|
var maxY = 0f
|
||||||
|
|
||||||
renderableEntries = filteredEntries.map { entry ->
|
renderableEntries = filteredEntries.map { entry ->
|
||||||
val use = if(displayWarmups) entry.warmups else entry.measurements
|
val use = if (displayWarmups) entry.warmups else entry.measurements
|
||||||
maxX = maxOf(maxX, use.size.toFloat())
|
maxX = maxOf(maxX, use.size.toFloat())
|
||||||
use.mapIndexed { idx, it ->
|
use.mapIndexed { idx, it ->
|
||||||
val converted = entry.nativeUnit.convertTo(currentUnit, it)
|
val converted = entry.nativeUnit.convertTo(currentUnit, it)
|
||||||
@@ -617,15 +627,23 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
var max = 0.0f
|
var max = 0.0f
|
||||||
baselineComparison = baseline?.let { bl ->
|
baselineComparison = baseline?.let { bl ->
|
||||||
class MeanAccumulator(var sum: Float = 0.0f, var count: Float = 0.0f) {
|
class MeanAccumulator(var sum: Float = 0.0f, var count: Float = 0.0f) {
|
||||||
fun add(f: Float) { sum += f; count += 1.0f }
|
fun add(f: Float) {
|
||||||
fun mean() = if(count == 0.0f) 0.0f else sum / count
|
sum += f; count += 1.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun add(f: Float, countsAs: Int) {
|
||||||
|
sum += f; count += countsAs.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mean() = if (count == 0.0f) 0.0f else (sum / count)
|
||||||
|
}
|
||||||
|
|
||||||
val perVersion = mutableMapOf<ProjectVM.Version, MutableMap<String, MeanAccumulator>>()
|
val perVersion = mutableMapOf<ProjectVM.Version, MutableMap<String, MeanAccumulator>>()
|
||||||
filteredEntries.forEach { entry ->
|
filteredEntries.forEach { entry ->
|
||||||
val corrected = entry.measurements.map { entry.nativeUnit.convertTo(currentUnit, it) }.sum()
|
val corrected = entry.measurements.map { entry.nativeUnit.convertTo(currentUnit, it) }.sum()
|
||||||
perVersion.getOrPut(entry.label) { mutableMapOf() }
|
perVersion.getOrPut(entry.label) { mutableMapOf() }
|
||||||
.getOrPut(entry.input) { MeanAccumulator() }
|
.getOrPut(entry.input) { MeanAccumulator() }
|
||||||
.add(corrected)
|
.add(corrected, entry.measurements.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
val subMeans = perVersion.mapValues { (_, v) -> v.mapValues { (_, acc) -> acc.mean() } }
|
val subMeans = perVersion.mapValues { (_, v) -> v.mapValues { (_, acc) -> acc.mean() } }
|
||||||
@@ -656,18 +674,33 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
verticalMinorStyle = KoalaPlotTheme.axis.minorGridlineStyle?.copy(brush = lineColor)
|
verticalMinorStyle = KoalaPlotTheme.axis.minorGridlineStyle?.copy(brush = lineColor)
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(Modifier.fillMaxSize().padding(20.dp)) {
|
val uiMode = GlobalUiMode.current
|
||||||
Row(Modifier.weight(0.66f).fillMaxHeight()) {
|
var showOptions by remember(uiMode) { mutableStateOf(uiMode == UiMode.WIDE) }
|
||||||
if(entries.isEmpty()) {
|
|
||||||
Box(Modifier.fillMaxWidth().fillMaxHeight(0.25f)) {
|
RowOrPanel(useRow = GlobalUiMode.current == UiMode.WIDE, Modifier.fillMaxSize()) {
|
||||||
|
mainContent { isNarrow, toggleSide ->
|
||||||
|
Box {
|
||||||
|
Row(Modifier.padding(end = if (isNarrow) 25.dp else 0.dp)) {
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
Box(Modifier.fillMaxWidth().fillMaxHeight(0.25f).padding(35.dp)) {
|
||||||
Text("No data yet.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic)
|
Text("No data yet.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
Column(Modifier.padding(35.dp)) {
|
||||||
Column {
|
|
||||||
Text("Measurements", style = MaterialTheme.typography.headlineSmall)
|
|
||||||
Row {
|
Row {
|
||||||
Row(Modifier.weight(0.66f), verticalAlignment = Alignment.CenterVertically) {
|
Text("Measurements", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
Text(
|
||||||
|
if (showOptions) "(hide options)" else "(show options)",
|
||||||
|
Modifier.clickable { showOptions = !showOptions }.align(Alignment.Bottom),
|
||||||
|
color = linkColor,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showOptions) {
|
||||||
|
Column {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("View as: ", fontWeight = FontWeight.Bold)
|
Text("View as: ", fontWeight = FontWeight.Bold)
|
||||||
Spacer(Modifier.width(5.dp))
|
Spacer(Modifier.width(5.dp))
|
||||||
SingleChoiceSegmentedButtonRow(Modifier.weight(1f)) {
|
SingleChoiceSegmentedButtonRow(Modifier.weight(1f)) {
|
||||||
@@ -675,45 +708,56 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
currentUnit == it,
|
currentUnit == it,
|
||||||
{ currentUnit = it },
|
{ currentUnit = it },
|
||||||
shape = SegmentedButtonDefaults.itemShape(idx, TimeUnit.entries.size)
|
shape = SegmentedButtonDefaults.itemShape(
|
||||||
|
idx,
|
||||||
|
TimeUnit.entries.size
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(it.disp)
|
Text(it.disp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
Spacer(Modifier.width(50.dp))
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
|
||||||
Row(Modifier.weight(0.33f), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text("Display: ", fontWeight = FontWeight.Bold)
|
Text("Display: ", fontWeight = FontWeight.Bold)
|
||||||
Spacer(Modifier.width(5.dp))
|
Spacer(Modifier.width(5.dp))
|
||||||
SingleChoiceSegmentedButtonRow(Modifier.weight(1f)) {
|
SingleChoiceSegmentedButtonRow(Modifier.weight(1f)) {
|
||||||
SegmentedButton(!displayWarmups, { displayWarmups = false },
|
SegmentedButton(
|
||||||
|
!displayWarmups, { displayWarmups = false },
|
||||||
shape = SegmentedButtonDefaults.itemShape(0, 2)
|
shape = SegmentedButtonDefaults.itemShape(0, 2)
|
||||||
) {
|
) {
|
||||||
Text("(Steady-state) measurements")
|
Text("(Steady-state) measurements")
|
||||||
}
|
}
|
||||||
|
|
||||||
SegmentedButton(displayWarmups, { displayWarmups = true },
|
SegmentedButton(
|
||||||
|
displayWarmups, { displayWarmups = true },
|
||||||
shape = SegmentedButtonDefaults.itemShape(1, 2)
|
shape = SegmentedButtonDefaults.itemShape(1, 2)
|
||||||
) {
|
) {
|
||||||
Text("Warmup measurements")
|
Text("Warmup measurements")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Spacer(Modifier.height(5.dp))
|
Spacer(Modifier.height(5.dp))
|
||||||
Column(Modifier.fillMaxWidth()) {
|
Column(Modifier.fillMaxWidth()) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Filter by timestamp: ", fontWeight = FontWeight.Bold)
|
Text("Filter by timestamp: ", fontWeight = FontWeight.Bold)
|
||||||
Spacer(Modifier.width(5.dp))
|
Spacer(Modifier.width(5.dp))
|
||||||
RangeSlider(timeFilter, { timeFilter = it }, Modifier.fillMaxWidth(), valueRange = 0f..1f)
|
RangeSlider(
|
||||||
|
timeFilter,
|
||||||
|
{ timeFilter = it },
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
valueRange = 0f..1f
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Text(timeFilterString, Modifier.align(Alignment.End))
|
Text(timeFilterString, Modifier.align(Alignment.End))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(15.dp))
|
Spacer(Modifier.height(15.dp))
|
||||||
|
|
||||||
|
if (!isNarrow || baselineComparison == null) {
|
||||||
XYGraph(
|
XYGraph(
|
||||||
rememberFloatLinearAxisModel(listOf(0f, xMax).autoScaleRange()),
|
rememberFloatLinearAxisModel(listOf(0f, xMax).autoScaleRange()),
|
||||||
rememberFloatLinearAxisModel(listOf(yMin, yMax).autoScaleRange()),
|
rememberFloatLinearAxisModel(listOf(yMin, yMax).autoScaleRange()),
|
||||||
@@ -735,35 +779,59 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else baselineComparison?.let { blc ->
|
||||||
|
baselineGraph(baseline, baselineMax, style, gridStyle, blc, currentUnit)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.width(20.dp))
|
if (isNarrow) {
|
||||||
|
ExpandableSidePanel(
|
||||||
|
Modifier.width(70.dp).fillMaxHeight().align(Alignment.CenterEnd),
|
||||||
|
toggleSide = toggleSide, elevation = 10.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(Modifier.weight(0.33f).fillMaxHeight()) {
|
sideContent(false) { isNarrow, toggleSide ->
|
||||||
Row {
|
val wrapper: @Composable (@Composable () -> Unit) -> Unit = if (isNarrow) { it ->
|
||||||
|
ExpandedSidePanel(Modifier.fillMaxWidth(0.8f), toggleSide = toggleSide) {
|
||||||
|
Box(Modifier.padding(10.dp)) { it() }
|
||||||
|
}
|
||||||
|
} else { it -> it() }
|
||||||
|
|
||||||
|
wrapper {
|
||||||
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
Row(Modifier.weight(0.33f)) {
|
||||||
Column(Modifier.weight(0.5f)) {
|
Column(Modifier.weight(0.5f)) {
|
||||||
Text("Versions", style = MaterialTheme.typography.headlineSmall)
|
Text("Versions", style = MaterialTheme.typography.headlineSmall)
|
||||||
Text("Click a version to toggle its visibility.", fontStyle = FontStyle.Italic,
|
Text(
|
||||||
color = LocalContentColor.current.copy(alpha = 0.5f), style = MaterialTheme.typography.bodySmall)
|
"Click a version to toggle its visibility.",
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = LocalContentColor.current.copy(alpha = 0.5f),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
if(versions.isEmpty()) {
|
if (versions.isEmpty()) {
|
||||||
Box(Modifier.fillMaxWidth().fillMaxHeight(0.25f)) {
|
Box(Modifier.fillMaxWidth().fillMaxHeight(0.25f)) {
|
||||||
Text("No versions yet.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic)
|
Text(
|
||||||
|
"No versions yet.",
|
||||||
|
Modifier.align(Alignment.Center),
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
LazyColumn(Modifier.padding(start = 5.dp)) {
|
LazyColumn(Modifier.padding(start = 5.dp)) {
|
||||||
items(versions.toList().sortedBy { it.second.name }) { (_, lbl) ->
|
items(versions.toList().sortedBy { it.second.name }) { (_, lbl) ->
|
||||||
Box(Modifier.fillMaxWidth().clickable {
|
Box(Modifier.fillMaxWidth().clickable {
|
||||||
baseline = null
|
baseline = null
|
||||||
if(versionFilter.size == versions.size) {
|
if (versionFilter.size == versions.size) {
|
||||||
versionFilter.clear()
|
versionFilter.clear()
|
||||||
versionFilter.add(lbl)
|
versionFilter.add(lbl)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
versionFilter.toggle(lbl)
|
versionFilter.toggle(lbl)
|
||||||
if(versionFilter.isEmpty()) versionFilter.addAll(versions.values)
|
if (versionFilter.isEmpty()) versionFilter.addAll(versions.values)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Row {
|
Row {
|
||||||
@@ -780,23 +848,39 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(editable) {
|
if (editable) {
|
||||||
IconButton({ editingVersion = lbl }, Modifier.align(Alignment.CenterVertically)) {
|
IconButton(
|
||||||
|
{ editingVersion = lbl },
|
||||||
|
Modifier.align(Alignment.CenterVertically)
|
||||||
|
) {
|
||||||
Icon(Lucide.Pencil, "Edit version")
|
Icon(Lucide.Pencil, "Edit version")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton({ baseline = if(baseline == lbl) null else lbl },
|
IconButton(
|
||||||
Modifier.align(Alignment.CenterVertically), enabled = lbl in versionFilter) {
|
{ baseline = if (baseline == lbl) null else lbl },
|
||||||
if(baseline == lbl) {
|
Modifier.align(Alignment.CenterVertically),
|
||||||
Surface(Modifier.padding(5.dp), color = MaterialTheme.colorScheme.inverseSurface, shape = MaterialTheme.shapes.large) {
|
enabled = lbl in versionFilter
|
||||||
Icon(Lucide.RulerDimensionLine, "Use as baseline", Modifier.padding(5.dp))
|
) {
|
||||||
|
if (baseline == lbl) {
|
||||||
|
Surface(
|
||||||
|
Modifier.padding(5.dp),
|
||||||
|
color = MaterialTheme.colorScheme.inverseSurface,
|
||||||
|
shape = MaterialTheme.shapes.large
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Lucide.RulerDimensionLine,
|
||||||
|
"Use as baseline",
|
||||||
|
Modifier.padding(5.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
} else Icon(Lucide.RulerDimensionLine, "Use as baseline")
|
||||||
}
|
}
|
||||||
else Icon(Lucide.RulerDimensionLine, "Use as baseline")
|
if (editable) {
|
||||||
}
|
IconButton(
|
||||||
if(editable) {
|
{ deletingVersion = lbl },
|
||||||
IconButton({ deletingVersion = lbl }, Modifier.align(Alignment.CenterVertically)) {
|
Modifier.align(Alignment.CenterVertically)
|
||||||
|
) {
|
||||||
Icon(Lucide.Trash, "Delete version")
|
Icon(Lucide.Trash, "Delete version")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -810,44 +894,57 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
LazyColumn(Modifier.weight(0.5f)) {
|
LazyColumn(Modifier.weight(0.5f)) {
|
||||||
item { Text("Inputs", style = MaterialTheme.typography.headlineSmall) }
|
item { Text("Inputs", style = MaterialTheme.typography.headlineSmall) }
|
||||||
item {
|
item {
|
||||||
Text("Click an input to set or clear the filter.", fontStyle = FontStyle.Italic,
|
Text(
|
||||||
color = LocalContentColor.current.copy(0.5f), style = MaterialTheme.typography.bodySmall)
|
"Click an input to set or clear the filter.",
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = LocalContentColor.current.copy(0.5f),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(10.dp)) }
|
item { Spacer(Modifier.height(10.dp)) }
|
||||||
|
|
||||||
items((visibleInputs intersect inputFilter.toSet()).sorted()) {
|
items((visibleInputs intersect inputFilter.toSet()).sorted()) {
|
||||||
Row(Modifier.fillMaxWidth().clickable {
|
Row(Modifier.fillMaxWidth().clickable {
|
||||||
if(inputFilter.size == allInputs.size) {
|
if (inputFilter.size == allInputs.size) {
|
||||||
inputFilter.clear()
|
inputFilter.clear()
|
||||||
inputFilter.add(it)
|
inputFilter.add(it)
|
||||||
}
|
} else if (inputFilter.size == 1) {
|
||||||
else if(inputFilter.size == 1) {
|
|
||||||
inputFilter.addAll(allInputs.toSet()); inputFilter.remove(it)
|
inputFilter.addAll(allInputs.toSet()); inputFilter.remove(it)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
inputFilter.toggle(it)
|
inputFilter.toggle(it)
|
||||||
if(inputFilter.isEmpty()) inputFilter.addAll(allInputs.toSet())
|
if (inputFilter.isEmpty()) inputFilter.addAll(allInputs.toSet())
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Text(it, Modifier.padding(start = 5.dp), color = LocalContentColor.current.copy(alpha = if(it in enabledInputs) 1.0f else 0.9f))
|
Text(
|
||||||
if(it !in enabledInputs) Text(" (no data)", Modifier.align(Alignment.Bottom),
|
it,
|
||||||
style = MaterialTheme.typography.bodySmall, color = LocalContentColor.current.copy(0.9f))
|
Modifier.padding(start = 5.dp),
|
||||||
|
color = LocalContentColor.current.copy(alpha = if (it in enabledInputs) 1.0f else 0.9f)
|
||||||
|
)
|
||||||
|
if (it !in enabledInputs) Text(
|
||||||
|
" (no data)",
|
||||||
|
Modifier.align(Alignment.Bottom),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = LocalContentColor.current.copy(0.9f)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items((visibleInputs - inputFilter.toSet()).sorted()) {
|
items((visibleInputs - inputFilter.toSet()).sorted()) {
|
||||||
Row(Modifier.fillMaxWidth().clickable {
|
Row(Modifier.fillMaxWidth().clickable {
|
||||||
if(inputFilter.size == allInputs.size) {
|
if (inputFilter.size == allInputs.size) {
|
||||||
inputFilter.clear()
|
inputFilter.clear()
|
||||||
inputFilter.add(it)
|
inputFilter.add(it)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
inputFilter.toggle(it)
|
inputFilter.toggle(it)
|
||||||
if(inputFilter.isEmpty()) inputFilter.addAll(allInputs.toSet())
|
if (inputFilter.isEmpty()) inputFilter.addAll(allInputs.toSet())
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Text(it, Modifier.padding(start = 5.dp), color = LocalContentColor.current.copy(0.66f))
|
Text(
|
||||||
|
it,
|
||||||
|
Modifier.padding(start = 5.dp),
|
||||||
|
color = LocalContentColor.current.copy(0.66f)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,59 +954,37 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
|
|
||||||
item { Text("Hardware", style = MaterialTheme.typography.headlineSmall) }
|
item { Text("Hardware", style = MaterialTheme.typography.headlineSmall) }
|
||||||
item {
|
item {
|
||||||
Text("Click an item to set or clear the filter.", fontStyle = FontStyle.Italic,
|
Text(
|
||||||
color = LocalContentColor.current.copy(0.5f), style = MaterialTheme.typography.bodySmall)
|
"Click an item to set or clear the filter.",
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = LocalContentColor.current.copy(0.5f),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(10.dp)) }
|
item { Spacer(Modifier.height(10.dp)) }
|
||||||
|
|
||||||
items((allHardware intersect enabledHardware.toSet()).toList()) {
|
items((allHardware intersect enabledHardware.toSet()).toList()) {
|
||||||
Row(Modifier.fillMaxWidth().clickable { filterHardware = if(filterHardware == null) it else null }) {
|
Row(
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.clickable { filterHardware = if (filterHardware == null) it else null }) {
|
||||||
Text(it, Modifier.padding(start = 5.dp))
|
Text(it, Modifier.padding(start = 5.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items((allHardware - enabledHardware.toSet()).toList()) {
|
items((allHardware - enabledHardware.toSet()).toList()) {
|
||||||
Text(it, Modifier.padding(start = 5.dp, top = 2.dp, bottom = 2.dp), color = LocalContentColor.current.copy(0.66f))
|
Text(
|
||||||
}
|
it,
|
||||||
}
|
Modifier.padding(start = 5.dp, top = 2.dp, bottom = 2.dp),
|
||||||
}
|
color = LocalContentColor.current.copy(0.66f)
|
||||||
|
|
||||||
Column(Modifier.weight(1f)) {
|
|
||||||
baselineComparison?.let { blc ->
|
|
||||||
Spacer(Modifier.height(10.dp))
|
|
||||||
HorizontalDivider(Modifier.height(1.dp))
|
|
||||||
Spacer(Modifier.height(10.dp))
|
|
||||||
Text("Speedup compared to ${baseline?.name ?: "<unknown version>"}", style = MaterialTheme.typography.headlineSmall)
|
|
||||||
Spacer(Modifier.height(10.dp))
|
|
||||||
|
|
||||||
val (xs, ys, colors) = blc.unzip()
|
|
||||||
|
|
||||||
XYGraph(
|
|
||||||
xAxisModel = remember(blc) { CategoryAxisModel(xs) },
|
|
||||||
yAxisModel = rememberFloatLinearAxisModel(0f..baselineMax),
|
|
||||||
xAxisContent = AxisContent(
|
|
||||||
labels = { AxisLabel(it) },
|
|
||||||
title = {},
|
|
||||||
style = style
|
|
||||||
),
|
|
||||||
yAxisContent = AxisContent(
|
|
||||||
labels = { AxisLabel(format(it)) },
|
|
||||||
title = {},
|
|
||||||
style = style
|
|
||||||
),
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
gridStyle = gridStyle
|
|
||||||
) {
|
|
||||||
VerticalBarPlot(
|
|
||||||
xData = xs,
|
|
||||||
yData = ys,
|
|
||||||
bar = { x, _, _ -> DefaultBar(brush = SolidColor(colors[x]), modifier = Modifier.fillMaxWidth()) }
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
blc.forEach { (x, y, _, time) ->
|
if (!isNarrow) {
|
||||||
XYAnnotation(Point(x, y), AnchorPoint.TopCenter) {
|
baselineComparison?.let { blc ->
|
||||||
Text("${format(y)}x\n${format(time)} ${currentUnit.disp}", Modifier.padding(top = 5.dp))
|
baselineGraph(baseline, baselineMax, style, gridStyle, blc, currentUnit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -932,6 +1007,60 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.baselineGraph(baseline: ProjectVM.Version?, baselineMax: Float, style: AxisStyle, gridStyle: GridStyle,
|
||||||
|
blc: List<Tuple4<String, Float, Color, Float>>, currentUnit: TimeUnit) {
|
||||||
|
Column(Modifier.weight(0.66f)) {
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
HorizontalDivider(Modifier.height(1.dp))
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
Text(
|
||||||
|
"Speedup compared to ${baseline?.name ?: "<unknown version>"}",
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|
||||||
|
val (xs, ys, colors) = blc.unzip()
|
||||||
|
|
||||||
|
XYGraph(
|
||||||
|
xAxisModel = remember(blc) { CategoryAxisModel(xs) },
|
||||||
|
yAxisModel = rememberFloatLinearAxisModel(0f..baselineMax),
|
||||||
|
xAxisContent = AxisContent(
|
||||||
|
labels = { AxisLabel(it) },
|
||||||
|
title = {},
|
||||||
|
style = style
|
||||||
|
),
|
||||||
|
yAxisContent = AxisContent(
|
||||||
|
labels = { AxisLabel(format(it)) },
|
||||||
|
title = {},
|
||||||
|
style = style
|
||||||
|
),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
gridStyle = gridStyle
|
||||||
|
) {
|
||||||
|
VerticalBarPlot(
|
||||||
|
xData = xs,
|
||||||
|
yData = ys,
|
||||||
|
bar = { x, _, _ ->
|
||||||
|
DefaultBar(
|
||||||
|
brush = SolidColor(colors[x]),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
blc.forEach { (x, y, _, time) ->
|
||||||
|
XYAnnotation(Point(x, y), AnchorPoint.TopCenter) {
|
||||||
|
Text(
|
||||||
|
"${format(y)}x\n${format(time)} ${currentUnit.disp}",
|
||||||
|
Modifier.padding(top = 5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfirmDeleteVersionDialog(version: ProjectVM.Version, onCancel: () -> Unit, onDelete: () -> Unit) {
|
fun ConfirmDeleteVersionDialog(version: ProjectVM.Version, onCancel: () -> Unit, onDelete: () -> Unit) {
|
||||||
Dialog(onDismissRequest = onCancel) {
|
Dialog(onDismissRequest = onCancel) {
|
||||||
@@ -939,7 +1068,7 @@ fun ConfirmDeleteVersionDialog(version: ProjectVM.Version, onCancel: () -> Unit,
|
|||||||
Column(Modifier.padding(8.dp).widthIn(min = 250.dp).width(IntrinsicSize.Min)) {
|
Column(Modifier.padding(8.dp).widthIn(min = 250.dp).width(IntrinsicSize.Min)) {
|
||||||
Text("Confirm deletion", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
Text("Confirm deletion", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
Text("Are you sure you want to delete $version?")
|
Text("Are you sure you want to delete ${version.name}?")
|
||||||
Text("This will also remove all data associated with this version.")
|
Text("This will also remove all data associated with this version.")
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
CancelConfirmXRow(onCancel, onDelete, confirmText = "Delete")
|
CancelConfirmXRow(onCancel, onDelete, confirmText = "Delete")
|
||||||
|
|||||||
@@ -1,10 +1,69 @@
|
|||||||
package com.jaytux.phoebench.clients
|
package com.jaytux.phoebench.clients
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.awt.SwingPanel
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.WindowPlacement
|
||||||
|
import androidx.compose.ui.window.WindowPosition
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import com.jaytux.phoebench.clients.theme.GlobalUiMode
|
||||||
|
import com.jaytux.phoebench.clients.theme.UiMode
|
||||||
|
import java.awt.GraphicsConfiguration
|
||||||
|
import java.awt.GraphicsEnvironment
|
||||||
|
import java.awt.Toolkit
|
||||||
|
import javax.swing.JPanel
|
||||||
|
|
||||||
|
fun windowData(config: GraphicsConfiguration): Pair<DpSize, DpOffset> {
|
||||||
|
val bounds = config.bounds
|
||||||
|
val insets = Toolkit.getDefaultToolkit().getScreenInsets(config)
|
||||||
|
|
||||||
|
val availableWidthPx = bounds.width - insets.left - insets.right
|
||||||
|
val availableHeightPx = bounds.height - insets.top - insets.bottom
|
||||||
|
|
||||||
|
val density = config.defaultTransform.scaleX.toFloat()
|
||||||
|
val size = DpSize((availableWidthPx / density).dp, (availableHeightPx / density).dp)
|
||||||
|
val offset = DpOffset((insets.left / density).dp, (insets.top / density).dp)
|
||||||
|
return size to offset
|
||||||
|
}
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
Window(onCloseRequest = ::exitApplication, title = "PhoeBench") {
|
val (winSize, winOffset) = windowData(GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration)
|
||||||
|
val state = rememberWindowState(placement = WindowPlacement.Maximized, size = winSize, position = WindowPosition.Aligned(Alignment.Center))
|
||||||
|
|
||||||
|
var mode by remember { mutableStateOf(UiMode.NARROW) }
|
||||||
|
|
||||||
|
Window(onCloseRequest = ::exitApplication, state, title = "PhoeBench") {
|
||||||
|
Column(Modifier.fillMaxSize()) {
|
||||||
|
SwingPanel(modifier = Modifier.size(0.dp), factory = {
|
||||||
|
JPanel().apply {
|
||||||
|
addPropertyChangeListener("graphicsConfiguration") {
|
||||||
|
val (newSize, newOffset) = windowData(this@Window.window.graphicsConfiguration)
|
||||||
|
state.size = newSize
|
||||||
|
state.position = WindowPosition.Aligned(Alignment.Center)
|
||||||
|
mode = UiMode.fromDp(state.size.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
CompositionLocalProvider(GlobalUiMode provides mode) {
|
||||||
App()
|
App()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -65,7 +65,7 @@ actual fun platformClient(builder: HttpClientConfig<*>.() -> Unit): HttpClient =
|
|||||||
actual suspend fun String.toClipEntry(): ClipEntry =
|
actual suspend fun String.toClipEntry(): ClipEntry =
|
||||||
ClipEntry(StringSelection(this))
|
ClipEntry(StringSelection(this))
|
||||||
|
|
||||||
actual fun format(f: Float): String = String.format("%.03f", f)
|
actual fun format(f: Float): String = String.format("%.03f", f).let { it.substring(0, minOf(it.length, 5)) }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun ServerSelect(server: String, onUpdate: (String) -> Unit) = DefaultServerSelect(server, onUpdate)
|
actual fun ServerSelect(server: String, onUpdate: (String) -> Unit) = DefaultServerSelect(server, onUpdate)
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ actual suspend fun String.toClipEntry(): ClipEntry =
|
|||||||
@JsFun("(num) => num.toFixed(3)")
|
@JsFun("(num) => num.toFixed(3)")
|
||||||
private external fun jsFormat(num: Double): String
|
private external fun jsFormat(num: Double): String
|
||||||
|
|
||||||
actual fun format(f: Float): String = jsFormat(f.toDouble())
|
actual fun format(f: Float): String = jsFormat(f.toDouble()).let { it.substring(0, minOf(it.length, 5)) }
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Config(val server: String)
|
data class Config(val server: String)
|
||||||
|
|||||||
Reference in New Issue
Block a user