JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.24k stars 1.18k forks source link

Can't have one part of a SplitPane be empty #1133

Closed rnett closed 1 month ago

rnett commented 3 years ago

I'm trying to use a HorizontalSplitPane for an info sidebar that will only be open sometimes. I have something like this:

val splitState = rememberSplitPaneState(0.8f)

HorizontalSplitPane(Modifier, splitState) {
    first(200.dp) {
        infoState.withNew {
            page.show(this@WithMainState)
        }
    }
    second(50.dp) {
        AnimatedVisibility(
            sidebarPage != null,
            Modifier.fillMaxWidth().weight(0.2f),
            enter = fadeIn() + expandHorizontally(),
            exit = shrinkHorizontally() + fadeOut()
        ) {

            sidebarPage?.let {
                Sidebar(sidebarState, it)
            }
        }
    }
    splitter {
        visiblePart {
            Box(
                Modifier
                    .width(4.dp)
                    .fillMaxHeight()
                    .background(Color.LightGray)
            )
        }
        handle {
            Box(
                Modifier
                    .markAsHandle()
                    .pointerIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
                    .background(Color.Gray.copy(alpha = 0.50f))
                    .width(9.dp)
                    .fillMaxHeight()
            )
        }
    }
}

When I run it I get:

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException: Cannot invoke "androidx.compose.ui.layout.Measurable.measure-BRTryo0(long)" because the return value of "java.util.List.get(int)" is null
    at org.jetbrains.compose.splitpane.DesktopSplitPaneKt$SplitPane$2.measure-3p2s80s(DesktopSplitPane.kt:113)
    at androidx.compose.ui.node.InnerPlaceable.measure-BRTryo0(InnerPlaceable.kt:50)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:100)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:128)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:75)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:63)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.measure-BRTryo0(OuterMeasurablePlaceable.kt:71)
    at androidx.compose.ui.node.LayoutNode.measure-BRTryo0(LayoutNode.kt:1238)
    at androidx.compose.foundation.layout.RowColumnImplKt$rowColumnMeasurePolicy$1.measure-3p2s80s(RowColumnImpl.kt:89)
    at androidx.compose.ui.node.InnerPlaceable.measure-BRTryo0(InnerPlaceable.kt:50)
    at androidx.compose.foundation.layout.FillModifier.measure-3p2s80s(Size.kt:627)
    at androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt:39)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:100)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:128)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:75)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:63)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.measure-BRTryo0(OuterMeasurablePlaceable.kt:71)
    at androidx.compose.ui.node.LayoutNode.measure-BRTryo0(LayoutNode.kt:1238)
    at androidx.compose.foundation.layout.BoxKt$boxMeasurePolicy$1.measure-3p2s80s(Box.kt:115)
    at androidx.compose.foundation.layout.BoxWithConstraintsKt$BoxWithConstraints$1$1.invoke-0kLqBqw(BoxWithConstraints.kt:67)
    at androidx.compose.foundation.layout.BoxWithConstraintsKt$BoxWithConstraints$1$1.invoke(BoxWithConstraints.kt:64)
    at androidx.compose.ui.layout.SubcomposeLayoutState$createMeasurePolicy$1.measure-3p2s80s(SubcomposeLayout.kt:345)
    at androidx.compose.ui.node.InnerPlaceable.measure-BRTryo0(InnerPlaceable.kt:50)
    at androidx.compose.foundation.layout.FillModifier.measure-3p2s80s(Size.kt:627)
    at androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt:39)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:100)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:128)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:75)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:63)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.measure-BRTryo0(OuterMeasurablePlaceable.kt:71)
    at androidx.compose.ui.node.LayoutNode.measure-BRTryo0(LayoutNode.kt:1238)
    at androidx.compose.foundation.layout.BoxKt$boxMeasurePolicy$1.measure-3p2s80s(Box.kt:115)
    at androidx.compose.ui.node.InnerPlaceable.measure-BRTryo0(InnerPlaceable.kt:50)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:219)
    at androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt:39)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.foundation.layout.FillModifier.measure-3p2s80s(Size.kt:627)
    at androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt:39)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:100)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:128)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:75)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:63)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.measure-BRTryo0(OuterMeasurablePlaceable.kt:71)
    at androidx.compose.ui.node.LayoutNode.measure-BRTryo0(LayoutNode.kt:1238)
    at androidx.compose.ui.layout.RootMeasurePolicy.measure-3p2s80s(RootMeasurePolicy.kt:38)
    at androidx.compose.ui.node.InnerPlaceable.measure-BRTryo0(InnerPlaceable.kt:50)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:100)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:1776)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:123)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:75)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:63)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui(LayoutNode.kt:1247)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-0kLqBqw(MeasureAndLayoutDelegate.kt:169)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.access$doRemeasure-0kLqBqw(MeasureAndLayoutDelegate.kt:38)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout(MeasureAndLayoutDelegate.kt:207)
    at androidx.compose.ui.platform.DesktopOwner.measureAndLayout(DesktopOwner.desktop.kt:253)
    at androidx.compose.ui.awt.ComposeLayer$Wrapped.doLayout(ComposeLayer.desktop.kt:124)
    at java.desktop/java.awt.Container.validateTree(Container.java:1722)
    at java.desktop/java.awt.Container.validateTree(Container.java:1731)
    at java.desktop/java.awt.Container.validateTree(Container.java:1731)
    at java.desktop/java.awt.Container.validateTree(Container.java:1731)
    at java.desktop/java.awt.Container.validateTree(Container.java:1731)
    at java.desktop/java.awt.Container.validateTree(Container.java:1731)
    at java.desktop/java.awt.Container.validate(Container.java:1657)
    at java.desktop/java.awt.Container.validateUnconditionally(Container.java:1694)
    at java.desktop/java.awt.Window.show(Window.java:1047)
    at java.desktop/java.awt.Component.show(Component.java:1717)
    at java.desktop/java.awt.Component.setVisible(Component.java:1664)
    at java.desktop/java.awt.Window.setVisible(Window.java:1028)
    at androidx.compose.ui.window.AwtWindow_desktopKt$AwtWindow$4$1.invokeSuspend(AwtWindow.desktop.kt:122)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:316)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
