Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@ freeline.py
freeline/
freeline_project_description.json
.hotswan/

# Claude Code task files
tasks/
83 changes: 83 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Designed and developed by 2019 skydoves (Jaewoong Eum)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.skydoves.balloon.Configuration

plugins {
id(libs.plugins.android.application.get().pluginId)
id(libs.plugins.kotlin.multiplatform.get().pluginId)
id(libs.plugins.jetbrains.compose.get().pluginId)
id(libs.plugins.compose.compiler.get().pluginId)
}

kotlin {
androidTarget()

sourceSets {
val androidMain by getting {
dependencies {
implementation(libs.compose.multiplatform.runtime)
implementation(libs.compose.multiplatform.foundation)
implementation(libs.compose.multiplatform.ui)
implementation(libs.compose.multiplatform.material)
implementation(libs.androidx.activity.compose)

// `:samples-shared` re-exports `:balloon-compose-multiplatform` via `api(...)`.
implementation(project(":samples-shared"))
}
}
}
}

android {
compileSdk = Configuration.compileSdk
namespace = "com.skydoves.balloon.kmpdemo"

sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")

defaultConfig {
applicationId = "com.skydoves.balloon.kmpdemo"
minSdk = Configuration.minSdk
targetSdk = Configuration.targetSdk
versionCode = Configuration.versionCode
versionName = Configuration.versionName
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}

buildFeatures {
compose = true
}

lint {
abortOnError = false
}
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile>().configureEach {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
33 changes: 33 additions & 0 deletions androidApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Designed and developed by 2019 skydoves (Jaewoong Eum)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:allowBackup="true"
android:label="Balloon KMP Demo"
android:supportsRtl="true"
android:theme="@style/KmpDemoTheme">
<activity
android:name=".KmpBalloonActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 skydoves
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.skydoves.balloon.kmpdemo

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.skydoves.balloon.compose.multiplatform.sample.BalloonDemoScreen

/**
* Hosts the shared [BalloonDemoScreen] composable inside an Android activity.
*
* This module deliberately lives alongside the existing `:app` (which uses the
* AndroidX Compose stack) so the Compose-Multiplatform classpath stays isolated
* from the legacy demo and there is no plugin / runtime conflict.
*
* The window setup mirrors `ComposeActivity` in `:app` — same `enableEdgeToEdge()`, same
* `Toast` feedback — so the two demos can be screenshotted and compared pixel for pixel.
*/
class KmpBalloonActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)

setContent {
BalloonDemoScreen(
onMessage = { message ->
Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show()
},
)
}
}
}
27 changes: 27 additions & 0 deletions androidApp/src/androidMain/res/values/styles.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Designed and developed by 2019 skydoves (Jaewoong Eum)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>

<!--
Mirrors `AppTheme.NoActionBar` in the `:app` demo (a plain no-action-bar theme with a
white primary text colour) so the two demos put their content in an identically
configured window and can be compared pixel for pixel.
-->
<style name="KmpDemoTheme" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:textColorPrimary">@android:color/white</item>
</style>
</resources>
190 changes: 190 additions & 0 deletions balloon-compose-multiplatform/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Migrating to `balloon-compose-multiplatform`

This guide is for users of the existing `balloon` (Android-only) and
`balloon-compose` (Android-only Compose wrapper) libraries who want to share
their tooltip code across Android, iOS, Desktop and Wasm/JS targets.

> **The existing `balloon` and `balloon-compose` modules are unchanged.** This
> module is purely additive — your existing Android-only code keeps working
> while you migrate one screen at a time.

## TL;DR

The KMP module ships a **migration-friendly facade** with the same names you
already know:

- `Balloon.Builder` with most of the original setters
- `rememberBalloonBuilder { ... }` Compose DSL
- `BalloonAnimation`, `ArrowOrientation`, `ArrowPositionRules` enums

What's gone (and why):

- All `*Resource(...)` setters — KMP has no `R.dimen` / `R.color` / `R.string`.
Pass values directly: `12.dp`, `Color(0xFFAA0000)`, `"text literal"`.
- All `Drawable` / `View` / `Typeface` / `MovementMethod` setters — those types
are Android-only.
- `Lifecycle` integration — composition lifecycle handles dismissal in Compose.
- Overlay / highlight / preference / show-counts / circular-reveal features —
these layered features will land in follow-up releases as the KMP renderer
matures.

