WITH the Box and VerticalScrollbar the Footer belo...
# compose-desktop
d
WITH the Box and VerticalScrollbar the Footer below the Column is (almost) not visible. (below window or whatever component I put it in, lower footer will be cut as in the screenshot) If I remove the Box and the VerticalScrollbar and only have the Column with the scrollbar inside the Column everything is fine ... any ideas why?
Copy code
@Composable
fun VanillaRootContent() {
    val floatingPopup: MutableState<Boolean> = mutableStateOf(true)
//    Card(Modifier.size(350.dp, 175.dp)) {
        Column(
            Modifier.border(1.dp, MaterialTheme.colors.onBackground).background(MaterialTheme.colors.secondary)
        ) {

            Footer(floatingPopup)

            val verticalScrollState = rememberScrollState(0)
            Box(Modifier.fillMaxWidth()) { // <-- remove
                Column(modifier = Modifier.verticalScroll(verticalScrollState)) {
                    for (i in 1..16) {
                        Text("LAST$i", style = MaterialTheme.typography.body2)
                    }
                }
                VerticalScrollbar( // <-- remove
                    adapter = rememberScrollbarAdapter(verticalScrollState),
                    modifier = Modifier.align(Alignment.CenterEnd),
                    style = LocalScrollbarStyle.current.copy(unhoverColor = Color.Gray.copy(alpha = 0.5F))
                )
            } // <-- remove

            Footer(floatingPopup)
        }
//    }
}

@Composable
private fun Footer(floatingPopup: MutableState<Boolean>) {
    Row(Modifier.border(2.dp, Color.LightGray), verticalAlignment = Alignment.CenterVertically) {
        Spacer(Modifier.weight(1f))
        Text("floatingWithCursor", Modifier.padding(2.dp), style = MaterialTheme.typography.body2)
        Checkbox(
            floatingPopup.value,
            onCheckedChange = { floatingPopup.value = !floatingPopup.value },
            colors = CheckboxDefaults.colors(checkedColor = MaterialTheme.colors.onBackground)
        )
    }
}
message has been deleted
i
VerticalScrollbar
occupies all available space. Should the height of scrollable list also occupy all available space or should it be dynamic?
occupy all available space
In this case you can just use
Box(Modifier.fillMaxWidth().weight(1f))
d
that worked!! @Igor Demin but I don't really understand ...
weight(1f)
says the box will eat all vertical available space after measuring. If
VerticalScrollbar
does eat all available space as you said, wouldn't that be the same?? As the Box will be "as high" as the components in it require
i
but I don't really understand ... 
weight(1f)
 says the box will eat all vertical available space after measuring.
weight
modifier only affects components in Column (VerticalScrollbar is not in Column). It tells how much free space component should occupy.
"free space" = "all space" - "space occupied by unweighted components"
As the Box will be "as high" as the components in it require
No, it will be as high as the height of the parent minus height of the footer/header. If you need the height of the Box to be depended on the child items, you need another approach.
d
I don't need that, but am curious ... could you sketch that "other approach"?
i
could you sketch that "other approach"
Probably it is difficult to implement. You will need a custom layout:
Copy code
Layout({
    Column(...)
    VerticalScrollbar(...)
}) { measureables, constraints ->
    val list = measureables[0].measure(constraints)
    val scrollbar = measureables[1].measure(Constraints.fixed(constraints.maxWidth, list.height))
    layout(list.width, list.height) {
        list.place(0, 0)
        scrollbar.place(constraints.maxWidth - scrollbar.width, 0)
    }
}
For placing the Footer maybe it is also needed to implement a custom layout (not sure).
weight
will not work in this case.
d
hmmm, what about this general "Solution"? Is that a good or bad idea?
var contentSize by mutableStateOf(IntSize.ZERO)
Box {
Box {
Box(Modifier.onGloballyPositioned { contentSize = it.size }) {
Column(Modifier.verticalScroll(verticalScrollState)) {
for(i in 1..20) {
Text("Number${i.toString().padStart(2, '0')}")
}
}
}
}
VerticalScrollbar(
adapter = rememberScrollbarAdapter(verticalScrollState),
modifier = Modifier.height( Dp(contentSize.height / LocalDensity.current.density) )
)
}
By getting the measured content size via
onGloballyPositioned
via its own Box (without the Scrollbar) and setting the Scrollbar (in the parent Box) to that size, it always has the height/width of the content ...
i
Usually it is better to use
Layout
directly (if we can) instead of
onGloballyPositioned
. When we use
onGloballyPositioned
we always resize on the next frame. User can see some short-timed glitches
some short-timed glitches
Probably they are noticeable when we resize the window (the size of the scrollbar will be always behind the size of the content)
d
hmmm, ok, evolving further it seems I really need to learn how to do my own Layout. What I try to achieve is a Popup/Dropdown over/under the cursor on Ctrl-Space. With fixed size content, it works pretty fine. But if I wanna give the Popup a
sizeIn(min, max)
and the Popup having a content (which will get a scrollbar if to large) and having a footer at the bottom of it ... Any Tutorials/Examples you can recommend beyond: https://developer.android.com/jetpack/compose/layout#custom-layouts and https://developer.android.com/codelabs/jetpack-compose-layouts#6 ?
ideally something that teaches my on how to implement my own "slotted" component (comparable to e.g. Scaffold)
i
Unfortunately I don't have good articles/examples besides those on android.com Maybe some examples of
Layout
in official samples can be useful.
d
I've done it very like
Scaffold
does it, result:
revised edition:
revision 4:
Copy code
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.unit.dp
import com.accorddesk.common.common.log.printlnErr

