fun PlotPanelComposeCanvas()

in lets-plot-compose/src/androidMain/kotlin/org/jetbrains/letsPlot/compose/PlotPanelComposeCanvas.kt [55:238]


fun PlotPanelComposeCanvas(
    rawSpec: MutableMap<String, Any>,
    preserveAspectRatio: Boolean,
    modifier: Modifier,
    errorTextStyle: TextStyle,
    errorModifier: Modifier,
    computationMessagesHandler: (List<String>) -> Unit
) {
    if (logRecompositions) {
        println("PlotPanelRaw: recomposition")
    }

    val density = LocalDensity.current.density
    val composeMouseEventMapper = remember { ComposeMouseEventMapper() }
    // Update density on each recomposition to handle monitor DPI changes (e.g., drag between HIDPI/regular monitor)

    // Cache processed plot spec to avoid reprocessing the same raw spec on every recomposition.
    // Note: Use remember(rawSpec.hashCode()), to bypass the equality check and use the content hash directly.
    // The issue was that remember(rawSpec) uses some kind of comparison (equals()?) which somehow not working for `MutableMap`.
    val processedPlotSpec = remember(rawSpec.hashCode()) {
        processRawSpecs(rawSpec, frontendOnly = false)
    }

    var panelSize by remember { mutableStateOf(DoubleVector.ZERO) }
    var plotPosition by remember { mutableStateOf(DoubleVector.ZERO) }
    var dispatchComputationMessages by remember { mutableStateOf(true) }
    var specOverrideList by remember { mutableStateOf(emptyList<Map<String, Any>>()) }

    var plotFigureModel by remember { mutableStateOf<PlotFigureModel?>(null) }

    var errorMessage: String? by remember(processedPlotSpec, panelSize) { mutableStateOf(null) }

    var redrawTrigger by remember { mutableStateOf(0) }

    val androidCanvasPeer = remember { AndroidCanvasPeer() }

    // Reset the old plot on error to prevent blinking
    // We can't reset PlotContainer using updateViewmodel(), so we create a new one.
    val plotCanvasFigure2 = remember(errorMessage) {
        PlotCanvasFigure2().apply {
            eventPeer.addEventSource(composeMouseEventMapper)
        }
    }

    val reg = remember(plotCanvasFigure2) {
        CompositeRegistration(
            // trigger recomposition on repaint request
            plotCanvasFigure2.onRepaintRequested { redrawTrigger++ },
            plotCanvasFigure2.mapToCanvas(androidCanvasPeer)
        )
    }

    // Background
    val finalModifier = if (errorMessage != null) {
        modifier.background(Color.LightGray)
    } else {
        if (containsBackground(modifier)) {
            // Do not change the user-defined background
            modifier
        } else {
            // Use background color from the plot theme
            val lpColor = PlotThemeHelper.plotBackground(processedPlotSpec)
            val lpBackground = Color(lpColor.red, lpColor.green, lpColor.blue, lpColor.alpha)
            modifier.background(lpBackground)
        }
    }


    DisposableEffect(reg) {
        onDispose {
            // Try/catch to ensure that any exception in dispose() does not break the Composable lifecycle
            // Otherwise, the app window gets unclosable.
            try {
                reg.dispose()
                //plotCanvasFigure2.dispose()
            } catch (e: Exception) {
                LOG.error(e) { "reg.dispose() failed" }
            }
        }
    }

    Column(modifier = finalModifier) {
        //if (plotFigureModel != null && GG_TOOLBAR in processedPlotSpec) {
        //    PlotToolbar(plotFigureModel!!)
        //}

        Box(
            modifier = finalModifier
                .weight(1f) // Take the remaining vertical space
                .fillMaxWidth() // Fill available width
                .onSizeChanged { newSize ->
                    // Convert logical pixels (from Compose layout) to physical pixels (plot SVG pixels)
                    panelSize = DoubleVector(newSize.width / density, newSize.height / density)
                }
        ) {
            val errMsg = errorMessage
            if (errMsg != null) {
                // Show error message
                BasicTextField(
                    value = errMsg,
                    onValueChange = { },
                    readOnly = true,
                    textStyle = errorTextStyle,
                    modifier = errorModifier
                )
            } else {
                // Render the plot
                LaunchedEffect(panelSize, processedPlotSpec, specOverrideList, preserveAspectRatio) {

                    if (PlotConfig.isFailure(processedPlotSpec)) {
                        errorMessage = PlotConfig.getErrorMessage(processedPlotSpec)
                        return@LaunchedEffect
                    }

                    runCatching {
                        if (panelSize != DoubleVector.ZERO) {
                            val plotSpec = applySpecOverride(processedPlotSpec, specOverrideList).toMutableMap()

                            plotCanvasFigure2.update(plotSpec, fitContainerSize(preserveAspectRatio)) { messages ->
                                if (dispatchComputationMessages) {
                                    // do once
                                    dispatchComputationMessages = false
                                    computationMessagesHandler(messages)
                                }
                            }

                            if (plotFigureModel == null) {
                                plotFigureModel = PlotFigureModel(
                                    onUpdateView = { specOverride ->
                                        specOverrideList = FigureModelHelper.updateSpecOverrideList(
                                            specOverrideList = specOverrideList,
                                            newSpecOverride = specOverride
                                        )
                                    }
                                )
                            }

                            plotFigureModel!!.toolEventDispatcher = plotCanvasFigure2.toolEventDispatcher


                            val plotWidth = plotCanvasFigure2.size.x
                            val plotHeight = plotCanvasFigure2.size.y

                            // Calculate centering position in physical pixels
                            // Both panelSize and plot dimensions are in physical pixels
                            plotPosition = DoubleVector(
                                maxOf(0.0, (panelSize.x - plotWidth) / 2.0),
                                maxOf(0.0, (panelSize.y - plotHeight) / 2.0)
                            )

                            composeMouseEventMapper.setOffset(plotPosition.x.toFloat(), plotPosition.y.toFloat())

                            redrawTrigger++ // trigger repaint
                        }
                    }.getOrElse { e ->
                        errorMessage = "${e.message}"
                        return@LaunchedEffect
                    }
                }

                Canvas(
                    modifier = modifier
                        .fillMaxSize()
                        .pointerHoverIcon(PointerIcon(TYPE_CROSSHAIR))
                        .onSizeChanged { size ->
                            // Convert canvas logical pixels (from Compose layout) to physical pixels (plot SVG pixels)
                            plotCanvasFigure2.resize(size.width / density, size.height / density)
                        }
                        .pointerInput(composeMouseEventMapper, composeMouseEventMapper)
                ) {
                    // By reading redrawTrigger here, Compose knows to recompose
                    // this Canvas block whenever it changes.
                    redrawTrigger

                    val ctx = AndroidContext2d(drawContext.canvas.nativeCanvas, pixelDensity = 1.0)
                    ctx.scale(density.toDouble(), density.toDouble()) // logical → physical pixels

                    ctx.translate(plotPosition.x, plotPosition.y)
                    plotCanvasFigure2.paint(ctx)
                }
            }
        }
    }
}