## Old vs. New, side-by-side

### Old (Android-only)

```kotlin
// balloon-compose
val builder = rememberBalloonBuilder {
setArrowSize(10)
setArrowPosition(0.5f)
setArrowPositionRules(ArrowPositionRules.ALIGN_BALLOON)
setWidthRatio(1.0f)
setHeight(BalloonSizeSpec.WRAP)
setPaddingResource(R.dimen.tooltip_padding)
setCornerRadiusResource(R.dimen.tooltip_corner)
setTextResource(R.string.tooltip_text)
setTextColorResource(R.color.white)
setTextIsHtml(true)
setBackgroundColorResource(R.color.tooltip_bg)
setBalloonAnimation(BalloonAnimation.ELASTIC)
setLifecycleOwner(lifecycleOwner)
}
```

### New (KMP, commonMain)

```kotlin
// balloon-compose-multiplatform
val style = rememberBalloonBuilder {
setArrowSize(10.dp)
setArrowPosition(0.5f)
setArrowPositionRules(ArrowPositionRules.ALIGN_BALLOON)
setMaxWidth(280.dp) // widthRatio -> explicit max
setPadding(12.dp) // dimens are inline now
setCornerRadius(8.dp)
setBackgroundColor(Color(0xFF272727))
setBalloonAnimation(BalloonAnimation.ELASTIC)
setAnimationDurationMillis(250)
}

// Text content moves out of the builder and into the balloonContent slot:
val state = rememberBalloonState(style)
Balloon(
state = state,
balloonContent = {
Text(
text = "Tooltip text",
color = Color.White,
)
},
) {
// anchor — the composable the balloon points at
Button(onClick = { state.showAlignTop() }) { Text("Show") }
}
```

The biggest mental shift: **content is a Compose slot now, not a builder
property.** You no longer call `setText(...)` / `setTextColor(...)` — just put a
`Text(...)` (or any composable) inside the `balloonContent { ... }` slot, and
trigger it with `state.show*()` from the anchor.

## Setter mapping

