Compare commits
2
Commits
1d10802140
...
8da391b6ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8da391b6ce
|
||
|
|
bc9861900e
|
@@ -13,7 +13,7 @@ plugins {
|
||||
alias(libs.plugins.buildconfig)
|
||||
}
|
||||
|
||||
version = PhoebenchVersion(1, 1, 1)
|
||||
version = PhoebenchVersion(1, 1, 2)
|
||||
|
||||
if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion))
|
||||
throw GradleException("UI Clients version must be at least as high as protocol/common version")
|
||||
|
||||
@@ -3,7 +3,10 @@ package com.jaytux.phoebench.clients
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jaytux.phoebench.clients.ui.RootView
|
||||
|
||||
@Composable
|
||||
|
||||
+4
-1
@@ -133,7 +133,10 @@ class ProjectVM(
|
||||
ProjectEvent.Deleted -> back()
|
||||
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.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 -> {
|
||||
_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 })) }
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
package com.jaytux.phoebench.clients.theme
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
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
|
||||
)
|
||||
+232
-103
@@ -20,6 +20,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
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.unit.dp
|
||||
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.mutableStateSetFrom
|
||||
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.kborowy.colorpicker.KolorPicker
|
||||
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)
|
||||
|
||||
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) {
|
||||
var lblWidth 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))
|
||||
Column {
|
||||
Row {
|
||||
Text(lbl.name)
|
||||
Text(" ${lbl.colorStr}", Modifier.align(Alignment.Bottom),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = LocalContentColor.current.copy(alpha = 0.75f),
|
||||
)
|
||||
}
|
||||
Text(lbl.name, overflow = TextOverflow.Ellipsis, maxLines = 1)
|
||||
if(lbl.meta.isNotBlank())
|
||||
Text(lbl.meta, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall,
|
||||
color = LocalContentColor.current.copy(0.75f))
|
||||
@@ -536,7 +534,9 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
val allInputs by vm.inputs
|
||||
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 timeMin by remember { mutableStateOf(Instant.DISTANT_PAST) }
|
||||
@@ -552,7 +552,17 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
val enabledHardware = remember(allHardware) { 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)
|
||||
enabledHardware.clear()
|
||||
enabledInputs.clear()
|
||||
@@ -617,15 +627,23 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
var max = 0.0f
|
||||
baselineComparison = baseline?.let { bl ->
|
||||
class MeanAccumulator(var sum: Float = 0.0f, var count: Float = 0.0f) {
|
||||
fun add(f: Float) { sum += f; count += 1.0f }
|
||||
fun mean() = if(count == 0.0f) 0.0f else sum / count
|
||||
fun add(f: Float) {
|
||||
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>>()
|
||||
filteredEntries.forEach { entry ->
|
||||
val corrected = entry.measurements.map { entry.nativeUnit.convertTo(currentUnit, it) }.sum()
|
||||
perVersion.getOrPut(entry.label) { mutableMapOf() }
|
||||
.getOrPut(entry.input) { MeanAccumulator() }
|
||||
.add(corrected)
|
||||
.add(corrected, entry.measurements.size)
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
Row(Modifier.fillMaxSize().padding(20.dp)) {
|
||||
Row(Modifier.weight(0.66f).fillMaxHeight()) {
|
||||
val uiMode = GlobalUiMode.current
|
||||
var showOptions by remember(uiMode) { mutableStateOf(uiMode == UiMode.WIDE) }
|
||||
|
||||
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)) {
|
||||
Box(Modifier.fillMaxWidth().fillMaxHeight(0.25f).padding(35.dp)) {
|
||||
Text("No data yet.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
else {
|
||||
Column {
|
||||
Text("Measurements", style = MaterialTheme.typography.headlineSmall)
|
||||
} else {
|
||||
Column(Modifier.padding(35.dp)) {
|
||||
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)
|
||||
Spacer(Modifier.width(5.dp))
|
||||
SingleChoiceSegmentedButtonRow(Modifier.weight(1f)) {
|
||||
@@ -675,45 +708,56 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
SegmentedButton(
|
||||
currentUnit == it,
|
||||
{ currentUnit = it },
|
||||
shape = SegmentedButtonDefaults.itemShape(idx, TimeUnit.entries.size)
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
idx,
|
||||
TimeUnit.entries.size
|
||||
)
|
||||
) {
|
||||
Text(it.disp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(50.dp))
|
||||
|
||||
Row(Modifier.weight(0.33f), verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(Modifier.height(5.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Display: ", fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.width(5.dp))
|
||||
SingleChoiceSegmentedButtonRow(Modifier.weight(1f)) {
|
||||
SegmentedButton(!displayWarmups, { displayWarmups = false },
|
||||
SegmentedButton(
|
||||
!displayWarmups, { displayWarmups = false },
|
||||
shape = SegmentedButtonDefaults.itemShape(0, 2)
|
||||
) {
|
||||
Text("(Steady-state) measurements")
|
||||
}
|
||||
|
||||
SegmentedButton(displayWarmups, { displayWarmups = true },
|
||||
SegmentedButton(
|
||||
displayWarmups, { displayWarmups = true },
|
||||
shape = SegmentedButtonDefaults.itemShape(1, 2)
|
||||
) {
|
||||
Text("Warmup measurements")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Filter by timestamp: ", fontWeight = FontWeight.Bold)
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(15.dp))
|
||||
|
||||
if (!isNarrow || baselineComparison == null) {
|
||||
XYGraph(
|
||||
rememberFloatLinearAxisModel(listOf(0f, xMax).autoScaleRange()),
|
||||
rememberFloatLinearAxisModel(listOf(yMin, yMax).autoScaleRange()),
|
||||
@@ -735,24 +779,49 @@ 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()) {
|
||||
Row {
|
||||
sideContent(false) { isNarrow, toggleSide ->
|
||||
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)) {
|
||||
Text("Versions", style = MaterialTheme.typography.headlineSmall)
|
||||
Text("Click a version to toggle its visibility.", fontStyle = FontStyle.Italic,
|
||||
color = LocalContentColor.current.copy(alpha = 0.5f), style = MaterialTheme.typography.bodySmall)
|
||||
Text(
|
||||
"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))
|
||||
if (versions.isEmpty()) {
|
||||
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)) {
|
||||
items(versions.toList().sortedBy { it.second.name }) { (_, lbl) ->
|
||||
Box(Modifier.fillMaxWidth().clickable {
|
||||
@@ -760,8 +829,7 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
if (versionFilter.size == versions.size) {
|
||||
versionFilter.clear()
|
||||
versionFilter.add(lbl)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
versionFilter.toggle(lbl)
|
||||
if (versionFilter.isEmpty()) versionFilter.addAll(versions.values)
|
||||
}
|
||||
@@ -781,22 +849,38 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
}
|
||||
}
|
||||
if (editable) {
|
||||
IconButton({ editingVersion = lbl }, Modifier.align(Alignment.CenterVertically)) {
|
||||
IconButton(
|
||||
{ editingVersion = lbl },
|
||||
Modifier.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Icon(Lucide.Pencil, "Edit version")
|
||||
}
|
||||
}
|
||||
|
||||
IconButton({ baseline = if(baseline == lbl) null else lbl },
|
||||
Modifier.align(Alignment.CenterVertically), enabled = lbl in versionFilter) {
|
||||
IconButton(
|
||||
{ baseline = if (baseline == lbl) null else lbl },
|
||||
Modifier.align(Alignment.CenterVertically),
|
||||
enabled = lbl in versionFilter
|
||||
) {
|
||||
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))
|
||||
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({ deletingVersion = lbl }, Modifier.align(Alignment.CenterVertically)) {
|
||||
IconButton(
|
||||
{ deletingVersion = lbl },
|
||||
Modifier.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Icon(Lucide.Trash, "Delete version")
|
||||
}
|
||||
}
|
||||
@@ -810,8 +894,12 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
LazyColumn(Modifier.weight(0.5f)) {
|
||||
item { Text("Inputs", style = MaterialTheme.typography.headlineSmall) }
|
||||
item {
|
||||
Text("Click an input to set or clear the filter.", fontStyle = FontStyle.Italic,
|
||||
color = LocalContentColor.current.copy(0.5f), style = MaterialTheme.typography.bodySmall)
|
||||
Text(
|
||||
"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)) }
|
||||
@@ -821,18 +909,24 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
if (inputFilter.size == allInputs.size) {
|
||||
inputFilter.clear()
|
||||
inputFilter.add(it)
|
||||
}
|
||||
else if(inputFilter.size == 1) {
|
||||
} else if (inputFilter.size == 1) {
|
||||
inputFilter.addAll(allInputs.toSet()); inputFilter.remove(it)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
inputFilter.toggle(it)
|
||||
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))
|
||||
if(it !in enabledInputs) Text(" (no data)", Modifier.align(Alignment.Bottom),
|
||||
style = MaterialTheme.typography.bodySmall, color = LocalContentColor.current.copy(0.9f))
|
||||
Text(
|
||||
it,
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,13 +935,16 @@ fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
if (inputFilter.size == allInputs.size) {
|
||||
inputFilter.clear()
|
||||
inputFilter.add(it)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
inputFilter.toggle(it)
|
||||
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("Click an item to set or clear the filter.", fontStyle = FontStyle.Italic,
|
||||
color = LocalContentColor.current.copy(0.5f), style = MaterialTheme.typography.bodySmall)
|
||||
Text(
|
||||
"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)) }
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
items((allHardware - enabledHardware.toSet()).toList()) {
|
||||
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()) }
|
||||
Text(
|
||||
it,
|
||||
Modifier.padding(start = 5.dp, top = 2.dp, bottom = 2.dp),
|
||||
color = LocalContentColor.current.copy(0.66f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
if (!isNarrow) {
|
||||
baselineComparison?.let { blc ->
|
||||
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
|
||||
fun ConfirmDeleteVersionDialog(version: ProjectVM.Version, onCancel: () -> Unit, onDelete: () -> Unit) {
|
||||
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)) {
|
||||
Text("Confirm deletion", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
||||
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.")
|
||||
Spacer(Modifier.height(10.dp))
|
||||
CancelConfirmXRow(onCancel, onDelete, confirmText = "Delete")
|
||||
|
||||
@@ -1,10 +1,69 @@
|
||||
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.WindowPlacement
|
||||
import androidx.compose.ui.window.WindowPosition
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -65,7 +65,7 @@ actual fun platformClient(builder: HttpClientConfig<*>.() -> Unit): HttpClient =
|
||||
actual suspend fun String.toClipEntry(): ClipEntry =
|
||||
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
|
||||
actual fun ServerSelect(server: String, onUpdate: (String) -> Unit) = DefaultServerSelect(server, onUpdate)
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
package com.jaytux.phoebench.clients
|
||||
|
||||
import androidx.compose.runtime.CompositionLocal
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.ComposeViewport
|
||||
import com.jaytux.phoebench.clients.theme.GlobalUiMode
|
||||
import com.jaytux.phoebench.clients.theme.UiMode
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.window
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
suspend fun main() {
|
||||
config = loadConfig()
|
||||
ComposeViewport(document.body!!) {
|
||||
val width = LocalWindowInfo.current.containerSize
|
||||
var mode by remember(width) { mutableStateOf(UiMode.fromDp(width.width.dp)) }
|
||||
|
||||
CompositionLocalProvider(GlobalUiMode provides mode) {
|
||||
App()
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -63,7 +63,7 @@ actual suspend fun String.toClipEntry(): ClipEntry =
|
||||
@JsFun("(num) => num.toFixed(3)")
|
||||
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
|
||||
data class Config(val server: String)
|
||||
|
||||
Reference in New Issue
Block a user