@Composable
fun FramedContent(
    modifier: Modifier = Modifier,
    topPanel: @Composable () -> Unit = {},
    bottomPanel: @Composable () -> Unit = {},
    leftPanel: @Composable (PaddingValues) -> Unit = {},
    rightPanel: @Composable (PaddingValues) -> Unit = {},
    backgroundColor: Color = MaterialTheme.colors.background,
    contentColor: Color = contentColorFor(backgroundColor),
    mainContent: @Composable (PaddingValues) -> Unit

) {
    val child = @Composable { childModifier: Modifier ->
        Surface(modifier = childModifier.border(1.dp, Color.Magenta), color = backgroundColor, contentColor = contentColor) {
            FramedContentLayout(
                topPanel = topPanel,
                bottomPanel = bottomPanel,
                leftPanel = leftPanel,
                rightPanel = rightPanel,
                mainContent = mainContent
            )
        }
    }

    // if something to draw over the FramedContent, e.g. a ModalDrawer, it comes here

    child(modifier)
}

@Composable
private fun FramedContentLayout(
    topPanel: @Composable () -> Unit,
    bottomPanel: @Composable () -> Unit,
    leftPanel: @Composable (PaddingValues) -> Unit,
    rightPanel: @Composable (PaddingValues) -> Unit,
    mainContent: @Composable (PaddingValues) -> Unit
) {
    SubcomposeLayout { constraints ->
        val layoutMaxWidth = constraints.maxWidth
        val layoutMaxHeight = constraints.maxHeight

        // ==========================================================================================================+
        // do some health asserts if FramedContent is placed inside a Component with INFINITE width and/or height
        // ==========================================================================================================+
        val unboundedInfinity = mutableListOf<String>()
        if ( !constraints.hasBoundedWidth && (layoutMaxWidth >= (Int.MAX_VALUE/2) || layoutMaxWidth <= (Int.MIN_VALUE/2)) ) {
            unboundedInfinity.add("unbounded width and width INFINITE")
        }
        if ( !constraints.hasBoundedHeight && (layoutMaxHeight >= (Int.MAX_VALUE/2) || layoutMaxHeight <= (Int.MIN_VALUE/2)) ) {
            unboundedInfinity.add("unbounded height and height INFINITE")
        }
        if (unboundedInfinity.isNotEmpty()) {
            throw Exception("FramedContent ${unboundedInfinity.joinToString()}")
        }

        // ==========================================================================================================+
        // measure each Panel and get back Placeable's for each
        // ==========================================================================================================+

        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)

        // topPanel measuring
        val topPanelPlaceables = subcompose(FramedContentLayoutContent.TopPanel, topPanel).map {
            it.measure(looseConstraints)
        }
        val topPanelHeight = topPanelPlaceables.maxByOrNull { it.height }?.height ?: 0
        val topPanelWidth = topPanelPlaceables.maxByOrNull { it.width }?.width ?: 0

        // bottomPanel measuring
        val bottomPanelPlaceables = subcompose(FramedContentLayoutContent.BottomPanel, bottomPanel).map {
            it.measure(looseConstraints)
        }
        val bottomPanelHeight = bottomPanelPlaceables.maxByOrNull { it.height }?.height ?: 0
        val bottomPanelWidth = bottomPanelPlaceables.maxByOrNull { it.width }?.width ?: 0


        val middlePanelsMaxHeight = (layoutMaxHeight - topPanelHeight - bottomPanelHeight).coerceAtLeast(0)
        val sidePanelInnerPadding = PaddingValues(bottom = bottomPanelHeight.toDp())


        // leftPanel measuring
        val leftPanelPlaceables = subcompose(FramedContentLayoutContent.LeftPanel) {
            leftPanel(sidePanelInnerPadding)
        }.map { it.measure(looseConstraints.copy(maxHeight = middlePanelsMaxHeight)) }
        val leftPanelWidth = leftPanelPlaceables.maxByOrNull { it.width }?.width ?: 0
        val leftPanelHeight = leftPanelPlaceables.maxByOrNull { it.height }?.height ?: 0

        // rightPanel measuring
        val rightPanelPlaceables = subcompose(FramedContentLayoutContent.RightPanel) {
            rightPanel(sidePanelInnerPadding)
        }.map { it.measure(looseConstraints.copy(maxHeight = middlePanelsMaxHeight)) }
        val rightPanelWidth = rightPanelPlaceables.maxByOrNull { it.width }?.width ?: 0
        val rightPanelHeight = rightPanelPlaceables.maxByOrNull { it.height }?.height ?: 0


        val mainContentWidthMax = (layoutMaxWidth - leftPanelWidth - rightPanelWidth).coerceAtLeast(0)


        // mainContent measuring
        val mainContentPlaceables = subcompose(FramedContentLayoutContent.MainContent) {
            val innerPadding = PaddingValues(start = leftPanelWidth.toDp(), bottom = bottomPanelHeight.toDp(), end = rightPanelWidth.toDp())
            mainContent(innerPadding)
        }.map { it.measure(looseConstraints.copy(maxHeight = middlePanelsMaxHeight, maxWidth = mainContentWidthMax)) }
        val mainContentWidth = mainContentPlaceables.maxByOrNull { it.width }?.width ?: 0
        val mainContentHeight = mainContentPlaceables.maxByOrNull { it.height }?.height ?: 0


        // ==========================================================================================================+
        // placing
        // ==========================================================================================================+

        val actualLayoutWidth = leftPanelWidth + mainContentWidth + rightPanelWidth
        val maxLayoutWidth = maxOf(actualLayoutWidth, topPanelWidth, bottomPanelWidth)
        val actualLayoutHeight = topPanelHeight + mainContentHeight + bottomPanelHeight
        val maxLayoutHeight = maxOf(actualLayoutHeight, leftPanelHeight, rightPanelHeight)


        // ==========================================================================================================+
        // do some health asserts before actual placing
        // ==========================================================================================================+

        val actualAsserts = mutableListOf<String>()
        if (bottomPanelWidth > actualLayoutWidth) {
            actualAsserts.add("bottomPanel.width <= actualLayoutWidth(=leftWidth+mainWidth+rightWidth)")
        }
        if (topPanelWidth > actualLayoutWidth) {
            actualAsserts.add("topPanel.width <= actualLayoutWidth(=leftWidth+mainWidth+rightWidth)")
        }
        if (leftPanelHeight > actualLayoutHeight) {
            actualAsserts.add("leftPanel.height <= actualLayoutHeight(=topHeight+mainHeight+rightHeight)")
        }
        if (rightPanelHeight > actualLayoutHeight) {
            actualAsserts.add("rightPanel.height <= actualLayoutHeight(=topHeight+mainHeight+rightHeight)")
        }
        if (actualAsserts.isNotEmpty()) {
            printlnErr("Warning: FramedContent asserts failed for: '${actualAsserts.joinToString()}'. (Do you have a Spacer() or .weight(1f) in it?")
        }

        layout(maxLayoutWidth, maxLayoutHeight) {

            mainContentPlaceables.forEach {
                it.place(leftPanelWidth, topPanelHeight)
            }
            leftPanelPlaceables.forEach {
                it.place( 0, topPanelHeight)
            }
            rightPanelPlaceables.forEach {
                it.place( leftPanelWidth + mainContentWidth, topPanelHeight)
            }
            topPanelPlaceables.forEach {
                it.place(0, 0)
            }
            // The bottom bar is always at the bottom of the layout
            bottomPanelPlaceables.forEach {
                it.place(0, topPanelHeight + mainContentHeight)
            }
        }
    }
}

private enum class FramedContentLayoutContent { TopPanel, BottomPanel, MainContent, LeftPanel, RightPanel }