| Old `Balloon.Builder` setter | New `Balloon.Builder` setter | Notes |
| --- | --- | --- |
| `setCornerRadius(Float)` | `setCornerRadius(Dp)` | Pass `Dp` directly. |
| `setCornerRadiusResource(@DimenRes)` | `setCornerRadius(value.dp)` | Inline the value. |
| `setArrowSize(Int)` | `setArrowSize(Dp)` | |
| `setArrowSize(width, height)` (n/a) | `setArrowSize(width: Dp, height: Dp)` | New convenience overload. |
| `setArrowSizeResource(@DimenRes)` | `setArrowSize(value.dp)` | |
| `setArrowWidth(Int)` | `setArrowWidth(Dp)` | |
| `setArrowWidthResource(@DimenRes)` | `setArrowWidth(value.dp)` | |
| `setArrowHeight(Int)` | `setArrowHeight(Dp)` | |
| `setArrowHeightResource(@DimenRes)` | `setArrowHeight(value.dp)` | |
| `setArrowOrientation(ArrowOrientation)` | `setArrowOrientation(ArrowOrientation)` | Same enum (re-declared in this package). |
| `setArrowOrientationRules(...)` | _not supported in KMP_ | Auto-derived from `BalloonAlign`; pass an explicit `setArrowOrientation` to fix it. |
| `setArrowPosition(Float)` | `setArrowPosition(Float)` | Now clamped to `0f..1f`. |
| `setArrowPositionRules(...)` | `setArrowPositionRules(...)` | Same enum. |
| `setArrowDrawable(...)` / `setArrowDrawableResource(...)` | _not supported in KMP_ | No `Drawable` in commonMain; arrow is rendered via `BalloonShape`. |
| `setArrowLeftPadding` / `Top` / `Right` / `BottomPadding` (+ `*Resource`) | _not supported in KMP_ | Use `setArrowPosition` to move the arrow along its edge. |
| `setArrowAlignAnchorPadding(...)` (+ `*Resource`) | _not supported in KMP_ | Will be reintroduced when the popup positioner is finalized. |
| `setArrowAlignAnchorPaddingRatio(...)` | _not supported in KMP_ | As above. |
| `setArrowElevation(...)` (+ `*Resource`) | _not supported in KMP_ | Compose has no per-arrow elevation primitive. |
| `setArrowColor(@ColorInt)` | `setArrowColor(Color)` | `Color.Unspecified` inherits the background color. |
| `setArrowColorMatchBalloon(true)` | `setArrowColor(Color.Unspecified)` | Same effect; the unspecified sentinel _is_ "match balloon". |
| `setArrowColorResource(@ColorRes)` | `setArrowColor(Color(0x...))` | |
| `setIsVisibleArrow(false)` | `setArrowSize(0.dp)` | Hiding the arrow == zero-sized arrow. |
| `setBackgroundColor(@ColorInt)` | `setBackgroundColor(Color)` | |
| `setBackgroundColorResource(@ColorRes)` | `setBackgroundColor(Color(0x...))` | |
| `setBackgroundDrawable(...)` (+ `*Resource`) | _not supported in KMP_ | Compose-native rendering uses `BalloonShape`; layer your own painter inside the slot if you need a gradient. |
| `setBalloonStroke(@ColorInt, @Dp Float)` | `setBalloonStroke(Color, Dp)` | Alias for `setBorder`. |
| `setIsClipArrowEnabled(...)` | _not supported in KMP_ | The KMP renderer always clips the arrow into the body shape. |
| `setPadding(Int)` | `setPadding(Dp)` | |
| `setPaddingResource(@DimenRes)` | `setPadding(value.dp)` | |
| `setPadding{Left,Top,Right,Bottom}(Int)` | `setPadding(start, top, end, bottom)` | Use the directional overload. |
| `setPaddingHorizontal(Int)` / `setPaddingVertical(Int)` | `setPaddingHorizontal(Dp)` / `setPaddingVertical(Dp)` | |
| `setPadding{Left,Top,Right,Bottom}Resource(@DimenRes)` | `setPadding(start.dp, top.dp, end.dp, bottom.dp)` | |
| `setMargin*` (all variants) | _not supported in KMP_ | Apply a Compose `Modifier.padding(...)` to the anchor or use `Popup` offsets via `BalloonState.show(xOffset, yOffset)`. |
| `setWidth(Int)` / `setWidthResource` | _not supported in KMP_ | Compose composables size to their content; constrain via the slot's `Modifier`. |
| `setMinWidth*` / `setMaxWidthRatio` / `setWidthRatio` / `setMinWidthRatio` | _not supported in KMP_ | Use `setMaxWidth(Dp)` for an upper bound; otherwise constrain via the slot. |
| `setMaxWidth(Int)` | `setMaxWidth(Dp)` | |
| `setMeasuredWidth(Int)` | _not supported in KMP_ | Anti-pattern in Compose. |
| `setHeight*` / `setSize*` / `setSizeResource` | _not supported in KMP_ | As above. |
| `setText(CharSequence)` (+ `*Resource`) | _not a builder concern_ | Place a `Text(...)` inside the `balloonContent { ... }` slot. |
| `setTextColor(@ColorInt)` (+ `*Resource`) | _not a builder concern_ | Set on the `Text(...)` itself. |
| `setTextSize(Float)` (+ `*Resource`) | _not a builder concern_ | Use `MaterialTheme.typography` or `Text(fontSize = ...)`. |
| `setTextIsHtml(...)` | _not supported in KMP_ | Use `AnnotatedString` / a Markdown library inside the slot. |
| `setMovementMethod(...)` | _not supported in KMP_ | View-only API. |
| `setEnableAutoSized(...)` / `setMin/MaxAutoSizeTextSize(...)` | _not supported in KMP_ | Compose has no equivalent; pre-compute or use `BasicText` with `AutoSize` (Compose 1.7+). |
| `setTextTypeface(Int|Typeface)` | _not supported in KMP_ | `Typeface` is Android-only; pass a `FontFamily` to the `Text(...)` directly. |
| `setTextLineSpacing(...)` / `setTextLetterSpacing(...)` (+ `*Resource`) | _not supported in KMP_ | Set on the `Text(...)` directly. |
| `setIncludeFontPadding(...)` | _not supported in KMP_ | Android-`TextView`-specific. |
| `setTextGravity(Int)` | _not supported in KMP_ | Use `Text(textAlign = ...)`. |
| `setTextForm(TextForm)` | _not supported in KMP_ | View-only abstraction. |
| `setIcon*` (all variants) | _not a builder concern_ | Compose icons go inside the slot — `Row { Icon(...); Text(...) }`. |
| `setAlpha(Float)` | _not supported in KMP_ | Wrap the slot in `Modifier.alpha(...)` if needed. |
| `setElevation(Int)` (+ `*Resource`) | _not supported in KMP_ | Wrap the slot in `Modifier.shadow(...)`. |
| `setLayout(View|@LayoutRes Int|ViewBinding)` | _not a builder concern_ | The whole point of the Compose slot. |
Comment on lines +139 to +147

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape the | characters in these table entries.

