1

I'm taking a DropdownMenu from Kotlin Compose for Desktop and I want to include a vertical scroll bar. The source code for the DropdownMenu is here. They have a sample which works fine but I can't get it to display the vertical scroll bar. It doesn't display by default.

There is a VerticalScrollbar example also that I have attempted but I haven't gotten it to work with the DropdownMenu.

Putting a verticalScroll() within the DropdownMenu(Modifier) results in an error Vertically scrolled component was measured with an infinity maximum height constraints, which is disallowed....

Adding a VerticalScrollbar() beneath the DropdownMenu results in an error Can't represent a size of 1073741824 in Constraints.

So as far as I can tell there is something in the DropdownMenu's Popup or something like that which is making this difficult for me.

Is there a way I can implement the visible scroll bar? My layout is like this so far. (you can scroll down to the Dropdown menu below...)

data class Lookup(val id: String, val name: String)

fun main() = application {
   Window(
      onCloseRequest = ::exitApplication,
      state = rememberWindowState(width = 1280.dp, height = 800.dp)
   ) {
      MaterialTheme {
         Scaffold {
            Column(
               modifier = Modifier
                  .fillMaxSize()
                  .verticalScroll(rememberScrollState()),
               horizontalAlignment = Alignment.CenterHorizontally
            ) {
               val lookups = LookupService.getLookups() // about 75 items
               val (expanded, setExpanded) = remember { mutableStateOf(false) }
               val (selected, setSelected) = remember { mutableStateOf<Lookup?>(null) }

               Spacer(Modifier.height(20.dp))
               Box(Modifier.wrapContentSize(Alignment.TopStart)) {
                  val icon = if (expanded) {
                     Icons.Filled.KeyboardArrowUp
                  } else {
                     Icons.Filled.KeyboardArrowDown
                  }
                  OutlinedTextField(
                     value = selected?.name ?: "",
                     onValueChange = { },
                     modifier = Modifier
                        .width(360.dp)
                        .onKeyEvent {
                           if (it.key == Key.DirectionDown && !expanded) {
                              setExpanded(true)
                              return@onKeyEvent true
                           }
                           return false
                        }
                        .clickable { setExpanded(true) },
                     singleLine = true,
                     label = { Text("Select an item") },
                     trailingIcon = {
                        Icon(icon, "Select an item", Modifier.clickable {
                           setExpanded(!expanded)
                        })
                     },
                     enabled = expanded,
                     colors = TextFieldDefaults.textFieldColors(
                        disabledTextColor = LocalContentColor.current.copy(LocalContentAlpha.current),
                        backgroundColor = Color.Transparent
                     )
                  )

                  DropdownMenu( // Desktop version, so it creates a "Popup" per the source code
                     expanded = expanded,
                     onDismissRequest = {
                        runBlocking { // to handle a glitch where the dropdown may "unexpand & expand" again on clicking
                           delay(200)
                           setExpanded(false)
                        }
                     },
                     modifier = Modifier
                        .width(360.dp)
                        .background(Color.White)
                        .clip(RoundedCornerShape(5.dp))
                     // SHOULD HAVE VERTICAL SCROLLBAR SHOW UP AS PART OF THIS DROPDOWNMENU COLUMN
                  ) {
                     lookups.forEach { lookup -> 
                        DropdownMenuItem(
                           onClick = {
                              setExpanded(false)
                              setSelected(lookup)
                           }
                        ) {
                           Text(lookup.name)
                        }
                     }
                  }
               }
            }
         }
      }
   }
}
Jesser
  • 76
  • 7
  • Can you show your layout where it says scroll enabled and visible – Camp Nerd Jun 14 '23 at 11:14
  • @CampNerd I edited my question and included the layout and more error details. I don't know what is meant by *"scroll enabled and visible"* - am I just missing something simple like that? – Jesser Jun 15 '23 at 16:31
  • do you have an xml layout for the UI. If you do you can type scroll and scrollable in the element for the drop down and it you can have it horizontal, vertical, visible and non visible... From what I can tell here is your trying to build a drop down from scratch. Is that correct? – Camp Nerd Jun 16 '23 at 09:37
  • take a look at the spinner https://developer.android.com/develop/ui/views/components/spinner – Camp Nerd Jun 16 '23 at 09:39