Exception in thread "AWT-EventQueue-0" java.lang.IllegalStateException: measure() may not be called multiple times on the same Measurable
    at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:86)
    at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui(LayoutNode.kt:1247)
    at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui$default(LayoutNode.kt:1243)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-0kLqBqw(MeasureAndLayoutDelegate.kt:171)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.access$doRemeasure-0kLqBqw(MeasureAndLayoutDelegate.kt:38)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout(MeasureAndLayoutDelegate.kt:207)
    at androidx.compose.ui.platform.DesktopOwner.measureAndLayout(DesktopOwner.desktop.kt:253)
    at androidx.compose.ui.platform.DesktopOwner.render(DesktopOwner.desktop.kt:226)
    at androidx.compose.ui.platform.DesktopOwners.onFrame(DesktopOwners.desktop.kt:129)
    at androidx.compose.ui.awt.ComposeLayer$1.onRender(ComposeLayer.desktop.kt:147)
    at org.jetbrains.skiko.SkiaLayer.update$skiko(SkiaLayer.kt:314)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer.update(Direct3DRedrawer.kt:49)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer.access$update(Direct3DRedrawer.kt:13)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invokeSuspend(Direct3DRedrawer.kt:22)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invoke(Direct3DRedrawer.kt)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invoke(Direct3DRedrawer.kt)
    at org.jetbrains.skiko.FrameDispatcher$job$1.invokeSuspend(FrameDispatcher.kt:36)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:316)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

This is with 1.0.0-alpha4-build321 but there haven't been any changes to SplitPane since then.

An alternative that works is moving the if outside of second, i.e.

if(sidebarPage != null){
    second(50.dp){
        ...
    }
}

however that does not animate, and re-composes first when the condition changes, which is prohibitively expensive.

okushnikov commented 2 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.