Raw pipes inside the type names are breaking the Markdown table structure, which is why markdownlint flags these rows and the rendered docs can lose part of the signature.

Fix
-| `setTextTypeface(Int|Typeface)` | _not supported in KMP_ | `Typeface` is Android-only; pass a `FontFamily` to the `Text(...)` directly. |
+| `setTextTypeface(Int\|Typeface)` | _not supported in KMP_ | `Typeface` is Android-only; pass a `FontFamily` to the `Text(...)` directly. |
...
-| `setLayout(View|`@LayoutRes` Int|ViewBinding)` | _not a builder concern_ | The whole point of the Compose slot. |
+| `setLayout(View\|`@LayoutRes` Int\|ViewBinding)` | _not a builder concern_ | The whole point of the Compose slot. |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 139-139: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 147-147: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@balloon-compose-multiplatform/MIGRATION.md` around lines 139 - 147, The
Markdown table contains raw pipe characters (`|`) within cell content that are
breaking the table structure. Escape these pipes by replacing them with `\|`.
Specifically, in entries like `setTextTypeface(Int|Typeface)` and
`setLayout(View|`@LayoutRes` Int|ViewBinding)`, add backslashes before each pipe
character that appears within the type signatures and method names to properly
escape them in the Markdown table format.

Source: Linters/SAST tools

| `setIsVisibleOverlay(...)` and the entire `Overlay*` family | _not yet supported_ | Overlay highlighting will return in a follow-up release. |
| `setOnBalloonClickListener(...)` | _not a builder concern_ | Use `Modifier.clickable { ... }` inside the slot. |
| `setOnBalloonDismissListener(...)` | Observe `BalloonState.isVisible` | Or compose a `LaunchedEffect` keyed on the state. |
| `setOnBalloonInitializedListener(...)` | _not needed in KMP_ | Compose callbacks (`onGloballyPositioned`) fire naturally. |
Comment on lines +150 to +151

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Map the legacy dismiss listener to BalloonState.onDismiss.

BalloonState already exposes a one-shot onDismiss callback with the same visible→hidden semantics, so telling migrators to observe isVisible or wire a LaunchedEffect is less precise and can miss the intended transition.

Fix
-| `setOnBalloonDismissListener(...)` | Observe `BalloonState.isVisible` | Or compose a `LaunchedEffect` keyed on the state. |
+| `setOnBalloonDismissListener(...)` | `state.onDismiss = { ... }` | Direct KMP equivalent; fires once per visible→hidden transition. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `setOnBalloonDismissListener(...)` | Observe `BalloonState.isVisible` | Or compose a `LaunchedEffect` keyed on the state. |
| `setOnBalloonInitializedListener(...)` | _not needed in KMP_ | Compose callbacks (`onGloballyPositioned`) fire naturally. |
| `setOnBalloonDismissListener(...)` | `state.onDismiss = { ... }` | Direct KMP equivalent; fires once per visible→hidden transition. |
| `setOnBalloonInitializedListener(...)` | _not needed in KMP_ | Compose callbacks (`onGloballyPositioned`) fire naturally. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@balloon-compose-multiplatform/MIGRATION.md` around lines 150 - 151, The
migration documentation for setOnBalloonDismissListener is currently directing
users to observe BalloonState.isVisible or use LaunchedEffect, which is
imprecise and misses the intended transition semantics. Update the migration
table entry for setOnBalloonDismissListener to instead map it directly to
BalloonState.onDismiss, since that callback already provides the same
visible→hidden semantics as a one-shot handler and is the more direct and
accurate migration path.

