@@ -168,31 +168,72 @@ public fun Modifier.balloon(
168168 val horizontalPadding = builder.paddingLeft + builder.paddingRight +
169169 builder.marginLeft + builder.marginRight
170170
171- // Pre-measure balloon content if not yet measured
172- if (balloonLayoutInfo.value == null ) {
171+ // Pre-measure balloon content if not yet measured (or if fixed-width target changed)
172+ val fixedWidthMode = builder.widthRatio > 0f || builder.maxWidthRatio > 0f
173+
174+ // In fixed-width mode, the target width is screen-based, not anchor-constraint-based.
175+ // We can compute it outside the measurePolicy so we can re-trigger this Layout when it changes.
176+ val desiredFixedWidth = if (fixedWidthMode) {
177+ val w = when {
178+ builder.widthRatio > 0f ->
179+ (screenWidth * builder.widthRatio - horizontalPadding).toInt()
180+
181+ builder.maxWidthRatio > 0f ->
182+ (screenWidth * builder.maxWidthRatio - horizontalPadding).toInt()
183+
184+ else -> 0
185+ }.coerceAtLeast(0 )
186+ w
187+ } else {
188+ null
189+ }
190+
191+ if (balloonLayoutInfo.value == null ||
192+ (desiredFixedWidth != null && balloonLayoutInfo.value?.width != desiredFixedWidth)
193+ ) {
173194 Layout (
174195 content = { balloonContent() },
175196 measurePolicy = { measurables, constraints ->
197+
176198 val maxContentWidth = when {
177199 builder.widthRatio > 0f ->
178200 (screenWidth * builder.widthRatio - horizontalPadding).toInt()
201+
179202 builder.maxWidthRatio > 0f ->
180203 (screenWidth * builder.maxWidthRatio - horizontalPadding).toInt()
181- else -> constraints.maxWidth - horizontalPadding
204+
205+ else ->
206+ constraints.maxWidth - horizontalPadding
207+ }.coerceAtLeast(0 )
208+
209+ val targetWidth = if (fixedWidthMode) {
210+ // IMPORTANT: do NOT clamp to constraints.maxWidth (anchor width)
211+ maxContentWidth
212+ } else {
213+ // Non-fixed mode: behave like before, limited by the anchor constraints.
214+ maxContentWidth.coerceAtMost((constraints.maxWidth - horizontalPadding).coerceAtLeast(0 ))
182215 }.coerceAtLeast(0 )
183216
184217 val contentConstraints = Constraints (
185- minWidth = 0 ,
186- maxWidth = maxContentWidth.coerceAtMost(constraints.maxWidth),
218+ // IMPORTANT: in fixed mode, force the content host to measure at EXACT width
219+ minWidth = if (fixedWidthMode) targetWidth else 0 ,
220+ maxWidth = targetWidth,
187221 minHeight = 0 ,
188222 maxHeight = constraints.maxHeight,
189223 )
190224
191225 val placeables = measurables.map { it.measure(contentConstraints) }
192226
193227 if (placeables.isNotEmpty()) {
194- val measuredWidth = placeables.maxOf { it.width }
195- val measuredHeight = placeables.maxOf { it.height }
228+ val measuredHeight = placeables.maxOf { it.height }.coerceAtLeast(0 )
229+
230+ // IMPORTANT: in fixed mode, the card width must be the fixed width,
231+ // not the max placeable width (which can still end up smaller).
232+ val measuredWidth = if (fixedWidthMode) {
233+ targetWidth
234+ } else {
235+ placeables.maxOf { it.width }.coerceAtLeast(0 )
236+ }
196237
197238 if (measuredWidth > 0 && measuredHeight > 0 ) {
198239 val size = IntSize (width = measuredWidth, height = measuredHeight)
0 commit comments