1 Answers1

0

I found the answer in a currently open issue Scrollbar doesn't work for DropdownMenu #587.

This is how I just got my scrollbar working within the Desktop version of DropdownMenu.

  1. Copy this version of DesktopMenu.desktop.kt into your project.
  2. Copy this version of Menu.kt into your project.
  3. Add this Card into your Menu.kt, wrapping around the Column which displays the content.
  4. As per this comment in the issue, add this into the Card from point 3.
Box(
    modifier = modifier
        .width(IntrinsicSize.Max)
) {
    val scrollState = rememberScrollState()
    var columnSize by remember { mutableStateOf<IntSize?>(null) }
    Column(
        modifier = Modifier
            .padding(vertical = DropdownMenuVerticalPadding)
            .verticalScroll(scrollState)
            .onSizeChanged { size ->
                columnSize = size
            },
        content = content
    )
    columnSize?.let { size ->
        VerticalScrollbar(
            modifier = Modifier
                .align(Alignment.CenterEnd)
                .height(with(LocalDensity.current) { size.height.toDp() }),
            scrollState = scrollState
        )
    }
}
  1. Last point is that this then would overflow above my screen, so I went back to the current version of DesktopMenu.desktop.kt to put back in this version of DesktopDropdownMenuPositionProvider into my DesktopMenu.desktop.kt.
/**
 * Positions a dropdown relative to another widget (its anchor).
 */
@Immutable
internal data class DesktopDropdownMenuPositionProvider(
    val contentOffset: DpOffset,
    val density: Density,
    val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }
) : PopupPositionProvider {
    override fun calculatePosition(
        anchorBounds: IntRect,
        windowSize: IntSize,
        layoutDirection: LayoutDirection,
        popupContentSize: IntSize
    ): IntOffset {

        val isLtr = layoutDirection == LayoutDirection.Ltr

        // Coerce such that this..this+size fits into min..max; if impossible, align with min
        fun Int.coerceWithSizeIntoRangePreferMin(size: Int, min: Int, max: Int) = when {
            this < min -> min
            this + size > max -> max - size
            else -> this
        }

        // Coerce such that this..this+size fits into min..max; if impossible, align with max
        fun Int.coerceWithSizeIntoRangePreferMax(size: Int, min: Int, max: Int) = when {
            this + size > max -> max - size
            this < min -> min
            else -> this
        }

        fun Int.coerceWithSizeIntoRange(size: Int, min: Int, max: Int) = when {
            isLtr -> coerceWithSizeIntoRangePreferMin(size, min, max)
            else -> coerceWithSizeIntoRangePreferMax(size, min, max)
        }

        // The min margin above and below the menu, relative to the screen.
        val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() }
        // The content offset specified using the dropdown offset parameter.
        val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
        val contentOffsetY = with(density) { contentOffset.y.roundToPx() }

        // Compute horizontal position.
        val preferredX = if (isLtr) {
            anchorBounds.left + contentOffsetX
        }
        else {
            anchorBounds.right - contentOffsetX - popupContentSize.width
        }
        val x = preferredX.coerceWithSizeIntoRange(
            size = popupContentSize.width,
            min = 0,
            max = windowSize.width
        )

        // Compute vertical position.
        val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
        val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
        val toCenter = anchorBounds.top - popupContentSize.height / 2
        val toWindowBottom = windowSize.height - popupContentSize.height - verticalMargin
        var y = sequenceOf(toBottom, toTop, toCenter, toWindowBottom).firstOrNull {
            it >= verticalMargin &&
                it + popupContentSize.height <= windowSize.height - verticalMargin
        } ?: toTop

        // Desktop specific vertical position checking
        val aboveAnchor = anchorBounds.top + contentOffsetY
        val belowAnchor = windowSize.height - anchorBounds.bottom - contentOffsetY

        if (belowAnchor >= aboveAnchor) {
            y = anchorBounds.bottom + contentOffsetY
        }

        if (y + popupContentSize.height > windowSize.height) {
            y = windowSize.height - popupContentSize.height
        }

        y = y.coerceAtLeast(0)

        onPositionCalculated(
            anchorBounds,
            IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height)
        )
        return IntOffset(x, y)
    }
}
Jesser
  • 76
  • 7