| `setOnBalloonOutsideTouchListener(...)` / `setOnBalloonOverlayTouchListener(...)` / `setOnBalloonOverlayClickListener(...)` | _not yet supported_ | See overlay note above. |
| `setOnBalloonTouchListener(View.OnTouchListener)` | _not supported in KMP_ | `View.OnTouchListener` is Android-only. |
| `setDismissWhenTouchOutside(...)` | `setDismissWhenTouchOutside(Boolean)` | |
| `setDismissWhenBackPressed(...)` | `setDismissWhenBackPressed(Boolean)` | KMP-equivalent dismissal on Escape / back. |
| `setDismissWhenTouchMargin(...)` | _not yet supported_ | Margin model not finalized for KMP. |
| `setDismissWhenShowAgain(...)` | _not needed in KMP_ | `BalloonState.show()` is idempotent. |
| `setDismissWhenClicked(...)` | _not a builder concern_ | Call `state.dismiss()` from the slot's `Modifier.clickable { ... }`. |
| `setDismissWhenOverlayClicked(...)` | _not yet supported_ | See overlay note above. |
| `setDismissWhenLifecycleOnPause(...)` | _not needed in KMP_ | Composition disposal handles this. |
| `setShouldPassTouchEventToAnchor(...)` | _not yet supported_ | Pending popup-host design in commonMain. |
| `setAutoDismissDuration(Long)` | `setAutoDismissDuration(Long)` | Supported — auto-dismisses after the given delay in ms (pass `0L` to disable). |
| `setLifecycleOwner(...)` / `setLifecycleObserver(...)` | _not needed in KMP_ | See above. |
| `setBalloonAnimation(BalloonAnimation)` | `setBalloonAnimation(BalloonAnimation)` | Same enum; `CIRCULAR` is rendered as a scale approximation (see `BalloonTransitions.kt`). |
| `setBalloonAnimationStyle(@StyleRes)` | _not supported in KMP_ | XML animation styles are Android-only. |
| `setBalloonOverlayAnimation*` | _not yet supported_ | Tied to overlay support. |
| `setCircularDuration(Long)` | `setAnimationDurationMillis(Int)` | Generalized to all animations. |
| `setBalloonHighlightAnimation*` / `setBalloonRotationAnimation` | _not yet supported_ | Highlight animations will return as Compose `Modifier`s. |
| `setPreferenceName(...)` / `setShowCounts(...)` / `runIfReachedShowCounts(...)` | _not supported in KMP_ | Use `kotlinx-multiplatform-settings` (or your DI) to gate `state.show()` yourself. |
| `setIsRtlLayout(...)` / `setSupportRtlLayoutFactor(...)` | _not needed in KMP_ | Compose's `LocalLayoutDirection` handles RTL. |
| `setFocusable(...)` | _not yet supported_ | Will be revisited with the popup host. |
| `setIsStatusBarVisible(...)` / `setIsAttachedInDecor(...)` / `setIsClippingEnabled(...)` | _not supported in KMP_ | Android `PopupWindow`-specific. |
| `setIsComposableContent(...)` | _not needed in KMP_ | The slot is _always_ composable in this module. |

## Notes for migrators

- **Resources go inline.** `R.dimen.x` becomes `12.dp`, `R.color.y` becomes
`Color(0xFFAA0000)`, `R.string.z` becomes a `"literal"` (or fetched via
`stringResource(...)` inside the slot composable, _not_ on the builder).
- **Padding tracking.** Internally the new builder tracks four directional
`Dp`s, then assembles a single `PaddingValues` at `build()` time. This keeps
`setPaddingHorizontal` / `setPaddingVertical` orthogonal without needing a
`LayoutDirection` to read an existing `PaddingValues` back.
- **`setIsVisibleArrow(false)`** has no direct equivalent — pass
`setArrowSize(0.dp)` instead. `BalloonShape` already treats zero-sized arrows
as "no arrow."
- **`setArrowColorMatchBalloon(true)`** is the default behavior: leave
`arrowColor` at `Color.Unspecified` and the arrow inherits the background.
- This is **purely additive** — the existing `balloon` and `balloon-compose`
modules are not deprecated. Migrate at your own pace, screen by screen.
Loading